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開發能遇到的問題

  1. 控制台輸出中文亂碼 可以通過 File Encoding 設置 項目和系統編碼為utf-8
  2. quartz.properties文件沒有被更新至 編譯後的目錄 這時候可以 重新rebuild項目 然後增加刪除文件就可以同步更新了(稍有延遲)
本項目源碼gitee地址 quartz-demo

撲克牌的四種花色分別叫紅桃、梅花、方塊和黑桃。
The suits are called hearts, clubs, diamonds and spades