Implementing Dynamic Add/Delete/Start/Stop Scheduled Tasks in Spring Boot

This article explains how to overcome Spring Boot's static @Scheduled limitation by customizing the task scheduler, creating wrapper classes and a registrar to dynamically add, remove, start, and stop cron‑based jobs, with full code examples and a database‑driven design.

Top Architect
Top Architect
Top Architect
Implementing Dynamic Add/Delete/Start/Stop Scheduled Tasks in Spring Boot

In a Spring Boot project the built‑in @EnableScheduling and @Scheduled annotations can create scheduled jobs, but they cannot be added, removed, started or stopped at runtime.

To achieve dynamic management the article suggests either integrating Quartz or modifying the internal org.springframework.scheduling.ScheduledTaskRegistrar. It demonstrates a lightweight solution that keeps the project lean.

@Configuration
public class SchedulingConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(4);
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
        return taskScheduler;
    }
}

A ScheduledTask wrapper holds the ScheduledFuture returned by the executor and provides a cancel() method.

public final class ScheduledTask {
    volatile ScheduledFuture<?> future;
    public void cancel() {
        ScheduledFuture<?> future = this.future;
        if (future != null) {
            future.cancel(true);
        }
    }
}

The core runnable SchedulingRunnable logs execution, obtains the target bean from the Spring context, uses reflection to invoke the specified method (with or without parameters), and records execution time. It also overrides equals and hashCode for proper map handling.

public class SchedulingRunnable implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
    private String beanName;
    private String methodName;
    private String params;
    // constructors omitted for brevity
    @Override
    public void run() {
        logger.info("Task start - bean:{}, method:{}, params:{}", beanName, methodName, params);
        long startTime = System.currentTimeMillis();
        try {
            Object target = SpringContextUtils.getBean(beanName);
            Method method = StringUtils.isNotEmpty(params)
                ? target.getClass().getDeclaredMethod(methodName, String.class)
                : target.getClass().getDeclaredMethod(methodName);
            ReflectionUtils.makeAccessible(method);
            if (StringUtils.isNotEmpty(params)) {
                method.invoke(target, params);
            } else {
                method.invoke(target);
            }
        } catch (Exception ex) {
            logger.error(String.format("Task error - bean:%s, method:%s, params:%s", beanName, methodName, params), ex);
        }
        long times = System.currentTimeMillis() - startTime;
        logger.info("Task end - bean:{}, method:{}, params:{}, cost:{} ms", beanName, methodName, params, times);
    }
    // equals and hashCode omitted
}

A CronTaskRegistrar component maintains a ConcurrentHashMap<Runnable, ScheduledTask> and provides methods to add, remove, and schedule cron tasks. It also implements DisposableBean to cancel all tasks on shutdown.

@Component
public class CronTaskRegistrar implements DisposableBean {
    private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
    @Autowired
    private TaskScheduler taskScheduler;
    public void addCronTask(Runnable task, String cronExpression) {
        addCronTask(new CronTask(task, cronExpression));
    }
    public void addCronTask(CronTask cronTask) {
        if (cronTask != null) {
            Runnable task = cronTask.getRunnable();
            if (this.scheduledTasks.containsKey(task)) {
                removeCronTask(task);
            }
            this.scheduledTasks.put(task, scheduleCronTask(cronTask));
        }
    }
    public void removeCronTask(Runnable task) {
        ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
        if (scheduledTask != null) scheduledTask.cancel();
    }
    private ScheduledTask scheduleCronTask(CronTask cronTask) {
        ScheduledTask scheduledTask = new ScheduledTask();
        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
        return scheduledTask;
    }
    @Override
    public void destroy() {
        for (ScheduledTask task : this.scheduledTasks.values()) {
            task.cancel();
        }
        this.scheduledTasks.clear();
    }
}

An example bean DemoTask shows a method with parameters and a no‑arg method that can be scheduled.

@Component("demoTask")
public class DemoTask {
    public void taskWithParams(String params) {
        System.out.println("Executing param task: " + params);
    }
    public void taskNoParams() {
        System.out.println("Executing no‑arg task");
    }
}

The persistent model SysJobPO stores job metadata (id, bean name, method name, parameters, cron expression, status, remarks, timestamps) with standard getters and setters.

Service‑level code demonstrates how to add, edit, delete, and toggle the status of jobs. When a job is added or its status becomes NORMAL, a SchedulingRunnable is created and registered via cronTaskRegistrar.addCronTask. When a job is removed or paused, the corresponding runnable is removed from the registrar.

At application startup a SysJobRunner implements CommandLineRunner to load all enabled jobs from the database and register them, ensuring that previously persisted schedules are active.

@Service
public class SysJobRunner implements CommandLineRunner {
    @Autowired private ISysJobRepository sysJobRepository;
    @Autowired private CronTaskRegistrar cronTaskRegistrar;
    @Override
    public void run(String... args) {
        List<SysJobPO> jobList = sysJobRepository.getSysJobListByStatus(SysJobStatus.NORMAL.ordinal());
        if (CollectionUtils.isNotEmpty(jobList)) {
            for (SysJobPO job : jobList) {
                SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams());
                cronTaskRegistrar.addCronTask(task, job.getCronExpression());
            }
            logger.info("All scheduled jobs loaded.");
        }
    }
}

A utility class SpringContextUtils implements ApplicationContextAware to expose static methods for retrieving beans by name or type from the Spring container.

@Component
public class SpringContextUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        applicationContext = ctx;
    }
    public static Object getBean(String name) { return applicationContext.getBean(name); }
    public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); }
    // other helper methods omitted
}

The article also includes screenshots of the task list page, execution logs, and the UI for adding a new scheduled job, illustrating the complete end‑to‑end solution.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentSpring BootDynamic Schedulingtask schedulerScheduled Tasks
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

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.