摘要
spring ,springboot整合quartz-2.3.2,實現spring管理jobBean
本文不涉及 JDBC儲存的方式,springboot yml配置也沒有 可自行百度 谷歌
本專案原始碼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