Spring Boot支持Crontab任务改造的方法
在以往的Tomcat项目中,一直习惯用Ant打包,使用build.xml配置,通过ant-buildfile的方式在机器上执行定时任务。虽然Spring本身支持定时任务,但都是服务一直运行时支持。其实在项目中,大多数定时任务,还是借助LinuxCrontab来支持,需要时运行即可,不需要一直占用机器资源。但SpringBoot项目或者普通的jar项目,就没这么方便了。
SpringBoot提供了类似CommandLineRunner的方式,很好的执行常驻任务;也可以借助ApplicationListener和ContextRefreshedEvent等事件来做很多事情。借助该容器事件,一样可以做到类似Ant运行的方式来运行定时任务,当然需要做一些项目改动。
1.监听目标对象
借助容器刷新事件来监听目标对象即可,可以认为,定时任务其实每次只是执行一种操作而已。
比如这是一个写好的例子,注意不要直接用@Service将其放入容器中,除非容器本身没有其它自动运行的事件。
packagecom.github.zhgxun.learn.common.task; importcom.github.zhgxun.learn.common.task.annotation.ScheduleTask; importlombok.extern.slf4j.Slf4j; importorg.springframework.boot.SpringApplication; importorg.springframework.context.ApplicationContext; importorg.springframework.context.ApplicationListener; importorg.springframework.context.event.ContextRefreshedEvent; importjava.lang.reflect.InvocationTargetException; importjava.lang.reflect.Method; importjava.util.List; importjava.util.stream.Collectors; importjava.util.stream.Stream; /** *不自动加入容器,用于区分是否属于任务启动,否则放入容器中,Spring无法选择性执行 *需要根据特殊参数在启动时注入 *该监听器本身不能访问容器变量,如果需要访问,需要从上下文中获取对象实例后方可继续访问实例信息 *如果其它类中启动了多线程,是无法接管异常抛出的,需要子线程中正确处理退出操作 *该监听器最好不用直接做线程操作,子类的实现不干预 */ @Slf4j publicclassTaskApplicationListenerimplementsApplicationListener{ /** *任务启动监听类标识,启动时注入 *即是java-Dspring.task.class=com.github.zhgxun.learn.task.TestTask-jarlearn.jar */ privatestaticfinalStringSPRING_TASK_CLASS="spring.task.class"; /** *支持该注解的方法个数,目前仅一个 *可以理解为控制台一次执行一个类,依赖的任务应该通过其它方式控制依赖 */ privatestaticfinalintSUPPORT_METHOD_COUNT=1; /** *保存当前容器运行上下文 */ privateApplicationContextcontext; /** *监听容器刷新事件 * *@paramevent容器刷新事件 */ @Override @SuppressWarnings("unchecked") publicvoidonApplicationEvent(ContextRefreshedEventevent){ context=event.getApplicationContext(); //不存在时可能为正常的容器启动运行,无需关心 StringtaskClass=System.getProperty(SPRING_TASK_CLASS); log.info("ScheduleTaskspringtaskClass:{}",taskClass); if(taskClass!=null){ try{ //获取类字节码文件 Classclazz=findClass(taskClass); //尝试从内容上下文中获取已加载的目标类对象实例,这个类实例是已经加载到容器内的对象实例,即可以获取类的信息 Objectobject=context.getBean(clazz); Methodmethod=findMethod(object); log.info("starttoruntaskClass:{},Method:{}",taskClass,method.getName()); invoke(method,object); }catch(ClassNotFoundException|IllegalAccessException|InvocationTargetExceptione){ e.printStackTrace(); }finally{ //需要确保容器正常出发停止事件,否则容器会僵尸卡死 shutdown(); } } } /** *根据class路径名称查找类文件 * *@paramclazz类名称 *@return类对象 *@throwsClassNotFoundExceptionClassNotFoundException */ privateClassfindClass(Stringclazz)throwsClassNotFoundException{ returnClass.forName(clazz); } /** *获取目标对象中符合条件的方法 * *@paramobject目标对象实例 *@return符合条件的方法 */ privateMethodfindMethod(Objectobject){ 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){ thrownewIllegalStateException("onlyonemethodshouldbeannotatedwith@ScheduleTask,butfound" +schedules.size()); } returnschedules.get(0); } /** *执行目标对象方法 * *@parammethod目标方法 *@paramobject目标对象实例 *@throwsIllegalAccessExceptionIllegalAccessException *@throwsInvocationTargetExceptionInvocationTargetException */ privatevoidinvoke(Methodmethod,Objectobject)throwsIllegalAccessException,InvocationTargetException{ method.invoke(object); } /** *执行完毕退出运行容器,并将返回值交给执行环节,比如控制台等 */ privatevoidshutdown(){ log.info("shutdown..."); System.exit(SpringApplication.exit(context)); } }
其实该处仅需要启动执行即可,容器启动完毕事件也是可以的。
2.标识目标方法
目标方法的标识,最方便的是使用注解标注。
packagecom.github.zhgxun.learn.common.task.annotation;
importjava.lang.annotation.Documented;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public@interfaceScheduleTask{
}
3.编写任务
packagecom.github.zhgxun.learn.task;
importcom.github.zhgxun.learn.common.task.annotation.ScheduleTask;
importcom.github.zhgxun.learn.service.first.LaunchInfoService;
importlombok.extern.slf4j.Slf4j;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;
importjava.util.concurrent.TimeUnit;
@Service
@Slf4j
publicclassTestTask{
@Autowired
privateLaunchInfoServicelaunchInfoService;
@ScheduleTask
publicvoidtest(){
log.info("Starttask...");
log.info("LaunchInfoList:{}",launchInfoService.findAll());
log.info("模拟启动线程操作");
for(inti=0;i<5;i++){
newMyTask(i).start();
}
try{
TimeUnit.SECONDS.sleep(3);
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
classMyTaskextendsThread{
privateinti;
privateintj;
privateStrings;
publicMyTask(inti){
this.i=i;
}
@Override
publicvoidrun(){
super.run();
System.out.println("第"+i+"个线程启动..."+Thread.currentThread().getName());
if(i==2){
thrownewRuntimeException("模拟运行时异常");
}
if(i==3){
//除数不为0
inta=i/j;
}
//未对字符串对象赋值,获取长度报空指针错误
if(i==4){
System.out.println(s.length());
}
}
}
4.启动改造
启动时需要做一些调整,即跟普通的启动区分开。这也是为什么不要把监听目标对象直接放入容器中的原因,在这里显示添加到容器中,这样就不影响项目中类似CommandLineRunner的功能,毕竟这种功能是容器启动完毕就能运行的。如果要改造,会涉及到很多硬编码。
packagecom.github.zhgxun.learn;
importcom.github.zhgxun.learn.common.task.TaskApplicationListener;
importorg.springframework.boot.autoconfigure.SpringBootApplication;
importorg.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
publicclassLearnApplication{
publicstaticvoidmain(String[]args){
SpringApplicationBuilderbuilder=newSpringApplicationBuilder(LearnApplication.class);
//根据启动注入参数判断是否为任务动作即可,否则不干预启动
if(System.getProperty("spring.task.class")!=null){
builder.listeners(newTaskApplicationListener()).run(args);
}else{
builder.run(args);
}
}
}
5.启动注入
-Dspring.task.class即是启动注入标识,当然这个标识不要跟默认的参数混淆,需要区分开,否则可能始终获取到系统参数,而无法获取用户参数。
java-Dspring.task.class=com.github.zhgxun.learn.task.TestTask-jartarget/learn.jar
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。