SpringBoot与Quartz整合,通过API动态配置定时任务,持久化到数据库。
SpringBoot2.0的spring-boot-starter-quartz包已为我们做好自动化配置,将Quartz托管给spring,不再像之前我们得使用QuartzConfiguration类来自己完成Quartz需要的一系列配置,如:JobFactory、SchedulerFactoryBean等。
<!-- springboot2.0之前的quartz相关依赖 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>${quartz.version}</version>
</dependency>
<!-- springboot2.0之后的quartz依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
自动化配置源码:
我们找到Idea的External Libraries并且展开spring-boot-autoconfigure-2.0.0.RELEASE.jar,找到org.springframework.boot.autoconfigure.quartz,该目录就是SpringBoot为我们提供的Quartz自动化配置源码实现,在该目录下有如下所示几个类:
- AutowireCapableBeanJobFactory
该类替代了我们之前在QuartzConfiguration配置类的AutowiringSpringBeanJobFactory内部类实现,主要作用是将我们自定义的QuartzJobBean子类托管给Spring IOC,可以在定时任务类内注入任意被Spring IOC托管的类。
- JobStoreType
该类是一个枚举类型,定义了对应application.yml、application.properties文件内spring.quartz.job-store-type配置,其目的是配置quartz任务的数据存储方式,分别为:MEMORY(内存方式:默认)、JDBC(数据库方式)。
- QuartzAutoConfiguration
该类是自动配置的主类,内部配置了SchedulerFactoryBean以及JdbcStoreTypeConfiguration,使用QuartzProperties作为属性自动化配置条件。
- QuartzDataSourceInitializer
该类主要用于数据源初始化后的一些操作,根据不同平台类型的数据库进行选择不同的数据库脚本。
- QuartzProperties
该类对应了spring.quartz在application.yml、application.properties文件内开头的相关配置。
- SchedulerFactoryBeanCustomizer
这是一个接口,我们实现该接口后并且将实现类使用Spring IOC托管,可以完成SchedulerFactoryBean的个性化设置,这里的设置完全可以对SchedulerFactoryBean做出全部的设置变更。
------ 开始 ------
pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.M1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>quartz</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>quartz</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
application.yml文件如下:
spring:
datasource:
url: jdbc:mysql://localhost:3306/lesson?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
jpa:
properties:
hibernate:
show_sql: true
format_sql: true
quartz:
properties:
org:
quartz:
scheduler:
instanceName: DefaultQuartzScheduler
#如果使用集群,instanceId必须唯一,设置成AUTO
instanceId: AUTO
jobStore:
#存储方式使用JobStoreTX,也就是数据库
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#默认表前缀
tablePrefix: QRTZ_
#是否使用集群(如果项目只部署到一台服务器,就不用了)
isClustered: false
clusterCheckinInterval: 10000
useProperties: true
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 5
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#quartz任务的数据持久化方式,默认是MEMORY内存方式,配置JDBC以使用数据库方式持久化任务
job-store-type: JDBC
#自动将quartz需要的数据表通过配置方式进行初始化
# jdbc:
# initialize-schema: never
使用Quartz需要的11张表:
CREATE TABLE QRTZ_JOB_DETAILS
(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(200) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
JobController.java
package com.example.quartz;
import com.example.quartz.entity.JobDetailsEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping(value="/job")
public class JobController {
@Resource
private JobService jobService;
@PostMapping(value="/add")
public String addJob(@RequestParam(value="jobClassName") String jobClassName,
@RequestParam(value="jobGroupName") String jobGroupName,
@RequestParam(value="cronExpression") String cronExpression) throws Exception {
return jobService.jobAdd(jobClassName, jobGroupName, cronExpression);
}
@PostMapping(value="/pause")
public String pauseJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName") String jobGroupName) throws Exception {
return jobService.jobPause(jobClassName, jobGroupName);
}
@PostMapping(value="/resume")
public String resumeJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName")String jobGroupName) throws Exception {
return jobService.jobResume(jobClassName, jobGroupName);
}
@PostMapping(value="/reschedule")
public String rescheduleJob(@RequestParam(value="jobClassName") String jobClassName,
@RequestParam(value="jobGroupName") String jobGroupName,
@RequestParam(value="cronExpression") String cronExpression) throws Exception {
return jobService.jobReschedule(jobClassName, jobGroupName, cronExpression);
}
@PostMapping(value="/delete")
public String deleteJob(@RequestParam(value="jobClassName") String jobClassName, @RequestParam(value="jobGroupName") String jobGroupName) throws Exception {
return jobService.jobDelete(jobClassName, jobGroupName);
}
@GetMapping("/query")
public List<JobDetailsEntity> queryJob() {
return jobService.queryJob();
}
}
JobServiceImpl.java(此处未贴出JobService接口)
直接注入org.quartz包中的定时任务调度器Scheduler,对进行定时任务的一系列操作。
package com.example.quartz;
import com.example.quartz.entity.JobDetailsEntity;
import com.example.quartz.jpa.JobDetailsJPA;
import org.quartz.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class JobServiceImpl implements JobService {
@Resource
private Scheduler scheduler;
@Resource
private JobDetailsJPA jobDetailsJPA;
@Override
public String jobAdd(String jobClassName, String jobGroupName, String cronExpression) throws Exception {
//启动调度器
scheduler.start();
//创建任务
JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobClassName, jobGroupName).build();
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
//按cronExpression构建一个新的触发器,定义任务调度的时间规则
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
.withSchedule(scheduleBuilder).build();
try {
//将任务与触发器绑定到调度器内
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
System.out.println("创建定时任务失败" + e);
throw new Exception("创建定时任务失败");
}
return "创建定时任务:" + jobClassName;
}
private static Job getClass(String classname) throws Exception {
return (Job) Class.forName(classname).newInstance();
}
@Override
public String jobPause(String jobClassName, String jobGroupName) throws Exception {
scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
return "暂停定时任务:" + jobClassName;
}
@Override
public String jobResume(String jobClassName, String jobGroupName) throws Exception {
scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
return "重启定时任务:" + jobClassName;
}
@Override
public String jobReschedule(String jobClassName, String jobGroupName, String cronExpression) throws Exception {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//按新的cronExpression重新构建触发器
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//按新的trigger重新绑定到调度器
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
System.out.println("更新定时任务失败" + e);
throw new Exception("更新定时任务失败");
}
return "更新定时任务:" + jobClassName;
}
@Override
public String jobDelete(String jobClassName, String jobGroupName) throws Exception {
scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
return "删除定时任务:" + jobClassName;
}
@Override
public List<JobDetailsEntity> queryJob() {
return jobDetailsJPA.findAll();
}
}
测试用定时任务 TestJob.java
package com.example.quartz;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class TestJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(new Date() + " 测试定时任务....");
}
}
注意调用add接口时,任务类名需要全路径com.example.quartz.TestJob
数据库相关代码: JobDetailsJPA接口
package com.example.quartz.jpa;
import com.example.quartz.entity.JobDetailsEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import java.io.Serializable;
public interface JobDetailsJPA extends
JpaRepository<JobDetailsEntity,Long>,
JpaSpecificationExecutor<JobDetailsEntity>,
Serializable {
}
任务详情实体类JobDetailsEntity.java
package com.example.quartz.entity;
import javax.persistence.*;
import java.io.Serializable;
import java.sql.Blob;
@Entity
@Table(name="qrtz_job_details")
public class JobDetailsEntity implements Serializable {
@EmbeddedId //复合主键
private JobDetailsKey jobDetailsKey;
@Column(name="DESCRIPTION")
private String description;
@Column(name="JOB_CLASS_NAME")
private String jobClassName;
@Column(name="IS_DURABLE")
private String isDurable;
@Column(name="IS_NONCONCURRENT")
private String isNonconcurrent;
@Column(name="IS_UPDATE_DATA")
private String isUpdateData;
@Column(name="REQUESTS_RECOVERY")
private String requestRecovery;
//方便起见,就不读取blob类型的属性了
// @Column(name="JOB_DATA")
// private Blob jobData;
public JobDetailsKey getJobDetailsKey() {
return jobDetailsKey;
}
public void setJobDetailsKey(JobDetailsKey jobDetailsKey) {
this.jobDetailsKey = jobDetailsKey;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getJobClassName() {
return jobClassName;
}
public void setJobClassName(String jobClassName) {
this.jobClassName = jobClassName;
}
public String getIsDurable() {
return isDurable;
}
public void setIsDurable(String isDurable) {
this.isDurable = isDurable;
}
public String getIsNonconcurrent() {
return isNonconcurrent;
}
public void setIsNonconcurrent(String isNonconcurrent) {
this.isNonconcurrent = isNonconcurrent;
}
public String getIsUpdateData() {
return isUpdateData;
}
public void setIsUpdateData(String isUpdateData) {
this.isUpdateData = isUpdateData;
}
public String getRequestRecovery() {
return requestRecovery;
}
public void setRequestRecovery(String requestRecovery) {
this.requestRecovery = requestRecovery;
}
}
由于qrtz_job_details表是复合主键PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`),所以需要创建复合主键类 JobDetailsKey.java
package com.example.quartz.entity;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Embeddable
public class JobDetailsKey implements Serializable {
@Column(name="SCHED_NAME")
private String schedName;
@Column(name="JOB_NAME")
private String jobName;
@Column(name="JOB_GROUP")
private String jobGroup;
public String getSchedName() {
return schedName;
}
public void setSchedName(String schedName) {
this.schedName = schedName;
}
public String getJobName() {
return jobName;
}
public void setJobName(String jobName) {
this.jobName = jobName;
}
public String getJobGroup() {
return jobGroup;
}
public void setJobGroup(String jobGroup) {
this.jobGroup = jobGroup;
}
}