Backend Development 11 min read

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 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 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 getBean(Class 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.

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

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.