摘要:虽然本身支持定时任务,但都是服务一直运行时支持。监听目标对象借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。
在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。其实在项目中,大多数定时任务,还是借助 Linux Crontab 来支持,需要时运行即可,不需要一直占用机器资源。但 Spring Boot 项目或者普通的 jar 项目,就没这么方便了。
Spring Boot 提供了类似 CommandLineRunner 的方式,很好的执行常驻任务;也可以借助 ApplicationListener 和 ContextRefreshedEvent 等事件来做很多事情。借助该容器事件,一样可以做到类似 Ant 运行的方式来运行定时任务,当然需要做一些项目改动。
1. 监听目标对象借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。
比如这是一个写好的例子,注意不要直接用 @Service 将其放入容器中,除非容器本身没有其它自动运行的事件。
package com.github.zhgxun.learn.common.task; import com.github.zhgxun.learn.common.task.annotation.ScheduleTask; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; /** * 不自动加入容器, 用于区分是否属于任务启动, 否则放入容器中, Spring 无法选择性执行 * 需要根据特殊参数在启动时注入 * 该监听器本身不能访问容器变量, 如果需要访问, 需要从上下文中获取对象实例后方可继续访问实例信息 * 如果其它类中启动了多线程, 是无法接管异常抛出的, 需要子线程中正确处理退出操作 * 该监听器最好不用直接做线程操作, 子类的实现不干预 */ @Slf4j public class TaskApplicationListener implements ApplicationListener{ /** * 任务启动监听类标识, 启动时注入 * 即是 java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar learn.jar */ private static final String SPRING_TASK_CLASS = "spring.task.class"; /** * 支持该注解的方法个数, 目前仅一个 * 可以理解为控制台一次执行一个类, 依赖的任务应该通过其它方式控制依赖 */ private static final int SUPPORT_METHOD_COUNT = 1; /** * 保存当前容器运行上下文 */ private ApplicationContext context; /** * 监听容器刷新事件 * * @param event 容器刷新事件 */ @Override @SuppressWarnings("unchecked") public void onApplicationEvent(ContextRefreshedEvent event) { context = event.getApplicationContext(); // 不存在时可能为正常的容器启动运行, 无需关心 String taskClass = System.getProperty(SPRING_TASK_CLASS); log.info("ScheduleTask spring task Class: {}", taskClass); if (taskClass != null) { try { // 获取类字节码文件 Class clazz = findClass(taskClass); // 尝试从内容上下文中获取已加载的目标类对象实例, 这个类实例是已经加载到容器内的对象实例, 即可以获取类的信息 Object object = context.getBean(clazz); Method method = findMethod(object); log.info("start to run task Class: {}, Method: {}", taskClass, method.getName()); invoke(method, object); } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } finally { // 需要确保容器正常出发停止事件, 否则容器会僵尸卡死 shutdown(); } } } /** * 根据class路径名称查找类文件 * * @param clazz 类名称 * @return 类对象 * @throws ClassNotFoundException ClassNotFoundException */ private Class findClass(String clazz) throws ClassNotFoundException { return Class.forName(clazz); } /** * 获取目标对象中符合条件的方法 * * @param object 目标对象实例 * @return 符合条件的方法 */ private Method findMethod(Object object) { Method[] methods = object.getClass().getDeclaredMethods(); List schedules = Stream.of(methods) .filter(method -> method.isAnnotationPresent(ScheduleTask.class)) .collect(Collectors.toList()); if (schedules.size() != SUPPORT_METHOD_COUNT) { throw new IllegalStateException("only one method should be annotated with @ScheduleTask, but found " + schedules.size()); } return schedules.get(0); } /** * 执行目标对象方法 * * @param method 目标方法 * @param object 目标对象实例 * @throws IllegalAccessException IllegalAccessException * @throws InvocationTargetException InvocationTargetException */ private void invoke(Method method, Object object) throws IllegalAccessException, InvocationTargetException { method.invoke(object); } /** * 执行完毕退出运行容器, 并将返回值交给执行环节, 比如控制台等 */ private void shutdown() { log.info("shutdown ..."); System.exit(SpringApplication.exit(context)); } }
其实该处仅需要启动执行即可,容器启动完毕事件也是可以的。
2. 标识目标方法目标方法的标识,最方便的是使用注解标注。
package com.github.zhgxun.learn.common.task.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface ScheduleTask { }3. 编写任务
package com.github.zhgxun.learn.task; import com.github.zhgxun.learn.common.task.annotation.ScheduleTask; import com.github.zhgxun.learn.service.first.LaunchInfoService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service @Slf4j public class TestTask { @Autowired private LaunchInfoService launchInfoService; @ScheduleTask public void test() { log.info("Start task ..."); log.info("LaunchInfoList: {}", launchInfoService.findAll()); log.info("模拟启动线程操作"); for (int i = 0; i < 5; i++) { new MyTask(i).start(); } try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } } class MyTask extends Thread { private int i; private int j; private String s; public MyTask(int i) { this.i = i; } @Override public void run() { super.run(); System.out.println("第 " + i + " 个线程启动..." + Thread.currentThread().getName()); if (i == 2) { throw new RuntimeException("模拟运行时异常"); } if (i == 3) { // 除数不为0 int a = i / j; } // 未对字符串对象赋值, 获取长度报空指针错误 if (i == 4) { System.out.println(s.length()); } } }4. 启动改造
启动时需要做一些调整,即跟普通的启动区分开。这也是为什么不要把监听目标对象直接放入容器中的原因,在这里显示添加到容器中,这样就不影响项目中类似 CommandLineRunner 的功能,毕竟这种功能是容器启动完毕就能运行的。如果要改造,会涉及到很多硬编码。
package com.github.zhgxun.learn; import com.github.zhgxun.learn.common.task.TaskApplicationListener; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @SpringBootApplication public class LearnApplication { public static void main(String[] args) { SpringApplicationBuilder builder = new SpringApplicationBuilder(LearnApplication.class); // 根据启动注入参数判断是否为任务动作即可, 否则不干预启动 if (System.getProperty("spring.task.class") != null) { builder.listeners(new TaskApplicationListener()).run(args); } else { builder.run(args); } } }5. 启动注入
-Dspring.task.class 即是启动注入标识,当然这个标识不要跟默认的参数混淆,需要区分开,否则可能始终获取到系统参数,而无法获取用户参数。
java -Dspring.task.class=com.github.zhgxun.learn.task.TestTask -jar target/learn.jar
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/73078.html
摘要:而我这里定时任务的触发是要通过接口的方式来触发,所以只用实现以下的调度器即可。我这里简单说下任务的调度器,具体的任务类,触发器,任务什么时候执行是由它决定的。遇到的坑解决方式这个是因为不兼容的问题,所以使用是不会出现这个错误的。 实现定时任务的几种方式: 1.使用linux的crontab 优点: 1.使用方式很简单,只要在crontab中写好 2.随时可以修改,不需要...
摘要:本文转载自微信公众号账号,作者为海航生态科技技术研究院大数据开发工程师高颜。文章介绍了海航生态科技舆情大数据平台的容器化改造经验,包括初期技术架构应用容器化架构迁移持续发布与部署。 本文转载自微信公众号Docker(账号:dockerone),作者为海航生态科技技术研究院大数据开发工程师高颜。 文章介绍了海航生态科技舆情大数据平台的容器化改造经验,包括初期技术架构、应用容器化、架构迁...
摘要:前言用组件实现简易的定时任务功能。步骤创建一个启动类注意,是关键,加了这个注解才能启动定时任务。编写定时任务方法可以实现两种定时,一种是每个一段时间执行一次方法,另一种是执行一次方法之后间隔若干时间后再执行下一次。 前言 用Spring-Context组件实现简易的定时任务功能。只可以支持较简单的业务场景,实用价值不高。如果想要投放到生产环境,需要进行一些改造。 步骤 1. pom.x...
阅读 2877·2021-11-11 16:55
阅读 906·2021-09-28 09:36
阅读 3774·2021-09-22 15:22
阅读 2193·2021-09-06 15:12
阅读 1726·2021-08-19 10:55
阅读 2867·2019-08-30 12:52
阅读 480·2019-08-29 14:03
阅读 1185·2019-08-29 12:27