spring-boot-2.0.3之quartz集成,數據源問題,源碼探究

前言

  開心一刻  html

    着火了,他報警說:119嗎,我家發生火災了。
    119問:在哪裏?
    他說:在我家。
    119問:具體點。
    他說:在我家的廚房裏。
    119問:我說你如今的位置。
    他說:我趴在桌子底下。
    119:咱們怎樣才能到你家?
    他說:大家不是有消防車嗎?
    119說:燒死你個傻B算了。java

  路漫漫其修遠兮,吾將上下而求索!
  github:https://github.com/youzhibing
  碼雲(gitee):https://gitee.com/youzhibinggit

前情回顧

  上篇博客中,講到了springboot與quartz的集成,很是簡單,pow.xml中引入spring-boot-starter-quartz依賴便可,工程中就能夠經過github

@Override
private Scheduler scheduler;

  自動注入quartz調度器,而後咱們就能夠經過調度器對quartz組件:Trigger、JobDetail進行添加與刪除等操做,實現對任務的調度。spring

  結果也如咱們預期同樣,每隔10s咱們的MyJob的executeinternal方法就被調用,打印一條信息:MyJob...sql

  彷佛一切都是那麼順利,感受集成quartz就是這麼簡單!數據庫

  測試工程:spring-boot-quartzspringboot

數據源問題

  產生背景

    若是定時任務不服務於業務,那將毫無心義;咱們不能讓定時任務只是空跑(或者打印一句:MyJob...),若是是,那麼相信我,把這個定時任務刪了吧,不要有任何留戀!mybatis

    既然是服務於咱們的業務,那麼很大程度上就會操做數據庫;個人業務需求就是凌晨某個時間點進行一次數據統計,既要從數據庫查數據,也要將統計後的數據插入到數據庫。那麼問題來了,業務job中如何操做數據庫?app

    業務job示例

package com.lee.quartz.job;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class MyJob extends QuartzJobBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(MyJob.class);

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        // TODO 如何進行數據庫的操做
        System.out.println("MyJob...")
    }
}

    能夠從4個方面來考慮(業務job中如何操做數據庫):

      一、既然是springboot與quartz的集成,那麼咱們能不能用spring的注入功能,將咱們的mapper(集成了mybatis)注入到業務job中了?

      二、利用JobDetail的jobDataMap,將咱們的mapper傳到業務job中

      三、quartz不是有它本身的11張表嗎,那它確定有對數據庫進行操做,咱們參考quartz是如何操做數據庫的

      四、實在是不行,咱們本身建立數據庫鏈接總行了吧

    咱們來逐個分析下以上4種方案

      方案4,我的不推薦,我的比較推薦鏈接池的方式來管理數據庫鏈接,但我的實現數據庫鏈接池已是個不小的挑戰了,不必;不到萬不得已不採用此方案。

      方案1,這個聽起來好像很不錯,鏈接交由spring的數據源管理,咱們只須要用其中的鏈接操做數據庫便可。但看上面的MyJob,spring管理的bean能注入進來嗎,顯然不能,由於MyJob實例不受spring管理;有小夥伴可能會認爲這很簡單,MyJob實例讓spring管理起來不就OK 了! ok,問題又來了,spring管理的MyJob實例能用到quartz中嗎,不能! quartz如何獲取MyJob實例? 咱們把MyJob的類全路徑:com.lee.quartz.job.MyJob傳給了quartz,那麼很顯然quartz會根據這個類全路徑,而後經過反射來實例化MyJob(這也是爲何業務Job必定要有無參構造方法的緣由),也就是quartz會從新建立MyJob實例,與spring管理MyJob實例沒有任何關係。顯然經過spring注入的方式是行不通的(真行不通嗎,請看spring-boot-2.0.3之quartz集成,最佳實踐)。

      方案2,咱們知道能夠經過JobDetail進行參數的傳遞,但有要求:傳遞的參數必須能序列化(實現Serializable);我沒測試此方案,不過我想實現起來會有點麻煩。

      方案3,這個好像可行,咱們能夠看看quartz是如何進行數據庫操做的,咱們把quartz的那套拿過來用是否是就好了呢?

    說了這麼多,方案總結下:

      一、如何利用quartz的數據源(或者數據庫鏈接)進行數據庫操做

      二、引伸下,能不能將quart的數據源設置成咱們應用的數據源,讓quartz與應用共用一個數據源,方便統一管理?

  源碼探究

    一、quartz自身是如何操做數據庫的

      咱們經過暫停任務來跟下源代碼,以下圖所示

      發現獲取connection的方式以下所示:

