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的相關知識點和問題處理就到此結束了,如有不對,請及時留言。