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 the
spring-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

建表以后,可以看到数据库里多了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包下的,不要导入错误
*/
@Configuration
public 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接口的一个实现类,里面定义了任务的具体内容,看一下代码:

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

这里面有一个小问题,上面用builder创建JobDetail时,传入了StartOfDayJob.class,按常理推测,应该是Quartz框架通过反射创建StartOfDayJob对象,再调用executeInternal()执行任务。这样依赖,这个Job是Quartz通过反射创建的,即使加了注解@Component,这个StartOfDayJob对象也不会被注册到ioc容器中,更不可能实现依赖的自动装配。

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

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层逻辑,重要的地方已在注释中说明。

@Service
public 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类逻辑,重要的地方已在注释中说明。

@Component
public 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了。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

SpringBoot集成Quartz实现定时任务的更多相关文章

  1. springBoot集成 quartz动态定时任务

    项目中需要用到定时任务,考虑了下java方面定时任务无非就三种: 用Java自带的timer类.稍微看了一下,可以实现大部分的指定频率的任务的调度(timer.schedule()),也可以实现关闭和 ...

  2. Spring Boot集成quartz实现定时任务并支持切换任务数据源

    org.quartz实现定时任务并自定义切换任务数据源 在工作中经常会需要使用到定时任务处理各种周期性的任务,org.quartz是处理此类定时任务的一个优秀框架.随着项目一点点推进,此时我们并不满足 ...

  3. SpringBoot集成Quartz(解决@Autowired空指针Null问题即依赖注入的属性为null)

    使用spring-boot作为基础框架,其理念为零配置文件,所有的配置都是基于注解和暴露bean的方式. Quartz的4个核心概念: 1.Job表示一个工作,要执行的具体内容.此接口中只有一个方法v ...

  4. Springboot集成Quartz

    之前学习过spring的定时任务 :https://www.cnblogs.com/slimshady/p/10112515.html 本文主要学习记录下springboot使用quartz 1.   ...

  5. 【spring-boot】 springboot整合quartz实现定时任务

    在做项目时有时候会有定时器任务的功能,比如某某时间应该做什么,多少秒应该怎么样之类的. spring支持多种定时任务的实现.我们来介绍下使用spring的定时器和使用quartz定时器 1.我们使用s ...

  6. ssh中使用spring的集成quartz 编写定时任务

    之前没有使用框架开发时对于开发定时任务都是 使用java的原声timer类,重写线程的run方法跑要执行的任务.刚刚换的新公司,项目使用ssh2,目前该项目中的定时任务的使用spirng集成的quar ...

  7. springboot整合Quartz实现定时任务

    1.maven依赖: <!--quartz--> <dependency> <groupId>org.quartz-scheduler</groupId> ...

  8. SpringBoot定时任务 - 集成quartz实现定时任务(单实例和分布式两种方式)

    最为常用定时任务框架是Quartz,并且Spring也集成了Quartz的框架,Quartz不仅支持单实例方式还支持分布式方式.本文主要介绍Quartz,基础的Quartz的集成案例本,以及实现基于数 ...

  9. Spring Boot笔记(三) springboot 集成 Quartz 定时任务

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 1. 在 pom.xml 中 添加 Quartz 所需要 的 依赖 <!--定时器 quartz- ...

随机推荐

  1. Nginx知多少系列之(六)Linux下.NET Core项目负载均衡

    目录 1.前言 2.安装 3.配置文件详解 4.工作原理 5.Linux下托管.NET Core项目 6.Linux下.NET Core项目负载均衡 7.负载均衡策略详解 8.Linux下.NET C ...

  2. AJ学IOS 之微博项目实战(1)微博主框架-子控制器的添加

    AJ分享,必须精品 一:简单介绍 这是新浪微博的iOS端项目,来自于黑马的一个实战项目. 主要分成五大模块,本次全部运用纯代码实现,其中会用到很多前面学过得内容,如果有的地方有重复的知识点,说明这个知 ...

  3. 动态规划_基础_最长公共子序列_多种方法_递归/dp

    D: 魔法少女资格面试 题目描述 众所周知,魔法少女是一个低危高薪职业.随着近年来报考魔法少女的孩子们越来越多,魔法少女行业已经出现饱和现象!为了缓和魔法少女界的就业压力,魔法少女考核员丁丁妹决定增加 ...

  4. centos7用户管理及root忘记密码恢复

    查看用户相关命令:#id 用户和组的信息#whoami #查看当前有效用户名#who #显示目前登入系统的用户信息.#w # w 命令用于显示已经登陆系统的用户列表#users #用于显示当前登录系统 ...

  5. 利用Ajax实现异步请求

    Ajax 1.课程引入      静态网站和动态网站都是同步的,但同步方式有缺点:页面请求响应式阻塞,影响用户体验      为了解决这个问题,可以通过变通的手段实现页面的局部更新(隐藏帧),由于隐藏 ...

  6. 解决项目迁移至Kubernetes集群中的代理问题

    解决项目迁移至Kubernetes集群中的代理问题 随着Kubernetes技术的日益成熟,越来越多的企业选择用Kubernetes集群来管理项目.新项目还好,可以选择合适的集群规模从零开始构建项目: ...

  7. HBase Filter 过滤器概述

    abc 过滤器介绍 HBase过滤器是一套为完成一些较高级的需求所提供的API接口. 过滤器也被称为下推判断器(push-down predicates),支持把数据过滤标准从客户端下推到服务器,带有 ...

  8. Java 虚拟机中的运行时数据区分析

    本文基于 JDK1.8 阐述分析 运行过程 我们都知道 Java 源文件通过编译器编译后,能产生相应的 .Class 文件,也就是字节码文件.而字节码文件通过 Java 虚拟机中的解释器,编译成特定机 ...

  9. Java 多线程实现方式三:实现 Callable 接口

    完整套路 java 通过实现Callable 接口来实现多线程相比较于继承Thread 接口和 实现Runnable 接口比较麻烦,但好处是可以有返回值. 基本套路: 1. 创建目标对象 2. 创建执 ...

  10. testlink配置修改

    1.      Read/write permissions报错 问题: Checking if  /var/testlink/logs/ directory exists [S] </B< ...