Dynamic Cron Expression Management in Spring Applications

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:

  1. Storing task metadata during registration
  2. Periodically checking for configuration updates
  3. 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.

Tags: Spring Spring Boot scheduling Cron expressions Dynamic Configuration

Posted on Fri, 26 Jun 2026 16:11:50 +0000 by sword