spring-quartz整合

culater發表於2020-12-27

摘要

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

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

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

相關文章