spring-quartz整合
- 2020 年 12 月 27 日
- 筆記
摘要
spring ,springboot整合quartz-2.3.2,實現spring管理jobBean
本文不涉及 JDBC存儲的方式,springboot yml配置也沒有 可自行百度 Google
本項目源碼gitee地址 quartz-demo
需求
比如發送郵件消息 在夜晚空閑時大批量更新統計數據,定時更新數據
1.0 spring scheduling
在看quartz之前想要先說一下 spring自帶的定時任務框架 spring-scheduling org.springframework.scheduling.annotation.Scheduled
相比於quartz,spring scheduling更加的輕量級 使用配置非常的簡單(基於註解開發) 是實現簡單需求時的最佳選擇
1.1 開啟scheduling配置
<task:annotation-driven />
或者配置類添加註解 @EnableScheduling
使用@Scheduled
官方注釋如下
Processing of {@code @Scheduled} annotations is performed by
registering a {@link ScheduledAnnotationBeanPostProcessor}. This can be
done manually or, more conveniently, through the {@code <task:annotation-driven/>}
element or @{@link EnableScheduling} annotation.
1.2 @Scheduled 內容
其中 cron fixedDelay(fixedDelayString) fixedRate(fixedRateString) 這三個屬性有且只能配置一個 配置錯誤會有類似提示
2.0 下面說說 quartz的配置使用
<!-- springboot項目引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- spring項目引入 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
2.1 quartz api
job: 定時任務執行的業務程式碼層 可以通過實現job介面 或者 繼承 QuartzJobBean 實現
JobDetail: 用來描述任務的分組,名稱 可以通過jobbuilder(推薦) 或者 factoryBean實現,需要將job.class 傳入JobDetail中
trigger: 執行任務的觸發條件 子類SimpleTrigger,CronTrigger.可由TriggerBuilder構建
可以設置 觸發器的的名稱 和分組 dataMap,trigger是載體
SimpleScheduleBuilder,CronScheduleBuilder 這倆builder才是設置執行周期的類
Scheduler: 負責調度 job 含有 Trigger 和job資訊 spring框架中 由 SchedulerFactoryBean創建
JobListener: job trigger Scheduler均有對應的Listener 在任務初始化,執行異常 ,執行結束 可以插入具體的動作
Listener 在 scheduler添加job時可以綁定 scheduler.getListenerManager().addJobListener(new OneJobListener());
2.2 測試先行
先寫一個簡單的測試類
/**
* demo class
*/
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap jobDataMap = context.getTrigger().getJobDataMap();
Object t1 = jobDataMap.get("t1");
Object t2 = jobDataMap.get("t2");
Object j1 = jobDataMap.get("j1");
Object j2 = jobDataMap.get("j2");
Object sv = null ;
try {
sv = context.getScheduler().getContext().get("skey");
}catch (Exception e){
e.printStackTrace();
}
String limiter = ":" ;
System.out.println(t1+limiter+j1);
System.out.println(t2+limiter+j2);
System.out.println(sv);
System.out.println("hello"+ LocalDateTime.now());
}
}
測試
public class QuartzTestSchedule {
@Test
@SneakyThrows
public void test01(){
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
scheduler.shutdown();
}
@Test
@SneakyThrows
public void test02(){
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
SchedulerContext context = scheduler.getContext();
context.put("skey","this is svalue");
SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
.withIdentity("trigger01", "group01")
.usingJobData("t1", "t1_value")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
.build();
JobDetail jobDetail1 = JobBuilder.newJob(HelloJob.class)
.usingJobData("j1", "j1_value")
.withIdentity("myjob", "jobgroup01")
.build();
scheduler.scheduleJob(jobDetail1, simpleTrigger);
scheduler.start();
//防止主執行緒結束 不執行定時任務
Thread.sleep(10_000);
}
}
控制台輸出
t1_value:null
null:null
this is svalue
hello2020-12-27T16:57:01.508
16:57:04.484 ['定時任務'_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobgroup01.myjob', class=site.culater.quartz.HelloJob
16:57:04.484 ['定時任務'_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
16:57:04.484 ['定時任務'_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobgroup01.myjob
t1_value:null
null:null
this is svalue
hello2020-12-27T16:57:04.484
16:57:07.488 ['定時任務'_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'jobgroup01.myjob', class=site.culater.quartz.HelloJob
16:57:07.488 ['定時任務'_Worker-3] DEBUG org.quartz.core.JobRunShell - Calling execute on job jobgroup01.myjob
t1_value:null
null:null
this is svalue
3.0 創建demo項目
首先我們要先創建 quartz demo項目 直接創建springboot項目 引入依賴 過程略……
引入spring-boot-starter-quartz依賴後 因為springboot的自動配置 可以直接用quartz
quartz 默認使用 記憶體存儲方式 JDBC存儲的方式本文不涉及,如果是分散式部署 必須使用jdbc存儲的方式 .
選擇因項目需求定各有優劣
使用 quartz.properties文件
quartz-jar內置一份文件 位置:quartz-2.3.2.jar!\org\quartz\quartz.properties
# 實例名稱 標識 無意義可隨意設置
org.quartz.scheduler.instanceName: '定時任務'
org.quartz.scheduler.instanceId: 'new-quartz'
# 遠程管理
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
# 是否啟用事務 企業級功能
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
#執行緒池實現類
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
# 執行緒池數
org.quartz.threadPool.threadCount: 5
# 執行優先順序
org.quartz.threadPool.threadPriority: 5
#設置程式啟動不執行
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
# 未執行確認時間
org.quartz.jobStore.misfireThreshold: 60000
# 默認記憶體 存儲任務數據
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
quartz.properties配置文件並不是添加到resources目錄下自動載入的 需要手動配置
指定 SchedulerFactoryBean 使用自己創建的
/**
* 直接注入 Scheduler 是無效的
* 對應的xml'配置
* <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
* <property name="configLocation" value="classpath:quartz.properties" />
* // ...
* </bean>
* @return
*/
@Primary
@SneakyThrows
@Bean
public SchedulerFactoryBean schedule(CulaterSpringBeanJobFactory culaterSpringBeanJobFactory){
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
ClassPathResource configLocation = new ClassPathResource("quartz.properties");
schedulerFactoryBean.setConfigLocation(configLocation);
//配置spring管理創建 job 不必每次執行實例 創建job實例
schedulerFactoryBean.setJobFactory(culaterSpringBeanJobFactory);
// schedulerFactoryBean.afterPropertiesSet();
return schedulerFactoryBean ;
}
項目啟動日誌 可以看到設置的定時任務實例名稱
2020-12-27 15:20:48.816 INFO 22532 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler ''定時任務'' initialized from an externally provided properties instance.
2020-12-27 15:20:48.816 INFO 22532 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.2
2020-12-27 15:20:48.816 INFO 22532 --- [ main] org.quartz.core.QuartzScheduler : JobFactory set to: site.culater.quartz.config.CulaterSpringBeanJobFactory@1aa61f3
2020-12-27 15:20:48.969 INFO 22532 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2020-12-27 15:20:49.001 INFO 22532 --- [ main] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now
2020-12-27 15:20:49.001 INFO 22532 --- [ main] org.quartz.core.QuartzScheduler : Scheduler '定時任務'_$_'new-quartz' started.
2020-12-27 15:20:49.016 INFO 22532 --- [ main] site.culater.quartz.QuartzApplication : Started QuartzApplication in 2.02 seconds (JVM running for 4.17)
3.1 schedulerFactoryBean
schedulerFactoryBean 用來創建 Scheduler ,創建完成後再對 schedulerFactoryBean 是無效的,
但是我們從 spring容器中獲得schedulerFactoryBean, get Scheduler是唯一的, 通過scheduler可以動態的添加 修改 刪除 job的執行
創建 triggerBean配置類
/**
* 創建Trigger jobdetail使用
*/
@Configuration
public class TtriggerBean {
@Bean("CulaterJob01")
public CulaterJob getCulaterJob01(){
JobDetail jobDetail = JobBuilder.newJob(OneJob.class).build();
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withRepeatCount(10).withIntervalInSeconds(3))
//.forJob(jobDetail)
.startNow().build();
CulaterJob culaterJob = CulaterJob.builder().trigger(trigger).jobDetail(jobDetail).build();
return culaterJob;
}
@Bean("CulaterJob02")
public CulaterJob getCulaterJob02(){
JobDetail jobDetail = JobBuilder.newJob(OneJob02.class).build();
SimpleTrigger trigger = TriggerBuilder.newTrigger()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withRepeatCount(10).withIntervalInSeconds(3))
//.forJob(jobDetail)
.startNow().build();
CulaterJob culaterJob = CulaterJob.builder().trigger(trigger).jobDetail(jobDetail).build();
return culaterJob;
}
}
TriggerStart實現了ApplicationRunner介面 springboot在項目啟動後 會自動執行run方法,
通過構造方法(lombok註解)注入 的 schedulerFactoryBean獲取Scheduler
culaterJobList 獲得所有的 CulaterJob在spring容器中的所有對象實例
trigger JobBuilder.*.forJob(jobDetail) 這種綁定任務的方法是無效的,必須使用 Scheduler同時添加 jobdetail和trigger
如果沒有添加job 則會提示job不能為null ,job可以繼承QuartzJobBean
注意:不能使用介面匿名內部類 或者內部類的方式創建Job 否則提示 newJob 方法執行失敗
所以創建了 CulaterJob 用來傳遞上述jobdetail,trigger對象
/**
* springboot啟動後添加定時任務
*/
@Component
@RequiredArgsConstructor
public class TriggerStart implements ApplicationRunner {
private final SchedulerFactoryBean schedulerFactoryBean;
private final List<CulaterJob> culaterJobList ;
@Override
public void run(ApplicationArguments args) throws Exception {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
for (CulaterJob culaterJob : culaterJobList) {
scheduler.scheduleJob(culaterJob.getJobDetail(),culaterJob.getTrigger());
}
}
}
這樣一個定時任務就配置完成了 可以運行試試哦,下面我們看看 quartz的任務執行
3.2 從 SchedulerFactoryBean
看quartz
quartz默認每次定時任務運行時創建新的job實例執行後丟棄掉
SchedulerFactoryBean從字面上看就知道是創建Scheduler的工廠方法,這是spring 官方提供的
SchedulerFactoryBean在創建的時候可以設置讀取 properties,可以配置jdbc 數據源
其中一個方法 setJobFactory 如果不設置 則默認AdaptableJobFactory
prepareScheduler(){
.....
Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName);
populateSchedulerContext(scheduler);
if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) {
// Use AdaptableJobFactory as default for a local Scheduler, unless when
// explicitly given a null value through the "jobFactory" bean property.
// 此處設置 默認
this.jobFactory = new AdaptableJobFactory();
}
if (this.jobFactory != null) {
if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) {
((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext);
}
if (this.jobFactory instanceof SchedulerContextAware) {
((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext());
}
scheduler.setJobFactory(this.jobFactory);
}
return scheduler;
}
AdaptableJobFactory中創建實例的方法,打斷點可以看到每次執行都會創建新的job實例
```java
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Class<?> jobClass = bundle.getJobDetail().getJobClass();
return ReflectionUtils.accessibleConstructor(jobClass).newInstance();
}
添加測試類列印job類地址
public class OneJob02 extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
Job jobInstance = context.getJobInstance();
System.out.println("OneTrigger----00002->"+jobInstance);
}
}
控制台列印 可以看到每次列印的地址都不同 每次執行job的實例都是新創建的
OneTrigger----00002->site.culater.quartz.job.OneJob02@fdf5a4
OneTrigger----00002->site.culater.quartz.job.OneJob02@4b06ee
OneTrigger----00002->site.culater.quartz.job.OneJob02@3c4abb
OneTrigger----00002->site.culater.quartz.job.OneJob02@8ce917
OneTrigger----00002->site.culater.quartz.job.OneJob02@5806db
3.3 改進job實例創建 託管spring
如果定時任務執行的很頻繁 我們不希望頻繁的創建銷毀實例 可以 繼承SpringBeanJobFactory 重寫 createJobInstance方法
schedulerFactoryBean.setJobFactory(culaterSpringBeanJobFactory);
只要任務通過spring創建實例則不需要再創建 否則創建新的 實例 也可以都創建實例 將單例多例的控制交給spring
@Component
public class CulaterSpringBeanJobFactory extends SpringBeanJobFactory {
@Autowired
ApplicationContext applicationContext;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
AutowireCapableBeanFactory autowireCapableBeanFactory = applicationContext.getAutowireCapableBeanFactory();
Class<? extends Job> jobClass = bundle.getJobDetail().getJobClass();
Object jobInstance = null;
try {
//如果可以從spring容器中獲得job實例則不需調用父方法創建
jobInstance = autowireCapableBeanFactory.getBean(jobClass);
}
catch (BeansException e) {
// 此處屏蔽異常 沒有找到更好的根據class 獲得bean的方法
}
if (jobInstance == null) {
jobInstance = super.createJobInstance(bundle);
}
return jobInstance;
}
}
3.5 控制並發調度
quartz 默認是並發調度 可能會出現 上一個定時任務執行時間過長還沒結束下一個任務就開始了
如果沒有這方面的需求 則可以在job子類添加@DisallowConcurrentExecution
關閉並發執行
最後控制台的輸出如下: 可以看到 OneJob一直是同一個實例 而且不進行並發調度,OneJob02每次都會創建一個新的實例
OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
OneTrigger----00002->site.culater.quartz.job.OneJob02@fdf5a4
OneTrigger----00002->site.culater.quartz.job.OneJob02@4b06ee
OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
OneTrigger----00002->site.culater.quartz.job.OneJob02@3c4abb
OneTrigger=>site.culater.quartz.job.OneJob@e8de5c
OneTrigger----00002->site.culater.quartz.job.OneJob02@8ce917
OneTrigger----00002->site.culater.quartz.job.OneJob02@5806db
4 使用idea開發能遇到的問題
- 控制台輸出中文亂碼 可以通過 File Encoding 設置 項目和系統編碼為utf-8
- quartz.properties文件沒有被更新至 編譯後的目錄 這時候可以 重新rebuild項目 然後增加刪除文件就可以同步更新了(稍有延遲)
本項目源碼gitee地址 quartz-demo
撲克牌的四種花色分別叫紅桃、梅花、方塊和黑桃。
The suits are called hearts, clubs, diamonds and spades