spring schedule 实时更新 cron 表达式,并且立即生效。(单机,非分布式调度,无需quartz)

本文的讨论,仅限于 单机下的调度,不是分布式调度的管理。分布式请参考 xxl-job ,redission分布式锁 等框架

 
  1. 主要解决3个问题:
  2. 1) @Scheduled(cron = "0/5 * * * * ?") 注解写死后,不能更新 cron  表达式;
  3. 2) 即使能更新,也不能立刻生效;
  4. 3) 事务管理失效。
 

总共3个目标:

1》quartz有点重,所以不考虑用quartz实现

2》 实现实时的更新cron,立刻生效;接口调用方式

3》实现事务管理 ,解决定时任务的run方法上直接注解 @Transactional 不生效的问题

效果展示:

$.post("http://localhost:你的端口号/schedule/update/customservice",{cron:"0/2 * * * * ?"})

 

 核心代码只有3 句话

 
  1. // 核心代码只有 3句话:
  2. // 1 获取任务句柄
  3. ScheduledFuture<?> future = taskScheduler.schedule(service.getTask(), service.getTrigger());
  4.  
  5. // 2 使用句柄,终止任务
  6. future.cancel(true);
  7.  
  8. //3 保证事务控制 ,仅对单机事务有效,未考虑分布式事务
  9. ContextLoader.getCurrentWebApplicationContext().getBean(CustomeService.class).run1();
 
 
  1. package com.stormfeng.test.config.schedule;
  2.  
  3. import com.stormfeng.test.model.vo.ResultVo;
  4. import com.stormfeng.test.service.schedule.task.ITriggerTask;
  5. import lombok.NonNull;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.beans.factory.DisposableBean;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10. import org.springframework.scheduling.TaskScheduler;
  11. import org.springframework.scheduling.annotation.EnableScheduling;
  12. import org.springframework.scheduling.annotation.SchedulingConfigurer;
  13. import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
  14. import org.springframework.scheduling.config.ScheduledTaskRegistrar;
  15. import org.springframework.scheduling.support.CronTrigger;
  16.  
  17. import java.util.Collection;
  18. import java.util.HashMap;
  19. import java.util.Map;
  20. import java.util.Set;
  21. import java.util.concurrent.ConcurrentHashMap;
  22. import java.util.concurrent.ScheduledFuture;
  23.  
  24. /**
  25. * @author stormfeng
  26. * @date 2020-11-06 10:29
  27. */
  28. @EnableScheduling
  29. @Configuration
  30. @Slf4j
  31. public class JobsConfigTest implements SchedulingConfigurer, DisposableBean {
  32.  
  33.  
  34. // 自定义,参考 TriggerTask,为了统一在实现类中,调用 getTrigger() 和 getTask()
  35. public Collection<ITriggerTask> scheduledServices;
  36. // 句柄,方便后期获取 future
  37. TaskScheduler taskScheduler;
  38.  
  39. // spring特性: 初始化该类时,自动获取和装配 项目中 所有的子类 ITriggerTask
  40. public JobsConfigTest(Collection<ITriggerTask> scheduledServices) {
  41. this.scheduledServices = scheduledServices;
  42. }
  43.  
  44. /**
  45. * Future handles, to cancel the running jobs
  46. */
  47. private static final Map<String, ScheduledFuture> FUTURE_MAP = new ConcurrentHashMap<>();
  48. /**
  49. * 获取 定时任务的具体的类,用于后期 重启,更新等操作
  50. */
  51. private static final Map<String, ITriggerTask> SERVICE_MAP = new ConcurrentHashMap<>();
  52.  
  53. /**
  54. * 线程池任务调度器
  55. * <p>
  56. * 支持注解方式,@Scheduled(cron = "0/5 * * * * ?")
  57. */
  58. @Bean
  59. public TaskScheduler taskScheduler() {
  60. ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
  61. scheduler.setPoolSize(Runtime.getRuntime().availableProcessors() / 3 + 1);
  62. scheduler.setThreadNamePrefix("TaskScheduler-");
  63. scheduler.setRemoveOnCancelPolicy(true); // 保证能立刻丢弃运行中的任务
  64.  
  65. taskScheduler = scheduler; // 获取 句柄,方便后期获取 future
  66.  
  67. return scheduler;
  68. }
  69.  
  70. /**
  71. * @see <a href='https://www.codota.com/code/java/methods/org.springframework.scheduling.config.ScheduledTaskRegistrar/addTriggerTask'>codota 代码提示工具</a>
  72. */
  73. @Override
  74. public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  75. taskRegistrar.setScheduler(taskScheduler()); // 不用担心,这里的scheduler跟 上面的注解 Bean 是同一个对象,亲自打断点验证
  76.  
  77. if (null != scheduledServices && scheduledServices.size() > 0) {
  78. for (final ITriggerTask service : scheduledServices) {
  79. // old 方式,不推荐,因为无法获取 调度任务的 future 对象。
  80. // taskRegistrar.addTriggerTask(scheduledService.getTask(),scheduledService.getTrigger());
  81.  
  82. //但是,最近发现用该对象也可以拿到 任务的引用,参考 大神博客 。但是该方法有些鸡肋,并不能作为万能的瑞士军刀,所以放弃 。 https://my.oschina.net/u/2411391/blog/3147701
  83. /*Set<ScheduledTask> tasks = taskRegistrar.getScheduledTasks();
  84. for (ScheduledTask task : tasks) {
  85. task.cancel();
  86. }*/
  87.  
  88. ScheduledFuture<?> schedule = taskScheduler.schedule(service.getTask(), service.getTrigger());
  89. FUTURE_MAP.put(service.type().toLowerCase(), schedule);
  90. SERVICE_MAP.put(service.type().toLowerCase(), service);
  91. }
  92. }
  93. }
  94.  
  95. //=============================动态配置 cron 表达式,立刻生效,支持 停止、重启、更新cron==============================================
  96.  
  97. public Object get() {
  98. final Set<String> names = FUTURE_MAP.keySet();
  99. HashMap<String, Object> map = new HashMap<String, Object>();
  100.  
  101. map.put("futures", names);
  102. map.put("services", new HashMap<Object, Object>() {{
  103. for (Map.Entry<String, ITriggerTask> entry : SERVICE_MAP.entrySet()) {
  104. put(entry.getKey(), entry.getValue().getTrigger().getExpression());
  105. }
  106. }});
  107.  
  108. return map.toString();
  109. }
  110.  
  111. /**
  112. * 新增
  113. */
  114. public Object add(@NonNull ITriggerTask task) {
  115. String type = task.type(), cron = task.getTrigger().getExpression();
  116.  
  117. if (FUTURE_MAP.containsKey(type)) {
  118. return "请重新指定 任务的 type 属性";
  119. }
  120.  
  121. ScheduledFuture<?> future = taskScheduler.schedule(task.getTask(), task.getTrigger());
  122. FUTURE_MAP.put(type, future);
  123. SERVICE_MAP.put(type, task);
  124.  
  125. String format = String.format("添加新任务成功: :[%s],[%s]", type, cron);
  126. log.info(format);
  127. return format;
  128. }
  129.  
  130. /**
  131. * 更新
  132. */
  133. public void update(@NonNull final String type, @NonNull final String cron) {
  134. if (!FUTURE_MAP.containsKey(type)) {
  135. return;
  136. }
  137. //BUG 修复
  138. ScheduledFuture future = FUTURE_MAP.get(type);
  139. if (future != null) {
  140. future.cancel(true);
  141. }
  142.  
  143. ITriggerTask service = SERVICE_MAP.get(type);
  144. CronTrigger old = service.getTrigger(), newTri = service.setTrigger(cron);
  145.  
  146. ScheduledFuture<?> future = taskScheduler.schedule(service.getTask(), newTri);
  147. FUTURE_MAP.put(type, future); // 必须更新一下对象,否则下次cencel 会失败
  148. }
  149.  
  150. /**
  151. * 取消
  152. */
  153. public Object cancel(@NonNull String type) {
  154. if (!FUTURE_MAP.containsKey(type)) {
  155. return "取消失败,不存在该任务,请检查 type: " + type;
  156. }
  157.  
  158. ScheduledFuture future = FUTURE_MAP.get(type);
  159. if (future != null) {
  160. future.cancel(true);
  161. }
  162.  
  163. FUTURE_MAP.remove(type);
  164.  
  165. return "成功取消执行中的任务 : " + type;
  166. }
  167.  
  168. /**
  169. * 重启已经存在的任务
  170. */
  171. public Object restart(@NonNull String type) {
  172. ITriggerTask service = SERVICE_MAP.get(type);
  173. if (service == null) {
  174. return "无法启动任务,请检查 type: " + type;
  175. }
  176.  
  177. if (FUTURE_MAP.containsKey(type)) {
  178. ScheduledFuture future = FUTURE_MAP.get(type);
  179. if (future != null) {
  180. future.cancel(true);
  181. }
  182. }
  183.  
  184. ScheduledFuture<?> future = taskScheduler.schedule(service.getTask(), service.getTrigger());
  185. FUTURE_MAP.put(type, future); // 必须更新一下对象,否则下次cencel 会失败
  186.  
  187. return "成功重启任务 type: " + type + ",cron: " + service.getTrigger().getExpression();
  188. }
  189.  
  190. @Override
  191. public void destroy() throws Exception {
  192. for (ScheduledFuture future : FUTURE_MAP.values()) {
  193. if (future != null) {
  194. future.cancel(true);
  195. }
  196. }
  197. FUTURE_MAP.clear();
  198. SERVICE_MAP.clear();
  199.  
  200. ((ThreadPoolTaskScheduler) taskScheduler).destroy();
  201.  
  202. }
  203.  
  204.  
  205. }
 

