经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring Boot » 查看文章
Spring Boot支持Crontab任务改造的方法
来源:jb51  时间:2019/1/21 9:20:39  对本文有异议

在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。其实在项目中,大多数定时任务,还是借助 Linux Crontab 来支持,需要时运行即可,不需要一直占用机器资源。但 Spring Boot 项目或者普通的 jar 项目,就没这么方便了。

Spring Boot 提供了类似 CommandLineRunner 的方式,很好的执行常驻任务;也可以借助 ApplicationListener 和 ContextRefreshedEvent 等事件来做很多事情。借助该容器事件,一样可以做到类似 Ant 运行的方式来运行定时任务,当然需要做一些项目改动。

1. 监听目标对象

借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。

比如这是一个写好的例子,注意不要直接用 @Service 将其放入容器中,除非容器本身没有其它自动运行的事件。

  1. package com.github.zhgxun.learn.common.task;
  2.  
  3. import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.boot.SpringApplication;
  6. import org.springframework.context.ApplicationContext;
  7. import org.springframework.context.ApplicationListener;
  8. import org.springframework.context.event.ContextRefreshedEvent;
  9.  
  10. import java.lang.reflect.InvocationTargetException;
  11. import java.lang.reflect.Method;
  12. import java.util.List;
  13. import java.util.stream.Collectors;
  14. import java.util.stream.Stream;
  15.  
  16. /**
  17. * 不自动加入容器, 用于区分是否属于任务启动, 否则放入容器中, Spring 无法选择性执行
  18. * 需要根据特殊参数在启动时注入
  19. * 该监听器本身不能访问容器变量, 如果需要访问, 需要从上下文中获取对象实例后方可继续访问实例信息
  20. * 如果其它类中启动了多线程, 是无法接管异常抛出的, 需要子线程中正确处理退出操作
  21. * 该监听器最好不用直接做线程操作, 子类的实现不干预
  22. */
  23. @Slf4j
  24. public class TaskApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
  25. /**
  26. * 任务启动监听类标识, 启动时注入
  27. * 即是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar
  28. */
  29. private static final String SPRING_TASK_CLASS = "spring.task.class";
  30.  
  31. /**
  32. * 支持该注解的方法个数, 目前仅一个
  33. * 可以理解为控制台一次执行一个类, 依赖的任务应该通过其它方式控制依赖
  34. */
  35. private static final int SUPPORT_METHOD_COUNT = 1;
  36.  
  37. /**
  38. * 保存当前容器运行上下文
  39. */
  40. private ApplicationContext context;
  41.  
  42. /**
  43. * 监听容器刷新事件
  44. *
  45. * @param event 容器刷新事件
  46. */
  47. @Override
  48. @SuppressWarnings("unchecked")
  49. public void onApplicationEvent(ContextRefreshedEvent event) {
  50. context = event.getApplicationContext();
  51. // 不存在时可能为正常的容器启动运行, 无需关心
  52. String taskClass = System.getProperty(SPRING_TASK_CLASS);
  53. log.info("ScheduleTask spring task Class: {}", taskClass);
  54. if (taskClass != null) {
  55. try {
  56. // 获取类字节码文件
  57. Class clazz = findClass(taskClass);
  58.  
  59. // 尝试从内容上下文中获取已加载的目标类对象实例, 这个类实例是已经加载到容器内的对象实例, 即可以获取类的信息
  60. Object object = context.getBean(clazz);
  61.  
  62. Method method = findMethod(object);
  63.  
  64. log.info("start to run task Class: {}, Method: {}", taskClass, method.getName());
  65. invoke(method, object);
  66. } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
  67. e.printStackTrace();
  68. } finally {
  69. // 需要确保容器正常出发停止事件, 否则容器会僵尸卡死
  70. shutdown();
  71. }
  72. }
  73. }
  74.  
  75. /**
  76. * 根据class路径名称查找类文件
  77. *
  78. * @param clazz 类名称
  79. * @return 类对象
  80. * @throws ClassNotFoundException ClassNotFoundException
  81. */
  82. private Class findClass(String clazz) throws ClassNotFoundException {
  83. return Class.forName(clazz);
  84. }
  85.  
  86. /**
  87. * 获取目标对象中符合条件的方法
  88. *
  89. * @param object 目标对象实例
  90. * @return 符合条件的方法
  91. */
  92. private Method findMethod(Object object) {
  93. Method[] methods = object.getClass().getDeclaredMethods();
  94. List<Method> schedules = Stream.of(methods)
  95. .filter(method -> method.isAnnotationPresent(ScheduleTask.class))
  96. .collect(Collectors.toList());
  97. if (schedules.size() != SUPPORT_METHOD_COUNT) {
  98. throw new IllegalStateException("only one method should be annotated with @ScheduleTask, but found "
  99. + schedules.size());
  100. }
  101. return schedules.get(0);
  102. }
  103.  
  104. /**
  105. * 执行目标对象方法
  106. *
  107. * @param method 目标方法
  108. * @param object 目标对象实例
  109. * @throws IllegalAccessException IllegalAccessException
  110. * @throws InvocationTargetException InvocationTargetException
  111. */
  112. private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException {
  113. method.invoke(object);
  114. }
  115.  
  116. /**
  117. * 执行完毕退出运行容器, 并将返回值交给执行环节, 比如控制台等
  118. */
  119. private void shutdown() {
  120. log.info("shutdown ...");
  121. System.exit(SpringApplication.exit(context));
  122. }
  123. }
  124.  

