Backend Development 13 min read

Dynamic Modification of Cron Expressions in SpringBoot Scheduling

This article explains how to dynamically modify Cron expressions for scheduled tasks in SpringBoot applications, covering @EnableScheduling, @Scheduled, mutable and immutable task configurations, custom interfaces, and code examples for registering, refreshing, and managing tasks at runtime without external schedulers.

Top Architect
Top Architect
Top Architect
Dynamic Modification of Cron Expressions in SpringBoot Scheduling

In a SpringBoot project, the @EnableScheduling annotation enables scheduling support, and the @Scheduled annotation quickly creates a series of timed tasks.

@Scheduled supports three ways to define execution times:

cron(expression) : Execute according to a Cron expression.

fixedDelay(period) : Fixed interval execution regardless of task duration.

fixedRate(period) : Fixed rate execution from the start time; if a run is delayed, it is executed immediately.

The most commonly used method is the first one, based on Cron expressions, because it is more flexible.

Mutable and Immutable

By default, a method annotated with @Scheduled becomes immutable after initialization. Spring parses the annotation parameters during bean post‑processing and registers the task before it starts. Before the task actually starts, we can still change its execution period or other parameters.

We can either configure the Cron expression in application.properties together with @Value , or load it from a database or other storage using CronTrigger . This part is mutable.

However, once a task is registered and running, its registration parameters become immutable; the task cannot be disabled or have its Cron expression changed at runtime.

Creation and Destruction

To achieve dynamic changes, we keep the key information of a task during registration and use another scheduled task to monitor configuration changes. If a change is detected, the old task is cancelled and a new one is registered.

First, define a simple abstraction to identify and manage tasks:

public interface IPollableService {
    /** Execute method */
    void poll();

    /** Get Cron expression */
    default String getCronExpression() {
        return null;
    }

    /** Get task name */
    default String getTaskName() {
        return this.getClass().getSimpleName();
    }
}

The crucial method is getCronExpression() , which each implementation can control.

Next, implement dynamic task registration:

@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer, ApplicationContextAware {
    private static final Logger log = LoggerFactory.getLogger(SchedulingConfiguration.class);
    private static ApplicationContext appCtx;
    private final ConcurrentMap
scheduledTaskHolder = new ConcurrentHashMap<>(16);
    private final ConcurrentMap
cronExpressionHolder = new ConcurrentHashMap<>(16);
    private ScheduledTaskRegistrar taskRegistrar;

    public static synchronized void setAppCtx(ApplicationContext ctx) { appCtx = ctx; }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { setAppCtx(applicationContext); }

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { this.taskRegistrar = taskRegistrar; }

    /** Refresh scheduled task expressions */
    public void refresh() {
        Map
beanMap = appCtx.getBeansOfType(IPollableService.class);
        if (beanMap.isEmpty() || taskRegistrar == null) { return; }
        beanMap.forEach((beanName, task) -> {
            String expression = task.getCronExpression();
            String taskName = task.getTaskName();
            if (expression == null) {
                log.warn("Task [{}] expression not configured", taskName);
                return;
            }
            boolean unmodified = scheduledTaskHolder.containsKey(beanName) &&
                                 cronExpressionHolder.get(beanName).equals(expression);
            if (unmodified) {
                log.info("Task [{}] expression unchanged, no refresh", taskName);
                return;
            }
            Optional.ofNullable(scheduledTaskHolder.remove(beanName)).ifPresent(t -> { t.cancel(); cronExpressionHolder.remove(beanName); });
            if (ScheduledTaskRegistrar.CRON_DISABLED.equals(expression)) {
                log.warn("Task [{}] disabled, will not be scheduled", taskName);
                return;
            }
            CronTask cronTask = new CronTask(task::poll, expression);
            ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);
            if (scheduledTask != null) {
                log.info("Task [{}] loaded with expression [{}]", taskName, expression);
                scheduledTaskHolder.put(beanName, scheduledTask);
                cronExpressionHolder.put(beanName, expression);
            }
        });
    }
}

Another component periodically triggers the refresh:

@Component
public class CronTaskLoader implements ApplicationRunner {
    private static final Logger log = LoggerFactory.getLogger(CronTaskLoader.class);
    private final SchedulingConfiguration schedulingConfiguration;
    private final AtomicBoolean appStarted = new AtomicBoolean(false);
    private final AtomicBoolean initializing = new AtomicBoolean(false);

    public CronTaskLoader(SchedulingConfiguration schedulingConfiguration) { this.schedulingConfiguration = schedulingConfiguration; }

    @Scheduled(fixedDelay = 5000)
    public void cronTaskConfigRefresh() {
        if (appStarted.get() && initializing.compareAndSet(false, true)) {
            log.info("Dynamic task loading start");
            try { schedulingConfiguration.refresh(); } finally { initializing.set(false); }
            log.info("Dynamic task loading end");
        }
    }

    @Override
    public void run(ApplicationArguments args) {
        if (appStarted.compareAndSet(false, true)) { cronTaskConfigRefresh(); }
    }
}

Verification

Three sample tasks are created to verify the approach:

@Service
public class CronTaskBar implements IPollableService {
    @Override public void poll() { System.out.println("Say Bar"); }
    @Override public String getCronExpression() { return "0/1 * * * * ?"; }
}
@Service
public class CronTaskFoo implements IPollableService {
    private static final Random random = new SecureRandom();
    @Override public void poll() { System.out.println("Say Foo"); }
    @Override public String getCronExpression() { return "0/" + (random.nextInt(9) + 1) + " * * * * ?"; }
}
@Service
public class CronTaskUnavailable implements IPollableService {
    private String cronExpression = "-";
    private static final Map
map = new HashMap<>();
    static { map.put("-", "0/1 * * * * ?"); map.put("0/1 * * * * ?", "-"); }
    @Override public void poll() { System.out.println("Say Unavailable"); }
    @Override public String getCronExpression() { return (cronExpression = map.get(cronExpression)); }
}

Running the application shows logs indicating which tasks are refreshed, loaded, or disabled, and the console output demonstrates the dynamic behavior.

Conclusion

By refreshing and rebuilding scheduled tasks at runtime, we can dynamically change Cron expressions without introducing external schedulers like Quartz, achieving a lightweight and elegant solution. Readers are encouraged to adapt the approach to their own projects and share alternative methods.

JavaSchedulingSpringBootCronDynamic
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.