其中,上面的 用到的自定义的接口 ITriggerTask  

 
  1. /**
  2. * TriggerTask 必须实现的方法,为了支持动态配置 cron表达式,所以
  3. *
  4. * @author stormfeng
  5. * @date 2020-11-03 11:21
  6. */
  7. public interface ITriggerTask {
  8. /**
  9. * 获取 类别,区分 不同的Bean 对象
  10. * @return
  11. */
  12. String type();
  13.  
  14. /**
  15. * 获取 run 方法
  16. * @return
  17. */
  18. Runnable getTask();
  19.  
  20. /**
  21. * 获取触发器,一般是 CronTrigger
  22. * @return
  23. */
  24. CronTrigger getTrigger();
  25.  
  26. /**
  27. * 接口 动态修改 定时任务的表达式
  28. */
  29. CronTrigger setTrigger(String cron);
  30. }
 

默认的父类实现,以后的所有类,均应该继承该父类,这样可以简化子类实现类的 type() 方法,  子类可以重写 其他三个方法

 
  1. /**
  2. * @author stormfeng
  3. * @date 2020-11-04 16:49
  4. */
  5. @Slf4j
  6. public abstract class TriggerTaskSupport implements ITriggerTask {
  7.  
  8. @Override
  9. public String type() {
  10. return this.getClass().getSimpleName().toLowerCase();
  11. }
  12.  
  13. @Override
  14. public String toString() {
  15. return "TriggerTask{" +
  16. "type=" + type() +
  17. ", task=" + getTask() +
  18. "cronTrigger=" + getTrigger().getExpression() +
  19. '}';
  20. }
  21. }
 

