前言 開心一刻 快過年了,大街上,爺爺在給孫子示範摔炮怎麼放,嘴裡還不停念叨:要像這樣,用勁甩才能響。示範了一個,兩個,三個... 孫子終於忍不住了,抱著爺爺的腿哭起來:爺呀,你給我剩個吧! 新的一年祝大家:健健康康,快快樂樂! github:https://github.com/youzhibin ...
前言
開心一刻
快過年了,大街上,爺爺在給孫子示範摔炮怎麼放,嘴裡還不停念叨:要像這樣,用勁甩才能響。示範了一個,兩個,三個... 孫子終於忍不住了,抱著爺爺的腿哭起來:爺呀,你給我剩個吧!
新的一年祝大家:健健康康,快快樂樂!
github:https://github.com/youzhibing
碼雲(gitee):https://gitee.com/youzhibing
前情回顧與問題
spring-boot-2.0.3之quartz集成,不是你想的那樣哦! 講到了quartz的基本概念,以及springboot與quartz的集成;集成非常簡單,引入相關依賴即可,此時我們job存儲方式採用的是jdbc。
spring-boot-2.0.3之quartz集成,數據源問題,源碼探究 講到了quartz的數據源問題,如果我們沒有@QuartzDataSource修飾的數據源,那麼預設情況下就是我們的工程數據源,springboot會將工程數據源設置給quartz;為什麼需要數據源,因為我們的job不會空跑,往往會進行資料庫的操作,那麼就會用到資料庫連接,而獲取資料庫連接最常用的的方式就是從數據源獲取。
後續使用過程中,發現了一些問題:
1、spring註入,job到底能不能註入到spring容器,job中能不能自動註入我們的mapper(spring的autowired);
2、job存儲方式,到底用JDBC還是MEMORY,最佳實踐是什麼
3、調度失準,沒有嚴格按照我們的cron配置進行
spring註入
spring-boot-2.0.3之quartz集成,數據源問題,源碼探究中我還分析的井井有條,並很自信的得出結論:job不能註入到spring,也不能享受spring的自動註入
那時候採用的是從quartz數據源中獲取connection,然後進行jdbc編程,發現jdbc用起來真的不舒服(不是說有問題,mybatis、spring jdbcTemplate等底層也是jdbc),此時我就有了一個疑問:quartz job真的不能註入到spring、不能享受spring的自動註入嗎? 結論可想而知:能!
打的真疼
job能不能註入到spring容器? 答案是可以的(各種註解:@Compoment、@Service、@Repository等),只是我們將job註入到spring容器有意義嗎? 我們知道quartz是通過反射來實例化job的(具體實例化過程請往下看),與spring中已存在的job bean沒有任何關聯,我們將job註入到spring也只是使spring中多了一個沒調用者的bean而已,沒有任何意義。這個問題應該換個方式來問:job有必要註入到spring容器中嗎? 很顯然沒必要。
job中能不能註入spring中的常規bean了? 答案是可以的。我們先來看下springboot官網是如何描述的:job可以定義setter來註入data map屬性,也可以以類似的方式註入常規bean,如下所示
public class SampleJob extends QuartzJobBean { private MyService myService; private String name; // Inject "MyService" bean (註入spring 常規bean) public void setMyService(MyService myService) { ... } // Inject the "name" job data property (註入job data 屬性) public void setName(String name) { ... } @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { ... } }View Code
實現
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lee</groupId> <artifactId>spring-boot-quartz</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <druid.version>1.1.10</druid.version> <pagehelper.version>1.2.5</pagehelper.version> <druid.version>1.1.10</druid.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper.version}</version> </dependency> <!-- 日誌 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> <exclusions> <!-- 排除spring-boot-starter-logging中的全部依賴 --> <exclusion> <groupId>*</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> <scope>test</scope> <!-- 打包的時候不打spring-boot-starter-logging.jar --> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <finalName>spring-boot-quartz</finalName> <plugins> <!-- 打包項目 mvn clean package --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>View Code
application.yml
server: port: 9001 servlet: context-path: /quartz spring: thymeleaf: mode: HTML cache: false #連接池配置 datasource: type: com.alibaba.druid.pool.DruidDataSource name: ownDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/spring-boot-quartz?useSSL=false&useUnicode=true username: root password: 123456 initial-size: 1 #連接池初始大小 max-active: 20 #連接池中最大的活躍連接數 min-idle: 1 #連接池中最小的活躍連接數 max-wait: 60000 #配置獲取連接等待超時的時間 pool-prepared-statements: true #打開PSCache,並且指定每個連接上PSCache的大小 max-pool-prepared-statement-per-connection-size: 20 validation-query: SELECT 1 FROM DUAL validation-query-timeout: 30000 test-on-borrow: false #是否在獲得連接後檢測其可用性 test-on-return: false #是否在連接放回連接池後檢測其可用性 test-while-idle: true #是否在連接空閑一段時間後檢測其可用性 quartz: #相關屬性配置 properties: org: quartz: scheduler: instanceName: quartzScheduler instanceId: AUTO threadPool: class: org.quartz.simpl.SimpleThreadPool threadCount: 10 threadPriority: 5 threadsInheritContextClassLoaderOfInitializingThread: true #mybatis配置 mybatis: type-aliases-package: com.lee.quartz.entity mapper-locations: classpath:mybatis/mapper/*.xml #分頁配置, pageHelper是物理分頁插件 pagehelper: #4.0.0以後版本可以不設置該參數,該示例中是5.1.4 helper-dialect: mysql #啟用合理化,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最後一頁 reasonable: true logging: level: com.lee.quartz.mapper: debugView Code
FetchDataJob.java
package com.lee.quartz.job; import com.lee.quartz.entity.User; import com.lee.quartz.mapper.UserMapper; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import java.util.Random; import java.util.stream.IntStream; public class FetchDataJob extends QuartzJobBean { private static final Logger LOGGER = LoggerFactory.getLogger(FetchDataJob.class); @Autowired private UserMapper userMapper; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { // TODO 業務處理 Random random = new Random(); IntStream intStream = random.ints(18, 100); int first = intStream.limit(1).findFirst().getAsInt(); int count = userMapper.saveUser(new User("zhangsan" + first, first)); if (count == 0) { LOGGER.error("用戶保存失敗!"); return; } LOGGER.info("用戶保存成功"); } }View Code
如上,FetchDataJob中是可以註入userMapper的,完整代碼請看:spring-boot-quartz-plus
job實例化過程源碼解析
還記得SchedulerFactoryBean的創建嗎,可以看看這裡,我們從SchedulerFactoryBean開始
QuartzSchedulerThread線程的啟動
QuartzSchedulerThread聲明如下
View Code負責觸發QuartzScheduler註冊的Triggers,可以理解成quartz的主線程(守護線程)。我們從SchedulerFactoryBean的afterPropertiesSet()開始
QuartzSchedulerThread繼承了Thread,通過DefaultThreadExecutor的execute()啟動了QuartzSchedulerThread線程
jobFactory的創建與替換
AutowireCapableBeanJobFactory實例後續會賦值給quartz,作為quartz job的工廠,具體在哪賦值給quartz的了,我們往下看
當quartz scheduler創建完成後,將scheduler的jobFactory替換成了AutowireCapableBeanJobFactory。
job的創建與執行
QuartzSchedulerThread在上面已經啟動了,AutowireCapableBeanJobFactory也已經賦值給了scheduler;我們來看看QuartzSchedulerThread的run(),裡面有job的創建與執行
最終會調用AutowireCapableBeanJobFactory的createJobInstance方法,通過反射創建了job實例,還向job實例中填充了job data map屬性和spring常規bean。具體this.beanFactory.autowireBean(jobInstance);是如何向job實例填充spring常規bean的,需要大家自己去跟了。job被封裝成了JobRunShell(實現了Runnable),然後從線程池中取第一個線程來執行JobRunShell,最終會執行到FetchDataJob的executeInternal,處理我們的業務;quartz的線程實現與線程機制,有興趣的小伙伴自行去看。
小結下:先啟動QuartzSchedulerThrea線程,然後將quartz的jobFactory替換成AutowireCapableBeanJobFactory;QuartzSchedulerThread是一個守護線程,會按規則處理trigger和job(要成對存在),最終完成我們的定時業務。
job存儲方式
JobStore是負責跟蹤調度器(scheduler)中所有的工作數據:作業任務、觸發器、日曆等。我們無需在我們的代碼中直接使用JobStore實例,只需要通過配置信息告知Quartz該用哪個JobStore即可。quartz的JobStore有兩種:RAMJobStore、JDBCJobStore,通過名字我們也能猜到這兩者之間的區別與優缺點
上述兩種JobStore對應到springboot就是:MEMORY、JDBC
/* * Copyright 2012-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.boot.autoconfigure.quartz; /** * Define the supported Quartz {@code JobStore}. * * @author Stephane Nicoll * @since 2.0.0 */ public enum JobStoreType { /** * Store jobs in memory. */ MEMORY, /** * Store jobs in the database. */ JDBC }View Code
至於選擇哪種方式,就看哪種方式更契合我們的業務需求,沒有絕對的選擇誰與不選擇誰,只看哪種更合適。據我的理解和工作中的應用,記憶體方式用的更多;實際應用中,我們往往只是持久化我們自定義的基礎job(不是quartz的job)到資料庫,應用啟動的時候載入基礎job到quartz中,進行quartz job的初始化,quartz的job相關信息全部存儲在RAM中;一旦應用停止,quartz的job信息全部丟失,但這影響不大,可以通過我們的自定義job進行quartz job的恢復,但是恢復的quartz job是原始狀態,如果需要實時保存quartz job的狀態,那就需要另外設計或者用JDBC方式了。
調度失準
當存儲方式是JDBCJobStore時,會出現調度失準的情況,沒有嚴格按照配置的cron表達式執行,例如cron表達式:1 */1 * * * ?,日誌輸入如下
秒數會有不對,但這影響比較小,我們還能接受,可是時間間隔有時候卻由1分鐘變成2分鐘,甚至3分鐘,這個就有點接受不了。具體原因我還沒有查明,個人覺得可能和資料庫持久化有關。
當存儲方式是RAMJobStore時,調度很準,還未發現調度失準的情況,cron表達式:3 */1 * * * ?,日誌輸入如下
總結
1、quartz job無需註入到spring容器中(註入進去了也沒用),但quartz job中是可以註入spring容器中的常規bean的,當然還可以註入jab data map中的屬性值;
2、 springboot覆寫了quartz的jobFactory,使得quartz在調用jobFactory創建job實例的時候,能夠將spring容器的bean註入到job中,AutowireCapableBeanJobFactory中createJobInstance方法如下
@Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { Object jobInstance = super.createJobInstance(bundle); // 通過反射實例化job,並將JobDataMap中屬性註入到job實例中 this.beanFactory.autowireBean(jobInstance); // 註入job依賴的spring中的bean this.beanFactory.initializeBean(jobInstance, null); return jobInstance; }
3、最佳實踐
JobStore選擇RAMJobStore;持久化我們自定義的job,應用啟動的時候將我們自定義的job都載入給quartz,初始化quartz job;quartz job狀態改變的時候,分析清楚是否需要同步到我們自定義的job中,有則同步改變自定義job狀態。