Spring Boot 内置定时任务使用

quartz定时任务
placeholder image
admin 发布于:2022-05-09 10:27:55
阅读:loading

在Spring Boot中内置了一套定时任务的实现(TaskSchedulingAutoConfiguration)在spring-context.jar中的scheduling包下,可以很方便的实现定时任务的实现,本篇文章主要围绕定时任务的常规应用和动态增加与删除定时任务的具体实践而来,详细实现与过程如下。

1.定时任务简单应用

package cn.chendd.modules.quartz.job;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * Quartz定时任务管理简单测试
 *
 * @author chendd
 * @date 2022/6/6 0:09
 */
@Component
@EnableScheduling
@Slf4j
public class SimpleScheduledJob {

    /**
     * 固定频率触发
     */
    @Scheduled(fixedRate = 1_000_000)
    public void job1() {
        log.info("按服务器加载完毕开始计算,每间隔1000秒钟执行一次");
    }

    /**
     * cron表达式
     */
    @Scheduled(cron = "0 0/20 17,18,19 * * ?")
    public void job12() {
        log.info("17-19时每间隔20分钟执行一次");
    }

    /**
     * 异步模式
     */
    @Scheduled(cron = "0 0/30 17-19 * * ?")
    @Async
    public void job3() {
        log.info("【异步模式】17-19时每间隔30分钟异步执行一次");
    }

}

知识点说明

(1)本例使用Spring Boot内置的定时任务实现,无需额外增加其它jar依赖;

(2)本例使用了两种常用定时触发策略,固定间隔时常和使用Cron表达式,通常来讲后者更好,可以控制到更细的时间粒度;

(3)Cron表达式的0/20的理解实际上并不能严谨的被理解为每隔20分钟执行一次,应该理解成:从每个0分钟开始计算,到20的倍数时触发;

(4)假设需要配置每隔40分钟执行一次时,则一条Cron表达式并不能满足需要,假如写成如下逻辑“0 0/40 17-19 * * ?”,则触发的详细情况参考如下:

2022-06-12 17:00:00

2022-06-12 17:40:00

2022-06-12 18:00:00

2022-06-12 18:40:00

2022-06-12 19:00:00

2022-06-12 19:40:00

可以看出第二次与第三次、第四次与第五次之间的时间间隔只为20分钟,并不满足每隔40分钟执行的需求,所以解决方式通常有两种,第一种设置成40分钟的一半20分钟触发一次,获取当前小时数的分钟进行判断,分钟不为40或者20时不触发即可,虽然这不是一个科学的搞法,但是也是一种实现方式,通过缩短触发频率,来增加程序内部的逻辑校验,不满足逻辑时不执行具体的逻辑;若仅仅只是40分钟触发,则推荐使用第二种方式,配置两个Cron表达式,以当前为例基数的小时40分钟触发,偶数的小时20分钟触发,即配置成:0 0/40 17-19 * * ? 和 0 20 18 * * ?”;

(5)关于多个时间点可以使用x-y的形式也可以使用逗号分割的多个分割;

(6)若@Async注解标记在类上,表示当前类中所有的定时任务方法都将采用异步模式执行,若标记为某个方法上表示该方法使用异步模式执行,所谓异步与默认同步的区别在于,同步的执行,假设由于执行定时逻辑慢导致的本次还未执行完毕,下次定时频率已经触发时会跳过本次新的触发频率不执行,等上次执行完毕后再触发新的任务;而异步的区别在于定时频率到时开启一个新的线程来执行本次逻辑,与上次执行完成与否不冲突,也就是说定时内部有一个线程池来管理任务,可通过代码或配置来覆盖或跳转线程池的参数。

2.动态定时任务

package cn.chendd.modules.quartz.job;

import ...;

/**
 * 动态定时任务
 *
 * @author chendd
 * @date 2022/6/11 18:38
 */
@Component
public class DynamicScheduledJob implements SchedulingConfigurer {

    private ScheduledTaskRegistrar taskRegistrar;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        this.taskRegistrar = taskRegistrar;
    }

    /**
     * 增加固定频率定时任务
     * @param task 任务实现
     * @param perio 毫秒频率
     */
    public void addFixedScheduler(Runnable task , long perio) {
        this.taskRegistrar.getScheduler().scheduleAtFixedRate(task , perio);
    }

    /**
     * 增加cron表达式频率定时任务
     * @param task 任务实现
     * @param expression cron表达式
     */
    public void addCronScheduler(Runnable task , String expression) {
        this.taskRegistrar.getScheduler().schedule(task , new CronTrigger(expression));
    }

}
package cn.chendd.modules.quartz.controller;

