专栏首页以Java架构赢天下SpringBoot集成Quartz实现定时任务

SpringBoot集成Quartz实现定时任务

1 需求

在我的前后端分离的实验室管理项目中,有一个功能是学生状态统计。我的设计是按天统计每种状态的比例。为了便于计算,在每天0点,系统需要将学生的状态重置,并插入一条数据作为一天的开始状态。另外,考虑到学生的请假需求,请假的申请往往是提前做好,等系统时间走到实际请假时间的时候,系统要将学生的状态修改为请假。

显然,这两个子需求都可以通过定时任务实现。在网上略做搜索以后,我选择了比较流行的定时任务框架Quartz。

2 Quartz

Quartz是一个定时任务框架,其他介绍网上也很详尽。这里要介绍一下Quartz里的几个非常核心的接口。

2.1 Scheduler接口

Scheduler翻译成调度器,Quartz通过调度器来注册、暂停、删除Trigger和JobDetail。Scheduler还拥有一个SchedulerContext,顾名思义就是上下文,通过SchedulerContext我们可以获取到触发器和任务的一些信息。

2.2 Trigger接口

Trigger可以翻译成触发器,通过cron表达式或是SimpleScheduleBuilder等类,指定任务执行的周期。系统时间走到触发器指定的时间的时候,触发器就会触发任务的执行。

2.3 JobDetail接口

Job接口是真正需要执行的任务。JobDetail接口相当于将Job接口包装了一下,Trigger和Scheduler实际用到的都是JobDetail。

3 SpringBoot官方文档解读

SpringBoot官方写了 spring-boot-starter-quartz 。使用过SpringBoot的同学都知道这是一个官方提供的启动器,有了这个启动器,集成的操作就会被大大简化。

现在我们来看一看SpingBoot2.2.6官方文档,其中第4.20小节 Quartz Scheduler 就谈到了Quartz,但很可惜一共只有两页不到的内容,先来看看这么精华的文档里能学到些什么。

Spring Boot offers several conveniences for working with the Quartz scheduler, including thespring-boot-starter-quartz “Starter”. If Quartz is available, a Scheduler is auto-configured (through the SchedulerFactoryBean abstraction).Beans of the following types are automatically picked up and associated with the Scheduler:• JobDetail: defines a particular Job. JobDetail instances can be built with the JobBuilder API.• Calendar.• Trigger: defines when a particular job is triggered.

翻译一下:

SpringBoot提供了一些便捷的方法来和Quartz协同工作,这些方法里面包括`spring-boot-starter-quartz`这个启动器。如果Quartz可用,Scheduler会通过SchedulerFactoryBean这个工厂bean自动配置到SpringBoot里。JobDetail、Calendar、Trigger这些类型的bean会被自动采集并关联到Scheduler上。
Jobs can define setters to inject data map properties. Regular beans can also be injected in a similar manner.

翻译一下:

Job可以定义setter(也就是set方法)来注入配置信息。也可以用同样的方法注入普通的bean。

下面是文档里给的示例代码,我直接完全照着写,拿到的却是null。不知道是不是我的使用方式有误。后来仔细一想,文档的意思应该是在创建Job对象之后,调用set方法将依赖注入进去。但后面我们是通过框架反射生成的Job对象,这样做反而会搞得更加复杂。最后还是决定采用给Job类加@Component注解的方法。

文档的其他篇幅就介绍了一些配置,但是介绍得也不全面,看了帮助也并不是很大。详细的配置可以参考w3school的 Quartz配置

4 SpringBoot集成Quartz

4.1 建表

我选择将定时任务的信息保存在数据库中,优点是显而易见的,定时任务不会因为系统的崩溃而丢失。

建表的sql语句在Quartz的github中可以找到,里面有针对每一种常用数据库的sql语句,具体地址是: Quartz数据库建表sql

SpringBoot集成Quartz实现定时任务

建表以后,可以看到数据库里多了11张表。我们完全不需要关心每张表的具体作用,在添加删除任务、触发器等的时候,Quartz框架会操作这些表。

4.2 引入依赖

在 pom.xml 里添加依赖。

<!-- quartz 定时任务 --><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-quartz</artifactId>    <version>2.2.6.RELEASE</version></dependency>

4.3 配置quartz

在 application.yml 中配置quartz。相关配置的作用已经写在注解上。

# spring的datasource等配置未贴出spring:  quartz:      # 将任务等保存化到数据库      job-store-type: jdbc      # 程序结束时会等待quartz相关的内容结束      wait-for-jobs-to-complete-on-shutdown: true      # QuartzScheduler启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录      overwrite-existing-jobs: true      # 这里居然是个map,搞得智能提示都没有,佛了      properties:        org:          quartz:              # scheduler相关            scheduler:              # scheduler的实例名              instanceName: scheduler              instanceId: AUTO            # 持久化相关            jobStore:              class: org.quartz.impl.jdbcjobstore.JobStoreTX              driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate              # 表示数据库中相关表是QRTZ_开头的              tablePrefix: QRTZ_              useProperties: false            # 线程池相关            threadPool:              class: org.quartz.simpl.SimpleThreadPool              # 线程数              threadCount: 10              # 线程优先级              threadPriority: 5              threadsInheritContextClassLoaderOfInitializingThread: true