但是子类 extends TriggerTaskSupport 后, 还是要重写其他三个方法的: 

 
  1. Runnable getTask();
  2. CronTrigger getTrigger();
  3. CronTrigger setTrigger(String cron);
 

至此,上面的代码完全可以拷贝到你的项目中,下面 是你需要 自己自定义的具体的任务实现类
借助 lombok 简化写法,示例如下

 

 
  1. @Service
  2. @Slf4j(topic = "自定义的定时任务1")
  3. public class CustomService extends TriggerTaskSupport {
  4.  
  5. @Getter
  6. @Builder.Default
  7. private CronTrigger trigger = new CronTrigger("0 0 0/6 * * ?");
  8.  
  9. @Override
  10. public CronTrigger setTrigger(String expression) {
  11. String old = trigger.getExpression();
  12. this.trigger = new CronTrigger(expression);
  13. log.info("update cron success, old: {} , new: {}", old, trigger.getExpression());
  14.  
  15. return this.trigger;
  16. }
  17.  
  18. @Getter
  19. @Builder.Default
  20. private Runnable task = new Runnable() {
  21. @Override
  22. public void run() {
  23. System.out.println("\n");
  24. log.info("================start runnig================");
  25. // service.run(); // 该service 是另外一个类的对象,这样才能 使得事务起作用
  26.  
  27.  
  28. // 也可以 用当前的Bean 对象 作为 target 调用,才能被AOP 拦截,进而达到事务管理的目的
  29. // ContextLoader.getCurrentWebApplicationContext().getBean(CustomService.class).run1();
  30. log.info("================ end runnig================");
  31. }
  32. };
  33.  
  34. /* 测试专用 ,使用当前类的 Bean对象. run1 方法,事务控制 也能生效
  35. @Transactional(value = "txManager", rollbackFor = Exception.class)
  36. public void run1() {
  37. int i = jdbcTemplate.update(" INSERT INTO T_TEST VALUES(555555)", null);
  38. int a = 1 / 0;
  39. }*/
  40. }
 

如此,所有代码配置完成,以后如果再次新增一个任务,就可以 参考 上面这个 CustomService  ,新增一个class 就行了

那么,怎么用对外开放接口,接受http请求,到动态实时的修改定时任务呢? 很明显,我们还需要 controller层,示例如下:

 
  1. @Autowired
  2. JobsConfigTest jobsConfigTest;
  3.  
  4. /**
  5. * 更新 定时任务
  6. */
  7. @PostMapping("/schedule/{op}/{type}")
  8. public Object update(@PathVariable String op, @PathVariable String type, String cron) {
  9. type= type.toLowerCase();
  10. switch (op.toLowerCase()) {
  11. case "update":
  12. return jobsConfigTest.update(type, cron);
  13. case "cancel":
  14. case "delete":
  15. return jobsConfigTest.cancel(type);
  16. case "restart":
  17. case "reload":
  18. return jobsConfigTest.restart(type);
  19. default:
  20. return jobsConfigTest.get();
  21. }
  22. }
 

大功告成,以上代码纯手打,参考了国内外一些大神的分享,就此告辞,后会有期!

参考1:篇幅太长不看系列 

参考2: Dynamic Task Scheduling with Spring - MBcoder

参考3:Spring内置任务调度实现添加、取消、重置_蒋固金的博客-CSDN博客_scheduledtaskregistrar如何初始化

参考4:stackoverflow 

参考5:插件codota 的代码提示 

codota代码提示
标题

参考6:注解 @Scheduled配置方式的任务,如何重启?
重启Spring Scheduler的正确打开方式 - Night Field's Blog

posted @ 2025-01-17 09:57  CharyGao  阅读(71)  评论(0)    收藏  举报