spring-boot-2.0.3之quartz集成,最佳實踐

来源:https://www.cnblogs.com/youzhibing/archive/2019/01/07/10208056.html
-Advertisement-
Play Games

前言 開心一刻 快過年了,大街上,爺爺在給孫子示範摔炮怎麼放,嘴裡還不停念叨:要像這樣,用勁甩才能響。示範了一個,兩個,三個... 孫子終於忍不住了,抱著爺爺的腿哭起來:爺呀,你給我剩個吧! 新的一年祝大家:健健康康,快快樂樂! 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: debug
View 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狀態。

參考

  Quartz Scheduler


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

-Advertisement-
Play Games
更多相關文章
  • document.write()通過文檔流向頁面寫入 瀏覽器自身的文檔流無法控制關閉 載入JavaScript時的write不會覆蓋原頁面內容 通過函數使用write時,會開啟新的文檔流,因此會覆蓋原頁面內容 ...
  • 預備知識: 必須:Git,GitHub,Jekyll,Markdown,YAML 可選:HTML,JavaScript,CSS,XML 工具: 可選:VSCode+Markdown Preview Github Styling,GitHub Desktop 操作: 註冊一個GitHub的賬號,可以使... ...
  • <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv ...
  • 全選 ...
  • 在狀態模式中,類的行為時基於它的狀態改變而改變。 介紹 狀態模式屬於行為型模式,通過運行對象在內部狀態發生改變時改變它的行為,主要解決的問題是對象的行為嚴重依賴於它的狀態。 類圖描述 代碼實現 1、定義狀態上下文 2、定義行為介面 3、定義行為 4、上層調用 總結 狀態模式封裝了轉換規則,將每種狀態 ...
  • 空對象模式取代簡單的 NULL 值判斷,將空值檢查作為一種不做任何事情的行為。 介紹 在空對象模式中,我們創建一個指定各種要執行的操作的抽象類和擴展該類的實體類,還創建一個未對該類做任何實現的空對象類,該空對象類將無縫地使用在需要檢查空值的地方。 類圖描述 代碼實現 1、定義抽象類 2、定義實體類 ...
  • 適配器模式是作為兩個不相容的介面之間的橋梁。這種類型的設計模糊屬於結構性模式,它結合了兩個獨立介面的功能 ...
  • Django是Python中一個非常牛逼的web框架,他幫我們做了很多事,裡邊也提前封裝了很多牛逼的功能,用起來簡直不要太爽,在寫網站的過程中,增刪改查這幾個基本的功能我們是經常會用到,Django把這系列複雜的邏輯性東西都封裝成了方法,供我們直接使用,在使用過程中的體會是簡單到令人髮指,一個簡單的 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...