conn = DBConnectionManager.getInstance().getConnection(getDataSource());

      很明顯,DBConnectionManager是單例的,經過DBConnectionManager從數據源中獲取數據庫鏈接(conn),既然都拿到conn了,那操做數據庫也就簡單了。注意:getDataSource()獲取的是數據源的名稱,不是數據源!

      接下來咱們再看看數據源是什麼數據源,druid?仍是quartz本身的數據源?

      數據源仍是用的咱們應用的數據源(druid數據源),springboot自動將咱們應用的數據源配置給了quartz。

      至此,該問題也就清晰了,總結下:springboot會自動將咱們的應用數據源(druid數據源)配置給quartz,quartz操做數據庫的時候從數據源中獲取數據庫鏈接,而後經過數據庫鏈接對數據庫進行操做。

    二、springboot是如何設置quartz數據源的

      凡是涉及到springboot自動配置的,去找spring-boot-autoconfigure-2.0.3.RELEASE.jar中spring.factories就對了,以下所示

      關於spring.factories文件內容的讀取,你們查閱此篇博文;關於springboot的自動配置,個人springboot啓動源碼系列篇中尚未講到。你們姑且先這樣認爲:

        當在類路徑下能找到Scheduler.class, SchedulerFactoryBean.class,PlatformTransactionManager.class時(只要pom.xml有spring-boot-starter-quartz依賴,這些類就能在類路徑下找到),QuartzAutoConfiguration就會被springboot當成配置類進行自動配置。

      將quartz的配置屬性設置給SchedulerFactoryBean;將數據源設置給SchedulerFactoryBean:若是有@QuartzDataSource修飾的數據源,則將@QuartzDataSource修飾的數據源設置給SchedulerFactoryBean,不然將應用的數據源(druid數據源)設置給SchedulerFactoryBean,顯然咱們的應用中沒有@QuartzDataSource修飾的數據源,那麼SchedulerFactoryBean中的數據源就是應用的數據源;將事務管理器設置給SchedulerFactoryBean。

      SchedulerFactoryBean,Scheduler的工程bean,負責建立和配置quartz Scheduler;它實現了FactoryBean、InitializingBean,FactoryBean的getObject方法實現的很簡單,以下

@Override
@Nullable
public Scheduler getObject() {
    return this.scheduler;
}

      就是返回scheduler實例,註冊到spring容器中,那麼scheduler是在哪裏實例化的呢,就是在afterPropertiesSet中完成的,關於FactoryBean、InitializingBean本文不作過多的講解,不瞭解的能夠先去查閱下資料(注意:InitializingBean的afterPropertiesSet()先於FactoryBean的getObject()執行)。接下來咱們仔細看看SchedulerFactoryBean實現InitializingBean的afterPropertiesSet方法

@Override
public void afterPropertiesSet() throws Exception {
    if (this.dataSource == null && this.nonTransactionalDataSource != null) {
        this.dataSource = this.nonTransactionalDataSource;
    }

    if (this.applicationContext != null && this.resourceLoader == null) {
        this.resourceLoader = this.applicationContext;
    }

    // Initialize the Scheduler instance... 初始化Scheduler實例
    this.scheduler = prepareScheduler(prepareSchedulerFactory());
    try {
        registerListeners();                // 註冊Scheduler相關監聽器,通常沒有
        registerJobsAndTriggers();            // 註冊jobs和triggers, 通常沒有
    }
    catch (Exception ex) {
        try {
            this.scheduler.shutdown(true);
        }
        catch (Exception ex2) {
            logger.debug("Scheduler shutdown exception after registration failure", ex2);
        }
        throw ex;
    }
}
View Code

      咱們來重點跟下:this.scheduler = prepareScheduler(prepareSchedulerFactory());

      能夠看到咱們經過org.quartz.jobStore.dataSource設置的dsName(quartzDs)最後會被替換成springTxDataSource.加scheduler實例名(咱們的應用中是:springTxDataSource.quartzScheduler),這也就是爲何咱們經過DBConnectionManager.getInstance().getConnection("quartzDs")報如下錯誤的緣由

