我以为动态停启定时任务一般用quartz,没想到还可以通过ScheduledTaskRegistrar来拓展。但是分布式场景,建议还是用quartz吧!
    在 spring boot 项目中,可以通过 @EnableScheduling 注解和 @Scheduled 注解实现定时任务,也可以通过 SchedulingConfigurer 接口来实现定时任务。但是这两种方式不能动态添加、删除、启动、停止任务。要实现动态增删启停定时任务功能,比较广泛的做法是集成 Quartz 框架。
    但是本人的开发原则是:在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂。查看 spring-context 这个 jar 包中 org.springframework.scheduling.ScheduledTaskRegistrar 这个类的源代码,发现可以通过改造这个类就能实现动态增删启停定时任务功能。
    实践:Spring Boot 实现定时任务的动态增删启停 - 图1
    定时任务列表页
    image.gif
    定时任务执行日志
    添加执行定时任务的线程池配置类

    1. @Configuration
    2. public class SchedulingConfig {
    3. @Bean
    4. public TaskScheduler taskScheduler() {
    5. ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    6. taskScheduler.setPoolSize(4);
    7. taskScheduler.setRemoveOnCancelPolicy(true);
    8. taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
    9. return taskScheduler;
    10. }
    11. }

    添加 ScheduledFuture 的包装类。ScheduledFuture 是 ScheduledExecutorService 定时任务线程池的执行结果。

    1. public final class ScheduledTask {
    2. volatile ScheduledFuture<?> future;
    3. public void cancel() {
    4. ScheduledFuture<?> future = this.future;
    5. if (future != null) {
    6. future.cancel(true);
    7. }
    8. }
    9. }

    添加 Runnable 接口实现类,被定时任务线程池调用,用来执行指定 bean 里面的方法。

    1. public class SchedulingRunnable implements Runnable {
    2. private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);
    3. private String beanName;
    4. private String methodName;
    5. private String params;
    6. public SchedulingRunnable(String beanName, String methodName) {
    7. this(beanName, methodName, null);
    8. }
    9. public SchedulingRunnable(String beanName, String methodName, String params) {
    10. this.beanName = beanName;
    11. this.methodName = methodName;
    12. this.params = params;
    13. }
    14. @Override
    15. public void run() {
    16. logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params);
    17. long startTime = System.currentTimeMillis();
    18. try {
    19. Object target = SpringContextUtils.getBean(beanName);
    20. Method method = null;
    21. if (StringUtils.isNotEmpty(params)) {
    22. method = target.getClass().getDeclaredMethod(methodName, String.class);
    23. } else {
    24. method = target.getClass().getDeclaredMethod(methodName);
    25. }
    26. ReflectionUtils.makeAccessible(method);
    27. if (StringUtils.isNotEmpty(params)) {
    28. method.invoke(target, params);
    29. } else {
    30. method.invoke(target);
    31. }
    32. } catch (Exception ex) {
    33. logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex);
    34. }
    35. long times = System.currentTimeMillis() - startTime;
    36. logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);
    37. }
    38. @Override
    39. public boolean equals(Object o) {
    40. if (this == o) return true;
    41. if (o == null || getClass() != o.getClass()) return false;
    42. SchedulingRunnable that = (SchedulingRunnable) o;
    43. if (params == null) {
    44. return beanName.equals(that.beanName) &&
    45. methodName.equals(that.methodName) &&
    46. that.params == null;
    47. }
    48. return beanName.equals(that.beanName) &&
    49. methodName.equals(that.methodName) &&
    50. params.equals(that.params);
    51. }
    52. @Override
    53. public int hashCode() {
    54. if (params == null) {
    55. return Objects.hash(beanName, methodName);
    56. }
    57. return Objects.hash(beanName, methodName, params);
    58. }
    59. }

    添加定时任务注册类,用来增加、删除定时任务。

    1. @Component
    2. public class CronTaskRegistrar implements DisposableBean {
    3. private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);
    4. @Autowired
    5. private TaskScheduler taskScheduler;
    6. public TaskScheduler getScheduler() {
    7. return this.taskScheduler;
    8. }
    9. public void addCronTask(Runnable task, String cronExpression) {
    10. addCronTask(new CronTask(task, cronExpression));
    11. }
    12. public void addCronTask(CronTask cronTask) {
    13. if (cronTask != null) {
    14. Runnable task = cronTask.getRunnable();
    15. if (this.scheduledTasks.containsKey(task)) {
    16. removeCronTask(task);
    17. }
    18. this.scheduledTasks.put(task, scheduleCronTask(cronTask));
    19. }
    20. }
    21. public void removeCronTask(Runnable task) {
    22. ScheduledTask scheduledTask = this.scheduledTasks.remove(task);
    23. if (scheduledTask != null)
    24. scheduledTask.cancel();
    25. }
    26. public ScheduledTask scheduleCronTask(CronTask cronTask) {
    27. ScheduledTask scheduledTask = new ScheduledTask();
    28. scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());
    29. return scheduledTask;
    30. }
    31. @Override
    32. public void destroy() {
    33. for (ScheduledTask task : this.scheduledTasks.values()) {
    34. task.cancel();
    35. }
    36. this.scheduledTasks.clear();
    37. }
    38. }

    添加定时任务示例类

    1. @Component("demoTask")
    2. public class DemoTask {
    3. public void taskWithParams(String params) {
    4. System.out.println("执行有参示例任务:" + params);
    5. }
    6. public void taskNoParams() {
    7. System.out.println("执行无参示例任务");
    8. }
    9. }

    定时任务数据库表设计
    实践:Spring Boot 实现定时任务的动态增删启停 - 图3
    定时任务数据库表设计
    添加定时任务实体类

    1. public class SysJobPO {
    2. private Integer jobId;
    3. private String beanName;
    4. private String methodName;
    5. private String methodParams;
    6. private String cronExpression;
    7. private Integer jobStatus;
    8. private String remark;
    9. private Date createTime;
    10. private Date updateTime;
    11. public Integer getJobId() {
    12. return jobId;
    13. }
    14. public void setJobId(Integer jobId) {
    15. this.jobId = jobId;
    16. }
    17. public String getBeanName() {
    18. return beanName;
    19. }
    20. public void setBeanName(String beanName) {
    21. this.beanName = beanName;
    22. }
    23. public String getMethodName() {
    24. return methodName;
    25. }
    26. public void setMethodName(String methodName) {
    27. this.methodName = methodName;
    28. }
    29. public String getMethodParams() {
    30. return methodParams;
    31. }
    32. public void setMethodParams(String methodParams) {
    33. this.methodParams = methodParams;
    34. }
    35. public String getCronExpression() {
    36. return cronExpression;
    37. }
    38. public void setCronExpression(String cronExpression) {
    39. this.cronExpression = cronExpression;
    40. }
    41. public Integer getJobStatus() {
    42. return jobStatus;
    43. }
    44. public void setJobStatus(Integer jobStatus) {
    45. this.jobStatus = jobStatus;
    46. }
    47. public String getRemark() {
    48. return remark;
    49. }
    50. public void setRemark(String remark) {
    51. this.remark = remark;
    52. }
    53. public Date getCreateTime() {
    54. return createTime;
    55. }
    56. public void setCreateTime(Date createTime) {
    57. this.createTime = createTime;
    58. }
    59. public Date getUpdateTime() {
    60. return updateTime;
    61. }
    62. public void setUpdateTime(Date updateTime) {
    63. this.updateTime = updateTime;
    64. }
    65. }

    新增定时任务
    实践:Spring Boot 实现定时任务的动态增删启停 - 图4
    新增定时任务

    1. boolean success = sysJobRepository.addSysJob(sysJob);
    2. if (!success)
    3. return OperationResUtils.fail("新增失败");
    4. else {
    5. if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    6. SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
    7. cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
    8. }
    9. }
    10. return OperationResUtils.success();

    修改定时任务,先移除原来的任务,再启动新任务

    1. boolean success = sysJobRepository.editSysJob(sysJob);
    2. if (!success)
    3. return OperationResUtils.fail("编辑失败");
    4. else {
    5. if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    6. SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    7. cronTaskRegistrar.removeCronTask(task);
    8. }
    9. if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    10. SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
    11. cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
    12. }
    13. }
    14. return OperationResUtils.success();

    删除定时任务

    1. boolean success = sysJobRepository.deleteSysJobById(req.getJobId());
    2. if (!success)
    3. return OperationResUtils.fail("删除失败");
    4. else{
    5. if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    6. SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    7. cronTaskRegistrar.removeCronTask(task);
    8. }
    9. }
    10. return OperationResUtils.success();

    定时任务启动 / 停止状态切换

    1. if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
    2. SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    3. cronTaskRegistrar.addCronTask(task, existedSysJob.getCronExpression());
    4. } else {
    5. SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
    6. cronTaskRegistrar.removeCronTask(task);
    7. }

    添加实现了 CommandLineRunner 接口的 SysJobRunner 类,当 spring boot 项目启动完成后,加载数据库里状态为正常的定时任务。

    1. @Service
    2. public class SysJobRunner implements CommandLineRunner {
    3. private static final Logger logger = LoggerFactory.getLogger(SysJobRunner.class);
    4. @Autowired
    5. private ISysJobRepository sysJobRepository;
    6. @Autowired
    7. private CronTaskRegistrar cronTaskRegistrar;
    8. @Override
    9. public void run(String... args) {
    10. List<SysJobPO> jobList = sysJobRepository.getSysJobListByStatus(SysJobStatus.NORMAL.ordinal());
    11. if (CollectionUtils.isNotEmpty(jobList)) {
    12. for (SysJobPO job : jobList) {
    13. SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams());
    14. cronTaskRegistrar.addCronTask(task, job.getCronExpression());
    15. }
    16. logger.info("定时任务已加载完毕...");
    17. }
    18. }
    19. }

    工具类 SpringContextUtils,用来从 spring 容器里获取 bean

    1. @Component
    2. public class SpringContextUtils implements ApplicationContextAware {
    3. private static ApplicationContext applicationContext;
    4. @Override
    5. public void setApplicationContext(ApplicationContext applicationContext)
    6. throws BeansException {
    7. SpringContextUtils.applicationContext = applicationContext;
    8. }
    9. public static Object getBean(String name) {
    10. return applicationContext.getBean(name);
    11. }
    12. public static <T> T getBean(Class<T> requiredType) {
    13. return applicationContext.getBean(requiredType);
    14. }
    15. public static <T> T getBean(String name, Class<T> requiredType) {
    16. return applicationContext.getBean(name, requiredType);
    17. }
    18. public static boolean containsBean(String name) {
    19. return applicationContext.containsBean(name);
    20. }
    21. public static boolean isSingleton(String name) {
    22. return applicationContext.isSingleton(name);
    23. }
    24. public static Class<? extends Object> getType(String name) {
    25. return applicationContext.getType(name);
    26. }
    27. }