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

来源:https://www.cnblogs.com/youzhibing/archive/2018/12/05/10056696.html
-Advertisement-
Play Games

前言 開心一刻 著火了,他報警說:119嗎,我家發生火災了。 119問:在哪裡? 他說:在我家。 119問:具體點。 他說:在我家的廚房裡。 119問:我說你現在的位置。 他說:我趴在桌子底下。 119:我們怎樣才能到你家? 他說:你們不是有消防車嗎? 119說:燒死你個傻B算了。 路漫漫其修遠兮, ...


前言

  開心一刻  

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

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

前情回顧

  上篇博客中,講到了springboot與quartz的集成,非常簡單,pow.xml中引入spring-boot-starter-quartz依賴即可,工程中就可以通過

@Override
private Scheduler scheduler;

  自動註入quartz調度器,然後我們就可以通過調度器對quartz組件:Trigger、JobDetail進行添加與刪除等操作,實現對任務的調度。

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

  似乎一切都是那麼順利,感覺集成quartz就是這麼簡單!

  測試工程:spring-boot-quartz

數據源問題

  產生背景

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

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

    業務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中如何操作資料庫):

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

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

      3、quartz不是有它自己的11張表嗎,那它肯定有對資料庫進行操作,我們參考quartz是如何操作資料庫的

      4、實在是不行,我們自己創建資料庫連接總行了吧

    我們來逐個分析下以上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註入的方式是行不通的。

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

      方案3,這個好像可行,我們可以看看quartz是如何進行資料庫操作的,我們把quartz的那套拿過來用是不是就行了呢?

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

      1、如何利用quartz的數據源(或者資料庫連接)進行資料庫操作

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

  源碼探究

    1、quartz自身是如何操作資料庫的

      我們通過暫停任務來跟下源代碼,如下圖所示

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

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

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

      接下來我們再看看數據源是什麼數據源,druid?還是quartz自己的數據源?

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

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

    2、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。

總結

  1、springboot集成quartz,應用啟動過程中會自動調用schedule的start方法來啟動調度器,也就相當於啟動了quartz,原因是SchedulerFactoryBean實現了SmartLifecycle介面;

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

  3、通過org.quartz.jobStore.dataSource設置的數據源名會被覆蓋掉,當我們通過quartz的DBConnectionManager獲取connection時,預設情況dbName給springTxDataSource.quartzScheduler或者springNonTxDataSource.quartzScheduler,一個支持事務,一個不支持事務;至於怎樣自定義dsName,我還沒去嘗試,有興趣的小伙伴可以自己試試;

  4、springboot集成quartz,只是將quartz的一些通用配置給配置好了,如果我們對quartz十分熟悉,那麼就很好理解,但如果對quartz不熟悉(樓主對quartz就不熟悉),那麼很多時候出了問題就無從下手了,所以建議大家先熟悉quartz;


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 繼承 1.Class可以通過extends關鍵字實現繼承。 繼承類的所有屬性和方法。 2.constructor方法和toString方法之中,都出現了super關鍵字,它在這裡表示父類的構造函數,用來新建父類的this對象。 3.子類必須在constructor方法中調用super方法,否則新建實 ...
  • 前端之 HTML 前言 python 基礎、網路編程、併發編程與資料庫要開始告一段落了,從現在開始進入前端的學習。前端的東西多且雜,需要好好地練習。 什麼是前端 前端即網站前臺部分,運行在 PC 端,移動端等瀏覽器上展現給用戶瀏覽的網頁。隨著互聯網技術的發展,HTML5、CSS3,前端框架的應用,跨 ...
  • js獲取月的第幾周和年的第幾周。 來自:https://blog.csdn.net/nu11_/article/details/53910724?utm_source=blogxgwz4 ...
  • 此文介紹好用的數據介面測試工具 Postman,能幫助您方便、快速、統一地管理項目中使用以及測試的數據介面。 ...
  • 2016-10-02 20:19:51 其實 設計模式 有很多人都談過,我自己認為沒有必要再去造輪子之類的,想了想算了,還是開個主題,用作自己以後查找方便。 市面上關於設計模式的書籍琳琅滿目,我自己推薦幾個(如果以後還有,會繼續在這邊文章添加): 下麵列舉的書籍都是豆瓣的鏈接,如果想買書的話,自己去 ...
  • 註解: @Cacheable // 在方法調用前,先在緩存中去找,若沒有,則在方法調用結束後,放到緩存中,屬性cacheNames、key。key中可以使用SpEl表達式,如#id,#root.args[0] @CachePut // 每次調用方法,都會刷新緩存。預設是調用方法後刷新;屬性可以使用 ...
  • 狀態模式(State Pattern)又稱為狀態對象模式,該模式允許一個對象在其內部狀態改變時改變其行為。 定義: 當一個對象內部狀態改變時允許改變行為,這個對象看起來像改變了其類型。 狀態模式的核心是封裝,狀態的變更引起行為的變動,從外部看來就好像該對象對應的類發生改變一樣。 狀態模式的類圖如下所 ...
  • 題意 "題目鏈接" 系統中有兩個數$(a, b)$,請使用$62$以內次詢問來確定出$(a, b)$ 每次可以詢問兩個數$(c, d)$ 若$a \oplus c b \oplus d$返回$1$ 若$a \oplus c = b \oplus d$返回$0$ 若$a \oplus c define ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...