Spring Boot支持Crontab任务改造

更新日期: 2019-01-19阅读: 2k标签: java

在以往的 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


链接: https://www.fly63.com/article/detial/1841

采用Java的ServerSocket进行编码一个简单的HTTP服务器

诸如tomcat等web服务器中间件简化了我们web的开发成本,但有时候我们或许并不需要这么一个完备的服务器,只是希望做一个简单地处理或者做特殊用途的服务器。

lucene的suggest(搜索提示功能的实现)

首先引入依赖,既然要进行智能联想,那么我们需要为提供联想的数据建立一个联想索引(而不是使用原来的数据索引),既然要建立索引,那么我们需要知道建立索引的数据来源。我们使用一个扩展自InputIterator的类来定义数据来源

HashMap剖析之内部结构

本文是基于Java 8的HashMap进行分析,主要是介绍HashMap中的成员变量和类变量的用途,以及分析HashMap的数据结构。在HashMap中存在多个成员变量和类变量,搞清楚它们的用途有助于我们更深入了解HashMap,下面是它们的介绍:

自定义HttpMessageConverter接受JSON数据

Spring默认使用Jackson处理json数据。实际开发中,在业界中,使用非常受欢迎的fastjson来接受json数据。创建一个项目,在web目录下新建一个assets/js目录,加入jquery和json2的js文件,在lib下加入fastjson的jar文件。

统计两个IP地址之间的IP个数

求两个IP地址之间的IP个数,例如192.18.16.1~192.18.16.5,2001:DB8:0000:0023:0008:0800:200C:417C~2001:DB8:0:23:8:800:200C:417D之间的IP个数?

JSP和JSF之间的区别是什么?

JSP和JSF这两种技术都基于Java,主要用于基于Web的应用程序。那么它们之间有什么区别?下面本篇文章就来给大家简单比较一下JSP和JSF,介绍JSP和JSF之间的区别有哪些,希望对大家有所帮助。

JVM 发生 OOM 的 8 种原因、及解决办法

Java 堆空间:发生频率:5颗星造成原因1、无法在 Java 堆中分配对象 2、吞吐量增加 3、应用程序无意中保存了对象引用,对象无法被 GC 回收 4、应用程序过度使用 finalizer

Java版的7种单例模式

今天看到某一篇文章的一句话 单例DCL 前面加 V 。就这句话让我把 单例模式 又仔细看了一遍。Java 中的 单例模式 是我们一直且经常使用的设计模式之一,大家都很熟悉,所以这篇文章仅仅做我自己记忆。

常问的15个顶级Java多线程面试题

在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得更多职位,那么你应该准备很多关于多线程的问题。面试官会问面试者很多令人混淆的Java线程问题

拦截器和过滤器的区别

拦截器功在对请求权限鉴定方面确实很有用处,在我所参与的这个项目之中,第三方的远程调用每个请求都需要参与鉴定,所以这样做非常方便,而且他是很独立的逻辑,这样做让业务逻辑代码很干净

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!