Backend Development 10 min read

Dynamic Cron Expression Management in SpringBoot Scheduling

This article explains how to enable SpringBoot scheduling with @EnableScheduling and @Scheduled, describes the three scheduling modes (cron, fixedDelay, fixedRate), and provides a complete solution for dynamically updating, disabling, and re‑enabling Cron expressions at runtime using custom interfaces and configuration classes.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Dynamic Cron Expression Management in SpringBoot Scheduling

In a SpringBoot project you can enable scheduling support with the @EnableScheduling annotation and quickly create scheduled tasks using @Scheduled .

@Scheduled supports three ways to define execution time: cron(expression) based on a Cron expression, fixedDelay(period) which runs with a fixed interval regardless of task duration, and fixedRate(period) which runs at a fixed rate and triggers immediately if a previous execution was delayed.

The most commonly used method is the Cron‑based approach because of its flexibility.

Mutable and immutable

By default the methods annotated with @Scheduled become immutable after the bean is initialized; Spring registers them once and they cannot be changed at runtime. However, you can supply the Cron expression through application.properties with @Value or load it from a database via CronTrigger , which makes the schedule mutable.

Changing a Cron expression after a task has already started is not possible out of the box; the registered parameters are fixed.

Creation and destruction

To achieve dynamic updates you can keep the key information of each task, periodically check for configuration changes, cancel the old task and register a new one when needed.

An interface IPollableService is introduced to abstract a pollable task and expose getCronExpression() and getTaskName() methods.

public interface IPollableService {
    void poll();
    default String getCronExpression() { return null; }
    default String getTaskName() { return this.getClass().getSimpleName(); }
}

The SchedulingConfiguration class implements SchedulingConfigurer and holds maps of ScheduledTask and Cron expressions. Its refresh() method iterates over all IPollableService beans, compares the current expression with the stored one, cancels unchanged tasks, and registers new CronTask instances when the expression has changed or when a task is enabled/disabled.

@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer, ApplicationContextAware {
    // ... fields omitted for brevity ...

    public void refresh() {
        Map
beanMap = appCtx.getBeansOfType(IPollableService.class);
        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);
            }
        });
    }
}

A separate CronTaskLoader component implements ApplicationRunner and periodically invokes SchedulingConfiguration.refresh() (every 5 seconds) to keep the schedule up‑to‑date.

@Component
public class CronTaskLoader implements ApplicationRunner {
    // ... fields omitted ...

    @Scheduled(fixedDelay = 5000)
    public void cronTaskConfigRefresh() {
        if (appStarted.get() && initializing.compareAndSet(false, true)) {
            log.info("定时调度任务动态加载开始>>>>>>");
            try { schedulingConfiguration.refresh(); } finally { initializing.set(false); }
            log.info("定时调度任务动态加载结束<<<<<<");
        }
    }

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

Three example services demonstrate the behavior:

CronTaskBar returns a fixed Cron expression and never changes.

CronTaskFoo returns a random Cron expression on each call, simulating a mutable schedule.

CronTaskUnavailable toggles between a valid expression and the special “‑” value that disables the task.

@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)); }
}

When the application runs, the logs show dynamic loading, unchanged tasks being skipped, newly loaded tasks, and disabled tasks being ignored, confirming that the Cron expressions can be refreshed, disabled and re‑enabled without external schedulers like Quartz.

Conclusion

By using Spring’s built‑in scheduling together with a custom refresh mechanism you can dynamically modify Cron expressions, disable and re‑enable tasks in a lightweight manner, suitable for most enterprise projects.

backendJavaschedulingSpringBootcronDynamic
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.