Java SpringBoot 定时任务
在SpringBoot项目中,可以通过@EnableScheduling注解和@Scheduled注解实现定时任务,也可以通过SchedulingConfigurer接口来实现定时任务。但是这两种方式不能动态添加、删除、启动、停止任务。
要实现动态增删启停定时任务功能,比较广泛的做法是集成Quartz框架。但是在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂。
查看spring-context这个jar包中org.springframework.scheduling.ScheduledTaskRegistrar这个类的源代码,发现可以通过改造这个类就能实现动态增删启停定时任务功能。
2021-12-24-20-14-02-874109.png
2021-12-24-20-14-03-239413.png

添加执行定时任务的线程池配置类

  1. @Configuration
  2. public class SchedulingConfig {
  3. @Bean
  4. public TaskScheduler taskScheduler() {
  5. ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
  6. // 定时任务执行线程池核心线程数
  7. taskScheduler.setPoolSize(4);
  8. taskScheduler.setRemoveOnCancelPolicy(true);
  9. taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");
  10. return taskScheduler;
  11. }
  12. }

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

  1. public final class ScheduledTask {
  2. volatile ScheduledFuture<?> future;
  3. /**
  4. * 取消定时任务
  5. */
  6. public void cancel() {
  7. ScheduledFuture<?> future = this.future;
  8. if (future != null) {
  9. future.cancel(true);
  10. }
  11. }
  12. }

添加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. }

定时任务数据库表设计

2021-12-24-20-14-03-441653.png

添加定时任务实体类

  1. public class SysJobPO {
  2. /**
  3. * 任务ID
  4. */
  5. private Integer jobId;
  6. /**
  7. * bean名称
  8. */
  9. private String beanName;
  10. /**
  11. * 方法名称
  12. */
  13. private String methodName;
  14. /**
  15. * 方法参数
  16. */
  17. private String methodParams;
  18. /**
  19. * cron表达式
  20. */
  21. private String cronExpression;
  22. /**
  23. * 状态(1正常 0暂停)
  24. */
  25. private Integer jobStatus;
  26. /**
  27. * 备注
  28. */
  29. private String remark;
  30. /**
  31. * 创建时间
  32. */
  33. private Date createTime;
  34. /**
  35. * 更新时间
  36. */
  37. private Date updateTime;
  38. public Integer getJobId() {
  39. return jobId;
  40. }
  41. public void setJobId(Integer jobId) {
  42. this.jobId = jobId;
  43. }
  44. public String getBeanName() {
  45. return beanName;
  46. }
  47. public void setBeanName(String beanName) {
  48. this.beanName = beanName;
  49. }
  50. public String getMethodName() {
  51. return methodName;
  52. }
  53. public void setMethodName(String methodName) {
  54. this.methodName = methodName;
  55. }
  56. public String getMethodParams() {
  57. return methodParams;
  58. }
  59. public void setMethodParams(String methodParams) {
  60. this.methodParams = methodParams;
  61. }
  62. public String getCronExpression() {
  63. return cronExpression;
  64. }
  65. public void setCronExpression(String cronExpression) {
  66. this.cronExpression = cronExpression;
  67. }
  68. public Integer getJobStatus() {
  69. return jobStatus;
  70. }
  71. public void setJobStatus(Integer jobStatus) {
  72. this.jobStatus = jobStatus;
  73. }
  74. public String getRemark() {
  75. return remark;
  76. }
  77. public void setRemark(String remark) {
  78. this.remark = remark;
  79. }
  80. public Date getCreateTime() {
  81. return createTime;
  82. }
  83. public void setCreateTime(Date createTime) {
  84. this.createTime = createTime;
  85. }
  86. public Date getUpdateTime() {
  87. return updateTime;
  88. }
  89. public void setUpdateTime(Date updateTime) {
  90. this.updateTime = updateTime;
  91. }
  92. }

新增定时任务

2021-12-24-20-14-03-581055.png

  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. //先移除再添加
  6. if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
  7. SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());
  8. cronTaskRegistrar.removeCronTask(task);
  9. }
  10. if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {
  11. SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());
  12. cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());
  13. }
  14. }
  15. 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类,当SpringBoot项目启动完成后,加载数据库里状态为正常的定时任务。

  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. // 初始加载数据库里状态为正常的定时任务
  11. List<SysJobPO> jobList = sysJobRepository.getSysJobListByStatus(SysJobStatus.NORMAL.ordinal());
  12. if (CollectionUtils.isNotEmpty(jobList)) {
  13. for (SysJobPO job : jobList) {
  14. SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams());
  15. cronTaskRegistrar.addCronTask(task, job.getCronExpression());
  16. }
  17. logger.info("定时任务已加载完毕...");
  18. }
  19. }
  20. }

工具类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. }