In Spring Boot applications, we can enable scheduling support using the @EnableScheduling annotation and create scheduled tasks with the @Scheduled annotation.
The @Scheduled annotation supports three configuration methods for execution timing:
cron(expression): Executes based on a Cron expression.fixedDelay(period): Executes at fixed intervals, regardless of task duration.fixedRate(period): Executes at fixed rates, starting from task initiation.
The most flexible approach is using Cron expressions for scheduling.
Mutable vs Immutable Scheduling
By default, scheduled tasks defined with @Scheduled are immutable after initialization. Spring processes all methods with @Scheduled annotations during bean initialization, parsing annotation parameters and registering tasks for execution. Before tasks actually start, we can modify their execution parameters through configuration files or dynamic loading mechanisms.
However, once tasks are running, we cannot modify their Cron expressions or disable them. The registration parameters become fixed, making the taskss immutable.
Dynamic Task Management Strategy
Since tasks cannot be modified after creation, we can implement a strategy of destroying and recreating tasks when configuration changes occur. The approach involves:
- Storing task metadata during registration
- Periodically checking for configuration updates
- Replacing outdated tasks with new ones when changes are detected
First, let's create an interface to standardize task management:
public interface ISchedulableTask {
/**
* Execute the task
*/
void execute();
/**
* Get the cron expression
*
* @return Cron expression string
*/
default String getCronPattern() {
return null;
}
/**
* Get task identifier
*
* @return Task name
*/
default String getTaskIdentifier() {
return this.getClass().getSimpleName();
}
}
The getCronPattern() method allows each task to control its own scheduling configuration. Now, let's implement dynamic task registration:
@Configuration
@EnableAsync
@EnableScheduling
public class TaskSchedulerConfig implements SchedulingConfigurer, ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(TaskSchedulerConfig.class);
private static ApplicationContext applicationContext;
private final ConcurrentMap<String, ScheduledFuture<?>> activeTasks = new ConcurrentHashMap<>(16);
private final ConcurrentMap<String, String> currentCronPatterns = new ConcurrentHashMap<>(16);
private ScheduledTaskRegistrar taskRegistrar;
public static synchronized void setApplicationContext(ApplicationContext ctx) {
applicationContext = ctx;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
setApplicationContext(applicationContext);
}
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
this.taskRegistrar = registrar;
}
/**
* Refresh scheduled task configurations
*/
public void refreshTaskConfigurations() {
Map<String, ISchedulableTask> taskBeans = applicationContext.getBeansOfType(ISchedulableTask.class);
if (taskBeans.isEmpty() || taskRegistrar == null) {
return;
}
taskBeans.forEach((beanName, task) -> {
String cronExpression = task.getCronPattern();
String taskName = task.getTaskIdentifier();
if (cronExpression == null) {
log.warn("Task [{}] has no valid cron expression configured", taskName);
return;
}
// Check if cron expression has changed
boolean noChanges = activeTasks.containsKey(beanName) &&
currentCronPatterns.get(beanName).equals(cronExpression);
if (noChanges) {
log.info("Task [{}] cron expression unchanged, no refresh needed", taskName);
return;
}
// Cancel existing task if it exists
Optional.ofNullable(activeTasks.remove(beanName)).ifPresent(ScheduledFuture::cancel);
currentCronPatterns.remove(beanName);
// Handle task disabling
if (ScheduledTaskRegistrar.CRON_DISABLED.equals(cronExpression)) {
log.warn("Task [{}] is disabled and will not be scheduled", taskName);
return;
}
// Schedule new task
CronTask cronTask = new CronTask(task::execute, cronExpression);
ScheduledFuture<?> scheduledTask = taskRegistrar.scheduleCronTask(cronTask);
if (scheduledTask != null) {
log.info("Task [{}] loaded with cron pattern [{}]", taskName, cronExpression);
activeTasks.put(beanName, scheduledTask);
currentCronPatterns.put(beanName, cronExpression);
}
});
}
}
The key is maintaining references to ScheduledFuture objects, which allow task control. The special value "-" disables tasks, and they can be reactivated by providing a valid cron expression.
Next, we need a mechanism to trigger configuration refreshes:
@Component
public class TaskConfigMonitor implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(TaskConfigMonitor.class);
private final TaskSchedulerConfig schedulerConfig;
private final AtomicBoolean applicationStarted = new AtomicBoolean(false);
private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
public TaskConfigMonitor(TaskSchedulerConfig schedulerConfig) {
this.schedulerConfig = schedulerConfig;
}
/**
* Periodically refresh task configurations
*/
@Scheduled(fixedDelay = 5000)
public void monitorTaskConfigurations() {
if (applicationStarted.get() && refreshInProgress.compareAndSet(false, true)) {
log.info("Starting dynamic task configuration refresh >>>>>>");
try {
schedulerConfig.refreshTaskConfigurations();
} finally {
refreshInProgress.set(false);
}
log.info("Dynamic task configuration refresh completed <<<<<<");
}
}
@Override
public void run(ApplicationArguments args) {
if (applicationStarted.compareAndSet(false, true)) {
monitorTaskConfigurations();
}
}
}
This refresh mechanism can be triggered by scheduled tasks, manual invocations, or message-driven events depending on your application requirements.
Validation Examples
Let's create three sample tasks to demonstrate the functionality:
@Service
public class FixedIntervalTask implements ISchedulableTask {
@Override
public void execute() {
System.out.println("Executing fixed interval task");
}
@Override
public String getCronPattern() {
return "0/1 * * * * ?";
}
}
@Service
public class DynamicIntervalTask implements ISchedulableTask {
private static final Random random = new SecureRandom();
@Override
public void execute() {
System.out.println("Executing dynamic interval task");
}
@Override
public String getCronPattern() {
return "0/" + (random.nextInt(9) + 1) + " * * * * ?";
}
}
@Service
public class ToggleableTask implements ISchedulableTask {
private String currentExpression = "-";
private static final Map<String, String> expressionToggle = new HashMap<>();
static {
expressionToggle.put("-", "0/1 * * * * ?");
expressionToggle.put("0/1 * * * * ?", "-");
}
@Override
public void execute() {
System.out.println("Executing toggleable task");
}
@Override
public String getCronPattern() {
return currentExpression = expressionToggle.get(currentExpression);
}
}
Expected log output would show task initialization, periodic refreshes, and dynamic chenges in execution patterns.