其实该处仅需要启动执行即可,容器启动完毕事件也是可以的。

2. 标识目标方法

目标方法的标识,最方便的是使用注解标注。

  1. package com.github.zhgxun.learn.common.task.annotation;
  2.  
  3. import java.lang.annotation.Documented;
  4. import java.lang.annotation.ElementType;
  5. import java.lang.annotation.Retention;
  6. import java.lang.annotation.RetentionPolicy;
  7. import java.lang.annotation.Target;
  8.  
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Target(ElementType.METHOD)
  11. @Documented
  12. public @interface ScheduleTask {
  13. }

3. 编写任务

  1. package com.github.zhgxun.learn.task;
  2.  
  3. import com.github.zhgxun.learn.common.task.annotation.ScheduleTask;
  4. import com.github.zhgxun.learn.service.first.LaunchInfoService;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.stereotype.Service;
  8.  
  9. import java.util.concurrent.TimeUnit;
  10.  
  11. @Service
  12. @Slf4j
  13. public class TestTask {
  14.  
  15. @Autowired
  16. private LaunchInfoService launchInfoService;
  17.  
  18. @ScheduleTask
  19. public void test() {
  20. log.info("Start task ...");
  21. log.info("LaunchInfoList: {}", launchInfoService.findAll());
  22.  
  23. log.info("模拟启动线程操作");
  24. for (int i = 0; i < 5; i++) {
  25. new MyTask(i).start();
  26. }
  27.  
  28. try {
  29. TimeUnit.SECONDS.sleep(3);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35.  
  36. class MyTask extends Thread {
  37. private int i;
  38. private int j;
  39. private String s;
  40.  
  41. public MyTask(int i) {
  42. this.i = i;
  43. }
  44.  
  45. @Override
  46. public void run() {
  47. super.run();
  48. System.out.println("第 " + i + " 个线程启动..." + Thread.currentThread().getName());
  49. if (i == 2) {
  50. throw new RuntimeException("模拟运行时异常");
  51. }
  52. if (i == 3) {
  53. // 除数不为0
  54. int a = i / j;
  55. }
  56. // 未对字符串对象赋值, 获取长度报空指针错误
  57. if (i == 4) {
  58. System.out.println(s.length());
  59. }
  60. }
  61. }
  62.  

4. 启动改造

启动时需要做一些调整,即跟普通的启动区分开。这也是为什么不要把监听目标对象直接放入容器中的原因,在这里显示添加到容器中,这样就不影响项目中类似 CommandLineRunner 的功能,毕竟这种功能是容器启动完毕就能运行的。如果要改造,会涉及到很多硬编码。

  1. package com.github.zhgxun.learn;
  2.  
  3. import com.github.zhgxun.learn.common.task.TaskApplicationListener;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.boot.builder.SpringApplicationBuilder;
  6.  
  7. @SpringBootApplication
  8. public class LearnApplication {
  9.  
  10. public static void main(String[] args) {
  11. SpringApplicationBuilder builder = new SpringApplicationBuilder(LearnApplication.class);
  12. // 根据启动注入参数判断是否为任务动作即可, 否则不干预启动
  13. if (System.getProperty("spring.task.class") != null) {
  14. builder.listeners(new TaskApplicationListener()).run(args);
  15. } else {
  16. builder.run(args);
  17. }
  18. }
  19. }
  20.  

5. 启动注入

-Dspring.task.class 即是启动注入标识,当然这个标识不要跟默认的参数混淆,需要区分开,否则可能始终获取到系统参数,而无法获取用户参数。

  1. java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号