Dynamic Change of Cron Expression in SpringBoot Scheduling
This article explains how to dynamically modify Cron expressions for scheduled tasks in a SpringBoot application by using @EnableScheduling, @Scheduled, custom interfaces, and a refresh mechanism that registers, updates, or disables tasks at runtime without additional middleware.
In a SpringBoot project, you can enable scheduling support with the @EnableScheduling annotation and quickly create scheduled tasks using the @Scheduled annotation.
@Scheduled supports three ways to define execution times:
cron(expression) : Execute according to a Cron expression.
fixedDelay(period) : Execute with a fixed delay between the end of one execution and the start of the next, regardless of task duration.
fixedRate(period) : Execute at a fixed rate from the start of one execution to the start of the next; if a run is delayed, it will be executed immediately.
The most commonly used method is the Cron‑based mode because of its flexibility.
By default, a method annotated with @Scheduled becomes immutable after initialization. Spring registers all @Scheduled methods after bean creation, parses their parameters, and stores them in a task list. Before the task actually starts, you can still modify its execution period.
You can change the Cron expression via application.properties together with @Value, or load it from a database using a CronTrigger . However, once a task is registered and running, its parameters become fixed and cannot be changed.
To achieve dynamic changes, the article proposes keeping key task information during registration and using another scheduled task to monitor configuration changes. If a change is detected, the old task is cancelled and a new one is registered.
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 core method is getCronExpression() , allowing each service implementation to control its own expression.
@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 needed", taskName); return; }
Optional.ofNullable(scheduledTaskHolder.remove(beanName)).ifPresent(t -> { t.cancel(); cronExpressionHolder.remove(beanName); });
if (ScheduledTaskRegistrar.CRON_DISABLED.equals(expression)) { log.warn("Task [{}] expression 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);
}
});
}
}The key is to keep a reference to the ScheduledTask object, which controls start/stop, and use the special expression "-" to mark a task as disabled.
A separate 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; }
/** Refresh task configuration */
@Scheduled(fixedDelay = 5000)
public void cronTaskConfigRefresh() {
if (appStarted.get() && initializing.compareAndSet(false, true)) {
log.info("Dynamic loading of scheduled tasks started >>>>>");
try { schedulingConfiguration.refresh(); }
finally { initializing.set(false); }
log.info("Dynamic loading of scheduled tasks finished <<<<<");
}
}
@Override
public void run(ApplicationArguments args) {
if (appStarted.compareAndSet(false, true)) { cronTaskConfigRefresh(); }
}
}Three example tasks are provided: a fixed‑interval task, a task that randomly changes its Cron expression, and a task that toggles between enabled and disabled using the "-" marker.
@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 steps are correctly followed, the logs will show task registration, updates, and disabling, demonstrating a lightweight and elegant solution for dynamically changing Cron expressions without additional middleware.
The article concludes that this approach satisfies most project scenarios, and invites readers to share alternative methods.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.