评论

收藏

[Java] Spring Boot支持Crontab任务改造的方法

编程语言 编程语言 发布于:2021-10-05 19:37 | 阅读数:405 | 评论:0

这篇文章主要介绍了Spring Boot支持Crontab任务改造的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
在以往的 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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持CodeAE代码之家
原文链接:https://segmentfault.com/a/1190000017946999

关注下面的标签,发现更多相似文章