SpringBoot整合Quartz任务调度框架的使用
Quartz框架是一个可以执行定时任务的框架,虽然spring也有提供定时功能,但功能不够强大,使用的不是很多。所谓的任务调度其实就是定时器,跟你设定一个闹钟在什么时间做什么事情一样。在我们的业务中,经常使用来做订单的超时判断,比如你下订单成功后30分钟没支付就会显示订单失效,这就是使用了定时功能去检查订单的时间。
Demo
完整版demo的项目结构图,先放在前面,以免部分人看不懂写的类该放到哪里去
使用基本步骤
使用Quartz的三大步骤:
1)Job - 任务:你要做什么事
2)Trigger - 触发器 - 你什么时候去做
3) Scheduler - 任务调度 - 你什么时候需要去做什么事
创建工程
使用springboot创建maven工程,在现在新版本中(我的是2.3.2)springboot整合了Quartz相关的资源,所以我们只需要加一个starter就行了
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
创建任务类
定时任务就是需要做的事情,创建Demo 类,继承QuartzJobBean,重写executeInternal方法
package com.wangshili.quartz;
import java.util.Date;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import com.wangshili.service.UserService;
/**
* 需要继承QuartzJobBean重写里面的方法
* @author wangshili
*
*/
public class Demo extends QuartzJobBean{
/**
* 要执行的具体内容
*/
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// TODO Auto-generated method stub
System.out.println(new Date());
}
}
创建Quartz配置类
@Configuration
public class QuartzDemoConfig {
//1.创建任务
@Bean
public JobDetail orderjobDetail() {
//指定job的名称和持久化保存任务
return JobBuilder
.newJob(Demo.class) //引入自定义的任务
.withIdentity("demo")//指定任务名称,随意
.storeDurably()
.build();
}
//2.定义触发器
@Bean
public Trigger orderTrigger(JobDetail jobDetail ) throws IOException {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withRepeatCount(5) //执行次数是从0开始计算的
.withIntervalInMilliseconds(1000); //设定间隔时间。1000ms
//0 0/1 * * * ? 时间表达式 规定任务多久执行一次
// CronScheduleBuilder scheduleBuilder
// = CronScheduleBuilder.cronSchedule("0/2 * * * * ?");
return TriggerBuilder
.newTrigger()
.forJob(orderjobDetail())
.withIdentity("demo") //指定触发器随意
.withSchedule(scheduleBuilder).build();
}
/**
* 3.任务调度 ,创建scheduler
* @throws IOException
*/
@Bean
public SchedulerFactoryBean schedulerFactoryBean(@Autowired Trigger orderTrigger,
@Autowired SpringJobFactory springJobFactory) throws IOException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobDetails(orderjobDetail());
schedulerFactoryBean.setTriggers(orderTrigger);
return schedulerFactoryBean;
}
}
启动后就可以看到打印输出的日期, 间隔1s,打印6次。
解决注入异常,空指针问题
在业务中不只是这么简单的打印,还要执行我们的业务代码,使用到自动注入相关的代码,所以这里我们先创建业务类
@Service
public class UserService {
public void user() {
System.out.println("user");
}
}
改造Demo类
/**
* 需要继承job重写里面的方法
* @author wangshili
*
*/
public class Demo extends QuartzJobBean{
//quartz中的Job是在quartz中实例化出来的,不受spring的管理,所以就导致注入不进去了。
@Autowired
private UserService userService;
/**
* 要执行的具体内容
*/
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// TODO Auto-generated method stub
System.out.println(new Date());
userService.user();
}
}
这里直接运行项目就会出现一个问题,无法注入,在执行userService.user();时会报空指针异常,原因很简单,quartz中的Job是在quartz中实例化出来的,不受spring的管理,所以就导致注入不进去了。
所以我们要重写jobFactory,创建一个类去继承AdaptableJobFactory,重写createJobInstance方法,如下:
这里面的代码可以直接复制过去用就可以了。
/**
* 解决spring bean注入Job的问题
*/
@Component
public class SpringJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
// 进行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
只是添加这个类就可以了吗?当然还没,你得把它设置到调度器中,改造QuartzDemoConfig配置类,如下,在之前的代码上添加加粗的地方就可以了:
@Bean
public SchedulerFactoryBean schedulerFactoryBean(@Autowired Trigger orderTrigger,
@Autowired SpringJobFactory springJobFactory) throws IOException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobDetails(orderjobDetail());
schedulerFactoryBean.setTriggers(orderTrigger);
//注意这里需要将我们重写的jobFactory加入才能生效
schedulerFactoryBean.setJobFactory(springJobFactory);
return schedulerFactoryBean;
}
重新运行项目就可以正常的执行了。
任务持久化
我们先来了解下,Quartz提供两种基本作业存储类型:
—>第一种类型叫做RAMJobStore:
优点:最佳的性能,因为内存中数据访问最快
缺点:不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失
—>第二种类型叫做JDBC作业存储:
优点:保证信息不丢失,即使服务器宕机,重启后仍然能接着完成剩下的任务
缺点:性能差一些,需要操作数据库
通常为了保证我们的任务信息持久化,会采用JDBC的方式作业,我们接下来通过改造我们的项目来熟悉下。
创建数据库
通过上面的描述,持久化是在数据库中的,所以肯定要创建数据库,而这个数据库官方有给,我们需要去下载,http://www.quartz-scheduler.org/downloads/,随便下载一个版本,尽量匹配当前项目中使用的吧,比如我们这个项目是2.3.2的,下载个2.3.1的就行。
下载后是一个tar.gz文件,直接解压得到tar文件,再解压tar就可以解压出来了
进入\quartz-2.3.1-SNAPSHOT\src\org\quartz\impl\jdbcjobstore这个目录
可以看到很多的sql文件,这里我用的是mysql数据库,所以选择对应的就行,然后去数据库创建数据库(sql里面没有创建数据库的语句,需要自己创建),在执行导入sql文件就行了,这里我数据库起名quartz
创建 quartz.properties配置文件
在resources目录创建
#使用自己的配置文件
org.quartz.jobStore.useProperties=true
#默认或是自己改名字都行
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#如果使用集群,instanceId必须唯一,设置成AUTO
org.quartz.scheduler.instanceId=AUTO
#配置线程池
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=10
org.quartz.threadPool.threadPriority=5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
#存储方式使用JobStoreTX,也就是数据库
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#是否使用集群(如果项目只部署到 一台服务器,就不用了)
#org.quartz.jobStore.isClustered = false
#org.quartz.jobStore.clusterCheckinInterval=20000
#配置数据源
org.quartz.jobStore.tablePrefix=qrtz_
#数据源名,随便起,要跟下面搭配
org.quartz.jobStore.dataSource=druid
#数据库中quartz表的表名前缀
org.quartz.dataSource.druid.driver=com.mysql.cj.jdbc.Driver
org.quartz.dataSource.druid.URL=jdbc:mysql://localhost:3306/quartz?serverTimezone=GMT&characterEncoding=utf-8
org.quartz.dataSource.druid.user=root
org.quartz.dataSource.druid.password=root
#最大连接数
org.quartz.dataSource.druid.maxConnections=5
配置文件可以直接复制过去,需要注意的地方是数据库那块的配置,如果是mysql,那driver驱动不用改,需要注意ip和端口和数据库名,用户名和密码都要匹配你自己的。
添加c3p0坐标
因为Quartz默认使用的是c3p0连接池,所以pom文件需要添加对应的坐标
<!-- https://mvnrepository.com/artifact/c3p0/c3p0 -->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
改造配置类
要想配置生效,还得进行配置,配置我们刚才的QuartzDemoConfig类,添加如下代码:
//指定quartz.properties
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
//填写刚刚创建的配置文件名
propertiesFactoryBean.setLocation(new ClassPathResource("quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
修改SchedulerFactoryBean,增加schedulerFactoryBean.setQuartzProperties(quartzProperties());即可
/**
* 3.任务调度 ,创建scheduler
* @throws IOException
*/
@Bean
public SchedulerFactoryBean schedulerFactoryBean(@Autowired Trigger orderTrigger,
@Autowired SpringJobFactory springJobFactory) throws IOException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobDetails(orderjobDetail());
schedulerFactoryBean.setTriggers(orderTrigger);
//###增加此代码即可
schedulerFactoryBean.setQuartzProperties(quartzProperties());
//注意这里需要将我们重写的jobFactory加入才能生效
schedulerFactoryBean.setJobFactory(springJobFactory);
return schedulerFactoryBean;
}
这样就算完成了相关的配置
检验
运行项目,打印一半就停止项目,再启动,看看会不会继续执行剩下的打印次数
图中已经打印了3次,因为我们设置的是5(包括0),那么还剩下3次,我们先看数据库的存储信息,在qrtz_simple_triggers表中,可以看到我们任务的信息,调度器名字、触发器名字、触发器组、目标次数、周期、已执行次数是3。所以我们恢复服务器后,就会根据这个表的内容继续执行我们剩下的任务
重新启动可以发现,执行完3次后就不继续运行了,说明持久化成功。
切换持久化的数据源
因为Quartz默认使用的是c3p0,如果你因业务需求或者喜好需要切换数据源比如druid,那么可以这样做
添加druid的pom坐标
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
修改配置文件
在quartz.properties的配置文件中添加一个配置信息
后面的类全路径名可以先不用管,待会配置成你自己的
#切换数据源成druid,默认使用的是c3p0
org.quartz.dataSource.druid.connectionProvider.class =com.wangshili.config.DruidConnectionProvider
添加配置类
要切换数据源必须重写ConnectionProvider 接口的方法,修改成druid的,具体配置类如下
/**
* Druid连接池的Quartz扩展类
*/
public class DruidConnectionProvider implements ConnectionProvider {
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 常量配置,与quartz.properties文件的key保持一致(去掉前缀),同时提供set方法,Quartz框架自动注入值。
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
//JDBC驱动
public String driver;
//JDBC连接串
public String URL;
//数据库用户名
public String user;
//数据库用户密码
public String password;
//数据库最大连接数
public int maxConnections;
//数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。
public String validationQuery;
private boolean validateOnCheckout;
private int idleConnectionValidationSeconds;
public String maxCachedStatementsPerConnection;
private String discardIdleConnectionsSeconds;
public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;
public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;
//Druid连接池
private DruidDataSource datasource;
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 接口实现
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
public Connection getConnection() throws SQLException {
return datasource.getConnection();
}
public void shutdown() throws SQLException {
datasource.close();
}
public void initialize() throws SQLException{
if (this.URL == null) {
throw new SQLException("DBPool could not be created: DB URL cannot be null");
}
if (this.driver == null) {
throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");
}
if (this.maxConnections < 0) {
throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");
}
datasource = new DruidDataSource();
try{
datasource.setDriverClassName(this.driver);
} catch (Exception e) {
try {
throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);
} catch (SchedulerException e1) {
}
}
datasource.setUrl(this.URL);
datasource.setUsername(this.user);
datasource.setPassword(this.password);
datasource.setMaxActive(this.maxConnections);
datasource.setMinIdle(1);
datasource.setMaxWait(0);
datasource.setMaxPoolPreparedStatementPerConnectionSize(DEFAULT_DB_MAX_CONNECTIONS);
if (this.validationQuery != null) {
datasource.setValidationQuery(this.validationQuery);
if(!this.validateOnCheckout)
datasource.setTestOnReturn(true);
else
datasource.setTestOnBorrow(true);
datasource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);
}
}
/*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* 提供get set方法
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
public String getDriver() {
return driver;
}
public void setDriver(String driver) {
this.driver = driver;
}
public String getURL() {
return URL;
}
public void setURL(String URL) {
this.URL = URL;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMaxConnections() {
return maxConnections;
}
public void setMaxConnections(int maxConnections) {
this.maxConnections = maxConnections;
}
public String getValidationQuery() {
return validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
public boolean isValidateOnCheckout() {
return validateOnCheckout;
}
public void setValidateOnCheckout(boolean validateOnCheckout) {
this.validateOnCheckout = validateOnCheckout;
}
public int getIdleConnectionValidationSeconds() {
return idleConnectionValidationSeconds;
}
public void setIdleConnectionValidationSeconds(int idleConnectionValidationSeconds) {
this.idleConnectionValidationSeconds = idleConnectionValidationSeconds;
}
public DruidDataSource getDatasource() {
return datasource;
}
public void setDatasource(DruidDataSource datasource) {
this.datasource = datasource;
}
public String getDiscardIdleConnectionsSeconds() {
return discardIdleConnectionsSeconds;
}
public void setDiscardIdleConnectionsSeconds(String discardIdleConnectionsSeconds) {
this.discardIdleConnectionsSeconds = discardIdleConnectionsSeconds;
}
}
完结
关于SpringBoot整合Quartz的相关知识点和问题处理就到此结束了,如有不对,请及时留言。