import ...;

/**
 * 定时任务管理Controler
 *
 * @author chendd
 * @date 2022/6/11 20:18
 */
@RestController
@RequestMapping(value = "/quartz", produces = MediaType.APPLICATION_JSON_VALUE)
@Api(value = "定时任务验证接口", tags = {"定时任务管理解析响应验证"})
public class QuartzManageController extends BaseController {

    /**
     * 临时逻辑放置定时任务被重复增加
     */
    private final AtomicBoolean atomicFixed = new AtomicBoolean();
    private final AtomicBoolean atomicCron = new AtomicBoolean();
    @Resource
    private DynamicScheduledJob scheduledJob;

    @GetMapping(value = "/fixed", produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiOperation(value = "增加固定触发频率定时任务", notes = "测试响应结果集(<b>增加固定触发频率定时任务</b>)",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @ApiOperationSupport(order = 10)
    public String fixedTask() {
        if (!atomicFixed.get()) {
            atomicFixed.set(true);
            scheduledJob.addFixedScheduler(() ->
                    System.out.println(String.format("固定频率定时任务执行:%s", DateFormat.formatDatetime())), 5000);
            return "增加固定触发频率为 5秒 的定时任务,请关注控制台";
        }
        return "已存在定时任务,本次不增加【销毁后可再增加】";
    }

    @GetMapping(value = "/cron", produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiOperation(value = "增加Cron表达式触发频率定时任务", notes = "测试响应结果集(<b>增加Cron表达式触发频率定时任务</b>)",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @ApiOperationSupport(order = 20)
    public String cronTask() {
        if (!atomicCron.get()) {
            atomicCron.set(true);
            scheduledJob.addCronScheduler(() ->
                    System.out.println(String.format("Cron表达式定时任务执行:%s", DateFormat.formatDatetime())), "0/5 * * * * ?");
            return "增加Cron表达式触发频率为 5秒 的定时任务,请关注控制台";
        }
        return "已存在定时任务,本次不增加【销毁后可再增加】";
    }

    @GetMapping(value = "/destroy", produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiOperation(value = "销毁定时任务", notes = "测试响应结果集(<b>销毁定时任务</b>)",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @ApiOperationSupport(order = 30)
    public String destroyTask() {
        ScheduledAnnotationBeanPostProcessor processor = SpringBeanFactory.getBean
                (TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME, ScheduledAnnotationBeanPostProcessor.class);
        processor.destroy();
        atomicFixed.set(false);
        atomicFixed.set(false);
        return "已销全部毁定时任务";
    }


}

2.1 知识点说明

(1)实现SchedulingConfigurer接口,获得ScheduledTaskRegistrar对象,通过该对象实现定时任务的动态增加;

(2)提供两种频率的定时增加方法,分别是固定频率和Cron表达式的实现;

(3)实际应用中可能需要再增加一个taskId的容器管理定时任务防止定时任务重复被增加或用于定时任务的删除;

(4)controller中提供了三个函数,可使用swagger在页面上执行实现两种定时任务的动态增加和全部定时任务的销毁,全部定时任务的销毁配合spring中的条件注解,可用于某些场景下禁用掉定时任务的需求(比如项目中存在较多的定时任务或者定时任务的触发频率高,导致控制台日志的输出率占比过大,对于日常编码来讲会造成一定的负担,此时我们可以实现一些场景的定时任务不触发的规则);

(5)关于单个定时任务的删除本次示例由于未维护taskId,所以就不给出具体的实现代码,至于实现方式可参考上述代码中的destroy方法中的任务cancel方法来取消某个定时任务;

(6)实际应用中可以增加服务器启动监听事件,配合ScheduledAnnotationBeanPostProcessor.destroy函数的实现可以实现开发/测试环境定时任务的不执行,来屏蔽定时任务(有些执行频率非常高的定时任务,特别是有一些日志输出的情况下对于开发新功能的控制台输出存在一定的负面影响);

2.2 运行示例

image.png

增加固定频率定时任务(左侧模块动态增加字母前缀)

image.png

(三个定时任务示例)

3.获取定时任务

获取定时任务列表的方式有很多种,经过很长时间的使用和分析,个人建议获取Spring Bean的ScheduledAnnotationBeanPostProcessor组件来实现,该类中可以获取到不同定时任务类型(基于固定频率的、Cron表达式的等),获取到各个任务类型中的定时任务所在的类,以及所在类的任务方法,不仅如此,还可以获得任务的触发器,通过触发器可以得到任务的执行时间等参数信息。PS:任务的触发器数据对象是一个私有的成员变量,如果需要获取其中参数需要进行反射。如下图可以看到各种类型的定时任务的结构:

image.png

如上图可以看到各种不通类型的定时任务的数量,以及各个定时所在的类和方法,实际在分析定时任务的参数时可以从scheduledTasks对象中获得参数,比如上次执行时间,参考如下图所示:

image.png

package cn.chendd.modules.quartz.controller;

import ...;

/**
 * Quartz列表Controller定义
 *
 * @author chendd
 * @date 2023/3/25 10:24
 */
@RestController
@RequestMapping(value = "/quartz", produces = MediaType.APPLICATION_JSON_VALUE)
@Api(value = "定时任务列表", tags = {"定时任务解析列表验证"})
public class QuartzListController extends BaseController {

    @Resource
    private ScheduledAnnotationBeanPostProcessor scheduledPostProcessor;

    @GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
    @ApiOperation(value = "任务列表", notes = "测试响应结果集(<b>获取定时任务列表</b>)", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    @ApiOperationSupport(order = 10)
    public List<QuartzBo> quartzList() throws Exception {
        return this.getQuartzList();
    }

    private List<QuartzBo> getQuartzList() throws Exception {
        List<QuartzBo> resultList = Lists.newArrayList();
        Set<ScheduledTask> scheduledTasks = scheduledPostProcessor.getScheduledTasks();
        for (ScheduledTask scheduled : scheduledTasks) {
            Task task = scheduled.getTask();
            if (task instanceof CronTask) {
                this.fillCronTask((CronTask) task , resultList);
            }
            //Fixme 未处理另外几种定时类型
        }
        return resultList;
    }

    private void fillCronTask(CronTask task, List<QuartzBo> resultList) {
        String expression = task.getExpression();
        QuartzBo result = new QuartzBo();
        resultList.add(result);
        result.setExpression(expression);
        String className = task.toString();
        result.setClassName(FilenameUtils.getBaseName(className));
        result.setMethodName(FilenameUtils.getExtension(className));
        CronSequenceGenerator cronSequenceGenerator = new CronSequenceGenerator(expression);
        //下一次
        Date next = cronSequenceGenerator.next(new Date());
        //下下次
        Date nextNext = cronSequenceGenerator.next(next);
        //计算上一次 Fixme 这种方式只适合固定频率的周期性任务,非固定频率的获得的并不准确
        Date prev = new Date(next.getTime() - (nextNext.getTime() - next.getTime()));
        //执行时间
        result.setExecuteTime(DateFormat.formatDatetime(prev));
        //下次执行时间
        result.setNextExecuteTime(DateFormat.formatDatetime(next));
    }

}

注意上述代码中的计算上次执行时间是存在问题的,仅仅只能应用于固定频率的场景,另外quartz框架中的CronExpression.isValidExpression可以用于验证cron表达式是否正确;getNextValidTimeAfter方法可以获取下次执行时间,虽然具体的时间巨复杂无比,所以计算上次执行时间真的要写起来也是非常复杂的,也可以把日期向前推移,然后按照1秒为单位向后递增,一直穷举下次执行时间,可以推算出上次执行时间,但是这个只是理论上的上次,并不是实际执行的上次,如果需要准确的掌握上次执行实际,则还是需要在执行时进行特别记录,所以如果可以的话,还是建议使用Spring Boot与Quartz,本站有更科学的实现。

上文代码动态增加的定时任务是不会被增加至此处的集合中的,该部分集合获取到的数据是只读类型的集合,无法进行新增或修改操作;

4.源码下载

源码工程下载可转至https://gitee.com/88911006/chendd-blog-examples项目的Quartz分支;

 点赞


 发表评论

当前回复:作者

 评论列表


留言区