How to Add Dynamic Scheduled Tasks in Spring Boot Without Quartz

This article explains how to implement dynamic creation, deletion, start, and stop of scheduled tasks in a Spring Boot application by customizing the ScheduledTaskRegistrar and using a lightweight thread‑pool scheduler, providing full code examples and a complete runnable solution.

Programmer DD
Programmer DD
Programmer DD
How to Add Dynamic Scheduled Tasks in Spring Boot Without Quartz

In a Spring Boot project, the @EnableScheduling annotation together with @Scheduled can create static scheduled tasks, but they cannot be added, removed, started, or stopped dynamically. The SchedulingConfigurer interface has the same limitation.

Although integrating the Quartz framework is a common solution for dynamic scheduling, the author prefers to avoid adding extra dependencies when the project requirements can be met with a lighter approach.

By examining the source of org.springframework.scheduling.ScheduledTaskRegistrar in the spring‑context jar, it is possible to modify this class to support dynamic addition, removal, start, and stop of tasks.

Thread‑pool configuration

@Configuration
public class SchedulingConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        // core thread count for scheduled tasks
        taskScheduler.setPoolSize(4);
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
        return taskScheduler;
    }
}

Scheduled task wrapper

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

Runnable implementation

public class SchedulingRunnable implements Runnable {
    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
    private String beanName;
    private String methodName;
    private String params;
    public SchedulingRunnable(String beanName, String methodName) {
        this(beanName, methodName, null);
    }
    public SchedulingRunnable(String beanName, String methodName, String params) {
        this.beanName = beanName;
        this.methodName = methodName;
        this.params = params;
    }
    @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;
            if (StringUtils.isNotEmpty(params)) {
                method = target.getClass().getDeclaredMethod(methodName, String.class);
            } else {
                method = 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 exception - 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);
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SchedulingRunnable that = (SchedulingRunnable) o;
        if (params == null) {
            return beanName.equals(that.beanName) && methodName.equals(that.methodName) && that.params == null;
        }
        return beanName.equals(that.beanName) && methodName.equals(that.methodName) && params.equals(that.params);
    }
    @Override
    public int hashCode() {
        if (params == null) {
            return Objects.hash(beanName, methodName);
        }
        return Objects.hash(beanName, methodName, params);
    }
}

Cron task registrar

@Component
public class CronTaskRegistrar implements DisposableBean {
    private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
    @Autowired
    private TaskScheduler taskScheduler;
    public TaskScheduler getScheduler() { return this.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();
        }
    }
    public 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();
    }
}

Entity for scheduled jobs

public class SysJobPO {
    private Integer jobId; // task ID
    private String beanName; // bean name
    private String methodName; // method name
    private String methodParams; // method parameters
    private String cronExpression; // cron expression
    private Integer jobStatus; // 1=normal, 0=paused
    private String remark;
    private Date createTime;
    private Date updateTime;
    // getters and setters omitted for brevity
}

CRUD operations for jobs

Adding a job:

boolean success = sysJobRepository.addSysJob(sysJob);
if (!success) return OperationResUtils.fail("Add failed");
if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
    cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
}
return OperationResUtils.success();

Editing a job (remove old then add new):

boolean success = sysJobRepository.editSysJob(sysJob);
if (!success) return OperationResUtils.fail("Edit failed");
// remove old task if it was running
if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    cronTaskRegistrar.removeCronTask(task);
}
// add new task if status is normal
if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
    cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
}
return OperationResUtils.success();

Deleting a job:

boolean success = sysJobRepository.deleteSysJobById(req.getJobId());
if (!success) return OperationResUtils.fail("Delete failed");
if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    cronTaskRegistrar.removeCronTask(task);
}
return OperationResUtils.success();

Start/stop toggle:

if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    cronTaskRegistrar.addCronTask(task, existedSysJob.getCronExpression());
} else {
    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    cronTaskRegistrar.removeCronTask(task);
}

Loading tasks on application start

@Service
public class SysJobRunner implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(SysJobRunner.class);
    @Autowired
    private ISysJobRepository sysJobRepository;
    @Autowired
    private CronTaskRegistrar cronTaskRegistrar;
    @Override
    public void run(String... args) {
        // Load all normal‑status jobs from the database
        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 have been loaded.");
        }
    }
}

Utility to access Spring beans

@Component
public class SpringContextUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }
    public static Object getBean(String name) { return applicationContext.getBean(name); }
    public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); }
    public static <T> T getBean(String name, Class<T> requiredType) { return applicationContext.getBean(name, requiredType); }
    public static boolean containsBean(String name) { return applicationContext.containsBean(name); }
    public static boolean isSingleton(String name) { return applicationContext.isSingleton(name); }
    public static Class<?> getType(String name) { return applicationContext.getType(name); }
}

The article also includes screenshots of the task list page, execution logs, and the database table design, which illustrate how the dynamic scheduling works in practice.

Task list page
Task list page
Add new task
Add new task
Task database design
Task database design
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.

JavaSpring BootcronDynamic SchedulingTaskScheduler
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.