Spring Boot支持Crontab任务改造的方法
作者:广训 发布时间:2023-08-08 20:20:24
在以往的 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<ContextRefreshedEvent> {
/**
* 任务启动监听类标识, 启动时注入
* 即是 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<Method> 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://segmentfault.com/a/1190000017946999
猜你喜欢
- 使用Mybatis-Plus的SqlSessionFactory问题前些日子工作中出现一个问题,项目中使用了MybatisPlus,然后出现
- SpringBoot v2.2以上重复读取Request Body内容一、需求项目有两个场景会用到从Request的Body中读取内容。打印
- 本文实例讲述了C#创建临时文件的方法。分享给大家供大家参考。具体分析如下:C#可以通过Path.GetTempFileName获得一个临时文
- 本文实例讲述了spring mvc 实现获取后端传递的值。分享给大家供大家参考,具体如下:jsp页面怎么获取从后端传递过来的值?JSTL 方
- 概述在使用Spring Boot的时候我们经常使用actuator,健康检查,bus中使用/refresh等。这里记录如何使用注解的方式自定
- 1.前提:已安装JDK14,可以到jdk官网下载,或加QQ群835259695,快速免费下载2.不说没用的直接上jdk14环境下的jpack
- 本文的目的是把json串转成map键值对存储,而且只存储叶节点的数据比如json数据如下:{responseHeader:{status:0
- Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动
- 概述ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存。ArrayList不是
- 如果我们在Intellij Idea中开发好程序,需要部署到远程SSH服务器运行,我们可以使用某些SSH软件的rz功能,也可以使用专用的FT
- 有参数传递的地方都少不了参数校验。在web开发中,前端
- 导读Lombok:可以让你的POJO代码特别简洁,不止简单在BO/VO/DTO/DO等大量使用,还有设计模式,对象对比等MybatisPlu
- 在本文中,我们将介绍二进制搜索相对于简单线性搜索的优势,并介绍它在 Java 中的实现。1. 需要有效的搜索假设我们在wine-sellin
- 本文实例完成人机猜拳互动游戏的开发,供大家参考,具体内容如下阶段一:实验——分析业务,创建用户类1.分析业务,抽象出类、类的特征和行为2.创
- FastJson是阿里开源的一个高性能的JSON框架,FastJson数据处理速度快,无论序列化(把JavaBean对象转化成Json格式的
- maven-compiler-plugin编译Java源码,一般只需设置编译的jdk版本<plugin> <g
- 线程状态NEW:刚创建未启动的线程RUNNABLE:正在执行状态BLOCKED:处于阻塞状态的线程WAITING:正在等待另一个线程执行特定
- 该方法把该字符串转换成一个新的字符数组。 String str="abcdefg"; char a[]; a=str.t
- 详解Java中HashSet和TreeSet的区别1. HashSetHashSet有以下特点:不能保证元素的排列顺序,顺序有可能发生变化不
- 介绍众所周知,AOP(面向切面编程)是Spring框架的特色功能之一。通过设置横切关注点(cross cutting concerns),A