4.4 注册周期性的定时任务

第1节中提到的第一个子需求是在每天0点执行的,是一个周期性的任务,任务内容也是确定的,所以直接在代码里注册JobDetail和Trigger的bean就可以了。当然,这些JobDetail和Trigger也是会被持久化到数据库里。

/** * Quartz的相关配置,注册JobDetail和Trigger * 注意JobDetail和Trigger是org.quartz包下的,不是spring包下的,不要导入错误 */@Configurationpublic class QuartzConfig {    @Bean    public JobDetail jobDetail() {        JobDetail jobDetail = JobBuilder.newJob(StartOfDayJob.class)                .withIdentity("start_of_day", "start_of_day")                .storeDurably()                .build();        return jobDetail;    }    @Bean    public Trigger trigger() {        Trigger trigger = TriggerBuilder.newTrigger()                .forJob(jobDetail())                .withIdentity("start_of_day", "start_of_day")                .startNow()                // 每天0点执行                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?"))                .build();        return trigger;    }}

builder类创建了一个JobDetail和一个Trigger并注册成为Spring bean。从第3节中摘录的官方文档中,我们已经知道这些bean会自动关联到调度器上。需要注意的是JobDetail和Trigger需要设置组名和自己的名字,用来作为唯一标识。当然,JobDetail和Trigger的唯一标识可以相同,因为他们是不同的类。

Trigger通过cron表达式指定了任务执行的周期。对cron表达式不熟悉的同学可以百度学习一下。

JobDetail里有一个StartOfDayJob类,这个类就是Job接口的一个实现类,里面定义了任务的具体内容,看一下代码:

@Componentpublic class StartOfDayJob extends QuartzJobBean {    private StudentService studentService;    @Autowired    public StartOfDayJob(StudentService studentService) {        this.studentService = studentService;    }    @Override    protected void executeInternal(JobExecutionContext jobExecutionContext)            throws JobExecutionException {        // 任务的具体逻辑    }}

网上很多博客也是这么介绍的。但是根据我的实际测试,这样写可以完成依赖注入,但我还不知道它的实现原理。

SpringBoot集成Quartz实现定时任务

SpringBoot集成Quartz实现定时任务

4.5 注册无周期性的定时任务

第1节中提到的第二个子需求是学生请假,显然请假是不定时的,一次性的,而且不具有周期性。

4.5节与4.4节大体相同,但是有两点区别:

  • Job类需要获取到一些数据用于任务的执行;
  • 任务执行完成后删除Job和Trigger。

业务逻辑是在老师批准学生的请假申请时,向调度器添加Trigger和JobDetail。

实体类:

public class LeaveApplication {    @TableId(type = IdType.AUTO)    private Integer id;    private Long proposerUsername;    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")    private LocalDateTime startTime;    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")    private LocalDateTime endTime;    private String reason;    private String state;    private String disapprovedReason;    private Long checkerUsername;    private LocalDateTime checkTime;    // 省略getter、setter}

Service层逻辑,重要的地方已在注释中说明。

@Servicepublic class LeaveApplicationServiceImpl implements LeaveApplicationService {    @Autowired    private Scheduler scheduler;        // 省略其他方法与其他依赖    /**     * 添加job和trigger到scheduler     */    private void addJobAndTrigger(LeaveApplication leaveApplication) {        Long proposerUsername = leaveApplication.getProposerUsername();        // 创建请假开始Job        LocalDateTime startTime = leaveApplication.getStartTime();        JobDetail startJobDetail = JobBuilder.newJob(LeaveStartJob.class)                // 指定任务组名和任务名                .withIdentity(leaveApplication.getStartTime().toString(),                        proposerUsername + "_start")                // 添加一些参数,执行的时候用                .usingJobData("username", proposerUsername)                .usingJobData("time", startTime.toString())                .build();        // 创建请假开始任务的触发器        // 创建cron表达式指定任务执行的时间,由于请假时间是确定的,所以年月日时分秒都是确定的,这也符合任务只执行一次的要求。        String startCron = String.format("%d %d %d %d %d ? %d",                startTime.getSecond(),                startTime.getMinute(),                startTime.getHour(),                startTime.getDayOfMonth(),                startTime.getMonth().getValue(),                startTime.getYear());        CronTrigger startCronTrigger = TriggerBuilder.newTrigger()                // 指定触发器组名和触发器名                .withIdentity(leaveApplication.getStartTime().toString(),                        proposerUsername + "_start")                .withSchedule(CronScheduleBuilder.cronSchedule(startCron))                .build();        // 将job和trigger添加到scheduler里        try {            scheduler.scheduleJob(startJobDetail, startCronTrigger);        } catch (SchedulerException e) {            e.printStackTrace();            throw new CustomizedException("添加请假任务失败");        }    }}

Job类逻辑,重要的地方已在注释中说明。

@Componentpublic class LeaveStartJob extends QuartzJobBean {    private Scheduler scheduler;    private SystemUserMapperPlus systemUserMapperPlus;    @Autowired    public LeaveStartJob(Scheduler scheduler,                         SystemUserMapperPlus systemUserMapperPlus) {        this.scheduler = scheduler;        this.systemUserMapperPlus = systemUserMapperPlus;    }    @Override    protected void executeInternal(JobExecutionContext jobExecutionContext)            throws JobExecutionException {        Trigger trigger = jobExecutionContext.getTrigger();        JobDetail jobDetail = jobExecutionContext.getJobDetail();        JobDataMap jobDataMap = jobDetail.getJobDataMap();        // 将添加任务的时候存进去的数据拿出来        long username = jobDataMap.getLongValue("username");        LocalDateTime time = LocalDateTime.parse(jobDataMap.getString("time"));        // 编写任务的逻辑        // 执行之后删除任务        try {            // 暂停触发器的计时            scheduler.pauseTrigger(trigger.getKey());            // 移除触发器中的任务            scheduler.unscheduleJob(trigger.getKey());            // 删除任务            scheduler.deleteJob(jobDetail.getKey());        } catch (SchedulerException e) {            e.printStackTrace();        }    }}

5 总结

上文所述的内容应该可以满足绝大部分定时任务的需求。我在查阅网上的博客之后,发现大部分博客里介绍的Quartz使用还是停留在Spring阶段,配置也都是通过xml,因此我在实现了功能以后,将整个过程总结了一下,留给需要的人以及以后的自己做参考。

总体上来说,Quartz实现定时任务还是非常方便的,与SpringBoot整合之后配置也非常简单,是实现定时任务的不错的选择。

5.2 小坑1

在IDEA2020.1版本里使用SpringBoot与Quartz时,报错找不到org.quartz程序包,但是依赖里面明明有org.quartz,类里的import也没有报错,还可以通过Ctrl+鼠标左键直接跳转到相应的类里。后面我用了IDEA2019.3.4就不再有这个错误。那么就是新版IDEA的BUG了。

SpringBoot集成Quartz实现定时任务

本文由博客群发一文多发等运营工具平台 OpenWrite 发布 来源:https://www.tuicool.com/articles/zyQfmqV

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Redis的主从复制原理

    1、从库向主库发送sync命令,也就是从库向主库发送同步请求; 2、当主库接受到sync命令后,会执行bgsave命令(保存此刻主库的一个快照),创建一个RD...

    用户5546570
  • 【Java】几种典型的内存溢出案例,都在这儿了!

    作为程序员,多多少少都会遇到一些内存溢出的场景,如果你还没遇到,说明你工作的年限可能比较短,或者你根本就是个假程序员!哈哈,开个玩笑。今天,我们就以Java代码...

    用户5546570
  • dubbo源码解析之负载均衡

    GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master

    用户5546570
  • httpclient处理多用户同时在线

    在使用httpclient做接口相关测试的过程中,遇到过一个障碍:如何处理多用户同时登陆。之前用户身份凭证一般都是做公参里面处理或者在header中单独定义一个...

    八音弦
  • 自相矛盾:Null is Not Null引发的成本误区

    黄玮(Fuyuncat) 资深Oracle DBA,个人网www.HelloDBA.com,致力于数据库底层技术的研究,其作品获得广大同行的高度评价. 在SQL...

    数据和云
  • 【开源】LLMAnimator 60多种动画让你的应用动起来

    github:  https://github.com/brookshi/LLMAnimator ,欢迎star/fork 。之前做android的时候需要给应...

    用户1147588
  • [Setting]解决Windows远程控制无法连接

    原文链接:https://blog.csdn.net/humanking7/article/details/85646998

    祥知道
  • 用 Python 生成彩色动态二维码

    伪君子
  • 金黄葡萄球菌RNA-seq数据分析

    这里出现问题了,突变株的比对率太低,不到1%,这是不可能的,怀疑样品污染,然后随机挑选了5条序列blast了下,发现应该是被溶血葡萄球菌污染。

    Y大宽
  • 跨平台运行 Rafy 首次部署记录

    一直想在 Linux 上使用 MONO 试试运行 Rafy,最近因为业务需要,总算是真正地试验了一次。下面是本次部署记录的一些要点。 Linux 这次部署,我是...

    用户1172223

扫码关注云+社区

领取腾讯云代金券