OpenSymphony所提供的Quartz是任務排程領域享譽盛名的開源框架。Spring提供了整合Quartz的功能,可以讓開發人員以更面向Spring的方式建立基於Quartz的任務排程應用。任務排程本身設計多執行緒併發、執行時間規則制定及解析、執行現場保持與恢復、執行緒池維護等諸多方面的工作。如果以自定義執行緒池的原始方法開發,難點很大。
1.普通JAVA任務
啟動基本的Quartz任務包含一下流程:
- 建立任務類:實現Job介面的void execute(JobExecutionContext context)方法,定義被執行任務的執行邏輯;
- 生成JobDetail物件:通過載入任務類(不是例項)來繫結任務邏輯與任務資訊;
- 生成Trigger物件:定時器的觸發時間有兩種方式可以定義,分別是CronSchedule和simpleSchedule()。前者使用正規表示式,後者則是簡單封裝後的定時器。
- 獲取Scheduler物件:通過
StdSchedulerFactory
工廠方法初始化scheduler物件,把任務和定時器繫結在一起,並啟動任務。
完整例項程式碼
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
public class RAMQuartz {
private static Logger logger = LoggerFactory.getLogger(RAMQuartz.class);
public static void main(String[] args) throws SchedulerException {
//建立scheduler
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
//定義一個JobDetail
//定義Job類為RAMJob類,這是真正的執行邏輯所在
JobDetail jb = JobBuilder.newJob(RAMJob1.class)
.withDescription("this is a ram job")
.withIdentity("ramJob", "ramGroup")//定義name/group
.build();
//通過JobDataMap傳遞引數
jb.getJobDataMap().put("Test", "This is test parameter value");
long time = System.currentTimeMillis() + 3*1000L;
Date startTime = new Date(time);
//定義一個Trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withDescription("")
.withIdentity("ramTrigger", "ramTriggerGroup")//定義name/group
.startAt(startTime)//加入scheduler後,在指定時間啟動
//使用CronTrigger
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
.build();
//繫結任務和定時器到排程器
scheduler.scheduleJob(jb,trigger);
//啟動
scheduler.start();
logger.info("啟動時間 : " + new Date());
}
}
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
public class RAMJob1 implements Job{
private static Logger logger = LoggerFactory.getLogger(RAMJob.class);
@Override
public void execute(JobExecutionContext jobExecutionContext)
throws JobExecutionException {
try {
JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();
String str = dataMap.getString("Test");
logger.info("Quartz dataMap : " + new Date() + "\n" + str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.物件注入
在Spring的WEB應用中使用定時器,通常都會用到spring的特性——物件注入。前面的程式碼雖然能夠很好地執行簡單的定時器任務,但是遇到複雜的執行邏輯(如資料庫讀寫等),就不能應付了。
下面程式碼可以看出,任務2需要執行myBatis的資料庫插入語句:
public class RAMJob2 implements Job{
@Autowired
private TestQuartzMapper testQuartzMapper;
private static Logger logger = LoggerFactory.getLogger(RAMJob.class);
@Override
public void execute(JobExecutionContext jobExecutionContext)
throws JobExecutionException {
try {
testQuartzMapper.insertSelective(testQuartz);
logger.info("Insert MyBatis Success!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行這個業務邏輯,就不得不注入物件。如果仍然延用上面的方法,我們會發現執行的時候,testQuartzMapper的物件為null,結果自然毫無懸念地不斷報錯。
如何為我們的定時器注入Spring的物件,下面介紹一下思路:
- 自定義
JobFactory
工廠方法,擴充套件AdaptableJobFactory
,重寫其createJobInstance
方法; - 宣告
SchedulerFactoryBean
,傳入自定義的JobFactory
工廠方法; - 通過新的
SchedulerFactoryBean
獲取scheduler
例項,用注入的方式在需要的地方使用。
完整示例
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
@Component
public class MyJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 呼叫父類的方法
Object jobInstance = super.createJobInstance(bundle);
// 進行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
@Configuration
public class QuartzConfig {
@Autowired
private MyJobFactory myJobFactory;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
// 載入quartz資料來源配置
factory.setQuartzProperties(quartzProperties());
// 自定義Job Factory,用於Spring注入
factory.setJobFactory(myJobFactory);
return factory;
}
@Bean
public Scheduler scheduler() throws IOException, SchedulerException {
Scheduler scheduler = schedulerFactoryBean().getScheduler();
scheduler.start();
return scheduler;
}
}
3.Spring簡單任務
Spring對Quartz進行了封裝,方便開發者呼叫。下面以Spring Boot為例,介紹一下簡單任務在Spring的執行方式。
任務類定義
仔細觀察可以發現,與普通Java任務的區別在於使用了@Component和@EnableScheduling的註釋,相應的,就不用宣告implements Job
,以及重寫execute
方法。這是Spring提供的一種便利。
@Component
@EnableScheduling
public class SpringJob {
@Autowired
WriteService writeService;
private Logger logger = LoggerFactory.getLogger(this.getClass());
public void myJobBusinessMethod() {
this.logger.info("MyFirstExerciseJob哇被觸發了哈哈哈哈哈");
writeService.writeMSG("張三");
}
}
配置JobDetail和Trigger的Bean
MethodInvokingJobDetailFactoryBean
是Spring提供的JobDetail工廠方法,使用它可以快速地定義JobDetail。然而,缺點是生成的任務無法持久化儲存,也就是說,無法管理任務的啟動、暫停、恢復、停止等操作。
CronTriggerFactoryBean
為表示式型觸發器。
@Configuration
public class QuartzJobConfig {
/**
* 方法呼叫任務明細工廠Bean
*/
@Bean(name = "SpringJobBean")
public MethodInvokingJobDetailFactoryBean myFirstExerciseJobBean(SpringJob springJob) {
MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
jobDetail.setConcurrent(false); // 是否併發
jobDetail.setName("general-springJob"); // 任務的名字
jobDetail.setGroup("general"); // 任務的分組
jobDetail.setTargetObject(springJob); // 被執行的物件
jobDetail.setTargetMethod("myJobBusinessMethod"); // 被執行的方法
return jobDetail;
}
/**
* 表示式觸發器工廠Bean
*/
@Bean(name = "SpringJobTrigger")
public CronTriggerFactoryBean myFirstExerciseJobTrigger(@Qualifier("SpringJobBean") MethodInvokingJobDetailFactoryBean springJobBean) {
CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
tigger.setJobDetail(springJobBean.getObject());
tigger.setCronExpression("0/10 * * * * ?"); // 什麼是否觸發,Spring Scheduler Cron表示式
tigger.setName("general-springJobTrigger");
return tigger;
}
}
排程器
下面將任務和觸發器註冊到排程器
@Configuration
public class QuartzConfig {
/**
* 排程器工廠Bean
*/
@Bean(name = "schedulerFactory")
public SchedulerFactoryBean schedulerFactory(@Qualifier("SpringJobTrigger") Trigger springJobTrigger) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
// 覆蓋已存在的任務
bean.setOverwriteExistingJobs(true);
// 延時啟動定時任務,避免系統未完全啟動卻開始執行定時任務的情況
bean.setStartupDelay(15);
// 註冊觸發器
bean.setTriggers(SpringJobTrigger);
return bean;
}
}
完成上述配置後,啟動spring boot就可以出發定時器任務了。而且,仔細觀察上面的程式碼,在執行過程中有WriteService
的spring物件注入,而無需我們自己去自定義JobFactory的Spring物件。
4.持久化
任務持久化需要用到資料庫,而初始化資料庫的SQL可以從下載的釋出版的檔案中找到,比如,我在官網的Download頁下載了當前版本的Full Distribution:Quartz 2.2.3 .tar.gz
,解壓後在quartz-2.2.3\docs\dbTables
能找到初始化指令碼,因我用的是MySQL的Innodb引擎,所以我用此指令碼tables_mysql_innodb.sql
。
配置
預設情況下,排程器的詳情資訊會被儲存在記憶體,模式為:RAMJobStore
,而且也不需要填寫quartz.properties的配置。然而,如果是持久化的模式,那麼quartz.properties就必須填寫,因為檔案中制定了資訊儲存模式和資料來源資訊。
# 執行緒排程器例項名
org.quartz.scheduler.instanceName = quartzScheduler
# 執行緒池的執行緒數,即最多3個任務同時跑
org.quartz.threadPool.threadCount = 3
# 如何儲存任務和觸發器等資訊
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 驅動代理
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 表字首
org.quartz.jobStore.tablePrefix = qrtz_
# 資料來源
org.quartz.jobStore.dataSource = quartzDataSource
# 是否叢集
org.quartz.jobStore.isClustered = false
# 資料來源
# 驅動
org.quartz.dataSource.quartzDataSource.driver = com.mysql.cj.jdbc.Driver
# 連線URL
org.quartz.dataSource.quartzDataSource.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8&useSSL=true&&serverTimezone=Asia/Shanghai
# 使用者名稱
org.quartz.dataSource.quartzDataSource.user = root
# 密碼
org.quartz.dataSource.quartzDataSource.password = 123456
# 最大連線數
org.quartz.dataSource.quartzDataSource.maxConnections = 5
其他內容和RAMJobStore
模式相同。