java.sql.SQLException: There is no DataSource named 'quartzDs'
    at org.quartz.utils.DBConnectionManager.getConnection(DBConnectionManager.java:104)
    at com.lee.quartz.job.FetchDataJob.executeInternal(FetchDataJob.java:24)
    at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:75)
    at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
    at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
View Code

      LocalDataSourceJobStore的initialize內容以下

@Override
    public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException {
        // Absolutely needs thread-bound DataSource to initialize.
        this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource();
        if (this.dataSource == null) {
            throw new SchedulerConfigException("No local DataSource found for configuration - " +
                    "'dataSource' property must be set on SchedulerFactoryBean");
        }

        // Configure transactional connection settings for Quartz.
        setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
        setDontSetAutoCommitFalse(true);

        // Register transactional ConnectionProvider for Quartz.
        DBConnectionManager.getInstance().addConnectionProvider(
                TX_DATA_SOURCE_PREFIX + getInstanceName(),
                new ConnectionProvider() {
                    @Override
                    public Connection getConnection() throws SQLException {
                        // Return a transactional Connection, if any.
                        return DataSourceUtils.doGetConnection(dataSource);
                    }
                    @Override
                    public void shutdown() {
                        // Do nothing - a Spring-managed DataSource has its own lifecycle.
                    }
                    /* Quartz 2.2 initialize method */
                    public void initialize() {
                        // Do nothing - a Spring-managed DataSource has its own lifecycle.
                    }
                }
        );

        // Non-transactional DataSource is optional: fall back to default
        // DataSource if not explicitly specified.
        DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
        final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);

        // Configure non-transactional connection settings for Quartz.
        setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());

        // Register non-transactional ConnectionProvider for Quartz.
        DBConnectionManager.getInstance().addConnectionProvider(
                NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
                new ConnectionProvider() {
                    @Override
                    public Connection getConnection() throws SQLException {
                        // Always return a non-transactional Connection.
                        return nonTxDataSourceToUse.getConnection();
                    }
                    @Override
                    public void shutdown() {
                        // Do nothing - a Spring-managed DataSource has its own lifecycle.
                    }
                    /* Quartz 2.2 initialize method */
                    public void initialize() {
                        // Do nothing - a Spring-managed DataSource has its own lifecycle.
                    }
                }
        );

        // No, if HSQL is the platform, we really don't want to use locks...
        try {
            String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getDatabaseProductName");
            productName = JdbcUtils.commonDatabaseName(productName);
            if (productName != null && productName.toLowerCase().contains("hsql")) {
                setUseDBLocks(false);
                setLockHandler(new SimpleSemaphore());
            }
        }
        catch (MetaDataAccessException ex) {
            logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
        }

        super.initialize(loadHelper, signaler);

    }
View Code

      註冊兩個ConnectionProvider給quartz:一個dsName叫springTxDataSource.quartzScheduler,有事務;一個dsName叫springNonTxDataSource.quartzScheduler,沒事務;因此咱們經過DBConnectionManager獲取connection時,經過指定dsName就能獲取支持事務或不支持事務的connection。

      另外,SchedulerFactoryBean實現了SmartLifecycle,會在ApplicationContext refresh的時候啓動Schedule,ApplicationContext shutdown的時候中止Schedule。

總結

  一、springboot集成quartz,應用啓動過程當中會自動調用schedule的start方法來啓動調度器,也就至關於啓動了quartz,緣由是SchedulerFactoryBean實現了SmartLifecycle接口;

  二、springboot會自動將咱們應用的數據源配置給quartz,在咱們示例應用中數據源是druid數據源,應用和quartz都是用的此數據源;

  三、經過org.quartz.jobStore.dataSource設置的數據源名會被覆蓋掉,當咱們經過quartz的DBConnectionManager獲取connection時,默認狀況dbName給springTxDataSource.quartzScheduler或者springNonTxDataSource.quartzScheduler,一個支持事務,一個不支持事務;至於怎樣自定義dsName,我還沒去嘗試,有興趣的小夥伴能夠本身試試;

  四、springboot集成quartz,只是將quartz的一些通用配置給配置好了,若是咱們對quartz十分熟悉,那麼就很好理解,但若是對quartz不熟悉(樓主對quartz就不熟悉),那麼不少時候出了問題就無從下手了,因此建議你們先熟悉quartz;

相關文章
相關標籤/搜索