Quartz是OpenSymphony开源组织在Job scheduling领域的一个开源项目,由Java开发,可以用来执行定时任务,类似于java.util.Timer。Quartz是功能强大的开源作业调度库,几乎可以集成到任何Java应用程序中。Quartz包含许多企业级功能,例如对JTA事务和集群的支持。Quartz作为定时任务组件,既可以单独使用,也可以整合Spring使用。
官网:http://www.quartz-scheduler.org/
maven坐标:
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
第一步:创建maven工程,导入Quartz的maven坐标
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
第二步:创建作业类,需要实现Job接口
package cn.test.jobs;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 自定义Job
*/
public class HelloJob implements Job {
/**
* 执行方法
* @param jobExecutionContext 任务执行上下文对象
* @throws JobExecutionException
*/
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
System.out.println("自定义Job开始执行:" + formatter.format(now));
}
}
第三步:基于Quartz提供的API启动任务调度
package cn.test;
import cn.itcast.jobs.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class HelloJobMain {
public static void main(String[] args) {
try {
//1:创建调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2:创建任务实例
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).
withIdentity("JobDetail_1").
build();
//3:创建触发器Trigger
Trigger trigger = TriggerBuilder.newTrigger().
withIdentity("Trigger_1").
withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).
build();
//4:触发器和任务关联
scheduler.scheduleJob(jobDetail,trigger);
//5:启动任务调度器
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
入门案例中触发器设置的是每隔5秒执行一次,是否可以使用cron表达式设置触发时间呢?
1. 解答
针对第一个问题,可以为HelloJob类添加无参构造方法:
public HelloJob(){
System.out.println("创建HelloJob实例");
}
观察控制台输出:
可以看到每次执行定时任务都会重新创建一个HelloJob实例。
2.解答
针对第二个问题,Quartz为我们提供了相应方法可以在创建任务时设置参数,并且通过提供的上下文对象(JobExecutionContext)来获取参数。
HelloJobMain做如下修改:
package cn.test;
import cn.itcast.jobs.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class HelloJobMain {
public static void main(String[] args) {
try {
//1:创建调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2:创建任务实例
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).
withIdentity("JobDetail_1").
build();
//设置参数,在定时任务执行时可以动态获取这些参数
jobDetail.getJobDataMap().put("key1","value1");
jobDetail.getJobDataMap().put("key2","value2");
//3:创建触发器Trigger
Trigger trigger = TriggerBuilder.newTrigger().
withIdentity("Trigger_1").
withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever()).
build();
//4:触发器和任务关联
scheduler.scheduleJob(jobDetail,trigger);
//5:启动任务调度器
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
HelloJob做如下修改:
package cn.test.jobs;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 自定义Job
*/
public class HelloJob implements Job {
public HelloJob(){
System.out.println("创建HelloJob实例");
}
/**
* 执行方法
* @param jobExecutionContext 任务执行上下文对象
* @throws JobExecutionException
*/
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
System.out.println("自定义Job开始执行:" + formatter.format(now));
//通过上下文对象JobExecutionContext可以获取到任务相关参数
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
Set<Map.Entry<String, Object>> entries = jobDataMap.entrySet();
entries.forEach((entry) -> {
String key = entry.getKey();
Object value = entry.getValue();
System.out.println(key + " = " + value);
});
}
}
查看控制台输出效果:
可以看到通过上下文对象可以获取到相应参数。
通过断点调试可以看到上下文对象中封装了很多信息,这些信息都可以通过上下文对象获取到:
3. 解答
针对第3个问题,Quartz是支持cron表达式来设置触发时间的,HelloJobMain做如下修改:
package cn.test;
import cn.itcast.jobs.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class HelloJobMain {
public static void main(String[] args) {
try {
//1:创建调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2:创建任务实例
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).
withIdentity("JobDetail_1").
build();
//设置参数,在定时任务执行时可以动态获取这些参数
jobDetail.getJobDataMap().put("key1","value1");
jobDetail.getJobDataMap().put("key2","value2");
//构建cron表达式
CronScheduleBuilder cronScheduleBuilder =
CronScheduleBuilder.cronSchedule("1,5,11,20,27,35 * * * * ?");
//3:创建触发器Trigger
Trigger trigger = TriggerBuilder.newTrigger().
withIdentity("Trigger_1").
withSchedule(cronScheduleBuilder).
build();
//4:触发器和任务关联
scheduler.scheduleJob(jobDetail,trigger);
//5:启动任务调度器
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
前面的入门案例中我们已经通过Quartz实现了简单的定时任务效果,但是这种方式的任务信息是保存在内存中的,如果服务重启后则任务信息都会丢失。通过上下文对象JobExecutionContext可以看到任务的存储是通过RAMJobStore实现的,如下图:
Quartz提供了另外一种方式,即通过JobStoreTX对象将任务相关信息保存在数据库中,这样就可以避免服务重启后任务信息丢失的问题。同时此种方式还支持动态定时任务,例如动态添加定时任务、暂停定时任务、删除定时任务等操作。
Quartz官方提供了一套数据模型(共11张表)用于保存定时任务相关信息,建表脚本:
可以看到Quartz针对不同的数据库提供了不同的sql脚本文件,本课程使用的是MySQL数据库,执行上面MySQL对应的sql脚本,创建如下11张表:
第一步:创建maven工程并导入maven坐标
<?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.5.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.test</groupId>
<artifactId>quartzTest</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>1.8</java.version>
<!-- Spring Cloud 版本信息 -->
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
<swagger.version>2.9.2</swagger.version>
<spring-cloud-alibaba-version>2.2.1.RELEASE</spring-cloud-alibaba-version>
<druid.version>1.1.23</druid.version>
<java.version>1.8</java.version>
<commons.lang.version>2.6</commons.lang.version>
<commons.io.version>2.5</commons.io.version>
<lombok.version>1.18.4</lombok.version>
<commons.io.version>2.5</commons.io.version>
<http-core.version>4.4.9</http-core.version>
<httpclient.version>4.5.11</httpclient.version>
<mybatis-spring-boot-starter.version>2.1.0</mybatis-spring-boot-starter.version>
<mybatis-plus.version>3.3.0</mybatis-plus.version>
<quartz.version>2.3.0</quartz.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud begin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud end-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
<exclusions>
<exclusion>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
<exclusion>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP-java6</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--MyBatisPLus-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第二步:创建springBoot配置文件application.yml
server:
port: 8080
spring:
application:
name: quartz-test
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/quartz?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
第三步:编写定时任务配置类ScheduleConfig
package com.test.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
/**
* 定时任务配置
*
* @author
*/
@Configuration
public class ScheduleConfig {
//调度器工厂,用于创建调度器对象Scheduler
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
//quartz参数
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "WitlinkedScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
//线程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
prop.put("org.quartz.threadPool.threadPriority", "5");
//JobStore配置
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
//集群配置
prop.put("org.quartz.jobStore.isClustered", "true");
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
factory.setQuartzProperties(prop);
factory.setSchedulerName("pdScheduler");
//延时启动
factory.setStartupDelay(3);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
factory.setOverwriteExistingJobs(true);
//设置自动启动,默认为true
factory.setAutoStartup(true);
return factory;
}
}
第四步:编写Swagger相关配置类ConfigurationSupport
package com.test.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Slf4j
@Configuration
@EnableSwagger2
public class ConfigurationSupport extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
// 创建api的基本信息
.apiInfo(apiInfo())
// 选择哪些接口去暴露
.select()
// 扫描的包
.apis(RequestHandlerSelectors.basePackage("com.test.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger文档")
.version("1.0")
.build();
}
/**
* 防止@EnableMvc把默认的静态资源路径覆盖了,手动设置的方式
*
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 解决静态资源无法访问
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
// 解决swagger无法访问
registry.addResourceHandler("/swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
// 解决swagger的js文件无法访问
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
第五步:编写自定义任务类HelloJob
package com.test.jobs;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 自定义Job
*/
public class HelloJob extends QuartzJobBean {
//任务触发时执行此方法
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String time = dateTimeFormatter.format(now);
Object bizId = context.getJobDetail().getJobDataMap().get("bizId");
System.out.println("自定义Job...bizId = " + bizId + " 执行时间" + time);
}
}
第六步:编写启动类QuartzApplication
package com.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class QuartzApplication {
public static void main(String[] args) {
SpringApplication.run(QuartzApplication.class,args);
}
}
第七步:编写QuartzController
package com.test.controller;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/quartz")
public class QuartzController {
//注入调度器对象
@Autowired
private Scheduler scheduler;
}
2.3.1 添加定时任务
在QuartzController中创建save方法
/**
* 添加定时任务
* @param bizId
* @return
*/
@PostMapping
@ApiOperation("添加定时任务")
public String save(String bizId,String cronExpression) throws Exception{
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity(bizId).build();
jobDetail.getJobDataMap().put("bizId",bizId);
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression)
.withMisfireHandlingInstructionDoNothing();
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(bizId).withSchedule(scheduleBuilder).build();
//关联任务和触发器
scheduler.scheduleJob(jobDetail,trigger);
return "OK";
}
2.3.2 暂停定时任务
在QuartzController中创建pause方法
/**
* 暂停定时任务
* @param bizId
* @return
*/
@PutMapping("/pause/{bizId}")
@ApiOperation("暂停定时任务")
public String pause(@PathVariable String bizId) throws Exception{
JobKey jobKey = JobKey.jobKey(bizId);
scheduler.pauseJob(jobKey);
return "OK";
}
2.3.3 恢复定时任务
在QuartzController中创建resume方法
/**
* 恢复定时任务
* @param bizId
* @return
*/
@PutMapping("/resume/{bizId}")
@ApiOperation("恢复定时任务")
public String resume(@PathVariable String bizId) throws Exception{
JobKey jobKey = JobKey.jobKey(bizId);
scheduler.resumeJob(jobKey);
return "OK";
}
2.3.4 删除定时任务
在QuartzController中创建delete方法
/**
* 删除定时任务
* @param bizId
* @return
*/
@DeleteMapping
@ApiOperation("删除定时任务")
public String delete(String bizId) throws Exception{
JobKey jobKey = JobKey.jobKey(bizId);
scheduler.deleteJob(jobKey);
return "OK";
}
2.3.5 立即执行定时任务
在QuartzController中创建run方法
/**
* 立即执行定时任务
* @param bizId
* @return
*/
@PutMapping("/run/{bizId}")
@ApiOperation("立即执行定时任务")
public String run(@PathVariable String bizId) throws Exception{
JobKey jobKey = JobKey.jobKey(bizId);
scheduler.triggerJob(jobKey);
return "OK";
}
2.3.6 更新定时任务
在QuartzController中创建update方法
/**
* 更新定时任务
* @param bizId
* @param cronExpression
* @return
* @throws Exception
*/
@PutMapping
@ApiOperation("更新定时任务")
public String update(String bizId,String cronExpression) throws Exception{
TriggerKey triggerKey = TriggerKey.triggerKey(bizId);
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression)
.withMisfireHandlingInstructionDoNothing();
//获取触发器对象
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
scheduler.rescheduleJob(triggerKey, trigger);
return "OK";
}