SpringBoot整合任務排程框架Quartz及持久化配置

天喬巴夏丶發表於2020-12-27

本文側重SpringBoot與Quartz的整合,Quartz的基本入門概念不清楚的小夥伴可以看看這篇文章:任務排程框架Quartz快速入門!

本篇要點

  • 介紹SpringBoot與Quartz單機版整合。
  • 介紹Quartz持久化儲存。

SpringBoot與Quartz單機版快速整合

學習完非Spring環境下Quartz的使用,再來看SpringBoot你會感到更加自如,因為SpringBoot無非是利用它自動配置的特性,將一些重要的Bean自動配置到環境中,我們直接開箱即用,關於Quartz的自動配置定義在QuartzAutoConfiguration中。

引入依賴

主要是spring-boot-starter-quartz這個依賴,是SpringBoot與Quartz的整合。

        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實現對 Quartz 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

建立Job

為了演示兩種Trigger及兩種配置方式,我們建立兩個不同的Job。

@Slf4j
public class FirstJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
        log.info("當前的時間: " + now);
    }
}

@Slf4j
public class SecondJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        String now = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now());
        log.info("SecondJob執行, 當前的時間: " + now);
    }
}

我們在建立Job的時候,可以實現Job介面,也可以繼承QuartzJobBean。

QuartzJobBean實現了Job,並且定義了公用的execute方法,子類可以繼承QuartzJobBean並實現executeInternal方法。

public abstract class QuartzJobBean implements Job {

	/**
	 * This implementation applies the passed-in job data map as bean property
	 * values, and delegates to {@code executeInternal} afterwards.
	 * @see #executeInternal
	 */
	@Override
	public final void execute(JobExecutionContext context) throws JobExecutionException {
		try {
             // 將當前物件包裝為BeanWrapper
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            // 設定屬性
			MutablePropertyValues pvs = new MutablePropertyValues();
			pvs.addPropertyValues(context.getScheduler().getContext());
			pvs.addPropertyValues(context.getMergedJobDataMap());
			bw.setPropertyValues(pvs, true);
		}
		catch (SchedulerException ex) {
			throw new JobExecutionException(ex);
		}
        // 子類實現該方法
		executeInternal(context);
	}

	/**
	 * Execute the actual job. The job data map will already have been
	 * applied as bean property values by execute. The contract is
	 * exactly the same as for the standard Quartz execute method.
	 * @see #execute
	 */
	protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;

}

排程器Scheduler繫結

Scheduler繫結有兩種方式,一種是使用bena的自動配置,一種是Scheduler手動配置。

自動配置,這裡演示SimpleScheduleBuilder

@Configuration
public class QuartzConfig {

    private static final String ID = "SUMMERDAY";

    @Bean
    public JobDetail jobDetail1() {
        return JobBuilder.newJob(FirstJob.class)
                .withIdentity(ID + " 01")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger trigger1() {
        // 簡單的排程計劃的構造器
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5) // 頻率
                .repeatForever(); // 次數

        return TriggerBuilder.newTrigger()
                .forJob(jobDetail1())
                .withIdentity(ID + " 01Trigger")
                .withSchedule(scheduleBuilder)
                .build();
    }
}

手動配置,這裡演示CronScheduleBuilder

@Component
public class JobInit implements ApplicationRunner {

    private static final String ID = "SUMMERDAY";

    @Autowired
    private Scheduler scheduler;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        JobDetail jobDetail = JobBuilder.newJob(FirstJob.class)
                .withIdentity(ID + " 01")
                .storeDurably()
                .build();
        CronScheduleBuilder scheduleBuilder =
                CronScheduleBuilder.cronSchedule("0/5 * * * * ? *");
        // 建立任務觸發器
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(ID + " 01Trigger")
                .withSchedule(scheduleBuilder)
                .startNow() //立即執行一次任務
                .build();
        // 手動將觸發器與任務繫結到排程器內
        scheduler.scheduleJob(jobDetail, trigger);
    }
}

yml配置

spring:
  # Quartz 的配置,對應 QuartzProperties 配置類
  quartz:
    job-store-type: memory # Job 儲存器型別。預設為 memory 表示記憶體,可選 jdbc 使用資料庫。
    auto-startup: true # Quartz 是否自動啟動
    startup-delay: 0 # 延遲 N 秒啟動
    wait-for-jobs-to-complete-on-shutdown: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true
    overwrite-existing-jobs: false # 是否覆蓋已有 Job 的配置
    properties: # 新增 Quartz Scheduler 附加屬性
      org:
        quartz:
          threadPool:
            threadCount: 25 # 執行緒池大小。預設為 10 。
            threadPriority: 5 # 執行緒優先順序
            class: org.quartz.simpl.SimpleThreadPool # 執行緒池型別
#    jdbc: # 這裡暫時不說明,使用 JDBC 的 JobStore 的時候,才需要配置

SpringBoot自動配置中:spring.quartz對應的配置項定義在QuartzProperties中。

主啟動類

@SpringBootApplication
public class DemoSpringBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoSpringBootApplication.class, args);
    }
}

測試

啟動程式,FirstJob每5s執行一次,SecondJob每10s執行一次。

 [eduler_Worker-1] com.hyhwky.standalone.FirstJob           : FirstJob執行, 當前的時間: 2020-12-26 16:54:00
 [eduler_Worker-2] com.hyhwky.standalone.SecondJob          : SecondJob執行, 當前的時間: 2020-12-26 16:54:00
 [eduler_Worker-3] com.hyhwky.standalone.FirstJob           : FirstJob執行, 當前的時間: 2020-12-26 16:54:05
 [eduler_Worker-4] com.hyhwky.standalone.SecondJob          : SecondJob執行, 當前的時間: 2020-12-26 16:54:10
 [eduler_Worker-5] com.hyhwky.standalone.FirstJob           : FirstJob執行, 當前的時間: 2020-12-26 16:54:10
 [eduler_Worker-6] com.hyhwky.standalone.FirstJob           : FirstJob執行, 當前的時間: 2020-12-26 16:54:15
 [eduler_Worker-7] com.hyhwky.standalone.SecondJob          : SecondJob執行, 當前的時間: 2020-12-26 16:54:20

Quartz持久化配置

Quartz持久化配置提供了兩種儲存器:

型別 優點 缺點
RAMJobStore 不要外部資料庫,配置容易,執行速度快 因為排程程式資訊是儲存在被分配給 JVM 的記憶體裡面,所以,當應用程式停止執行時,所有排程資訊將被丟失。另外因為儲存到JVM記憶體裡面,所以可以儲存多少個 Job 和 Trigger 將會受到限制
JDBC 作業儲存 支援叢集,因為所有的任務資訊都會儲存到資料庫中,可以控制事物,還有就是如果應用伺服器關閉或者重啟,任務資訊都不會丟失,並且可以恢復因伺服器關閉或者重啟而導致執行失敗的任務 執行速度的快慢取決與連線資料庫的快慢

建立資料庫表

為了測試Quartz的持久化配置,我們事先在mysql中建立一個資料庫quartz,並執行指令碼,指令碼藏在org\quartz-scheduler\quartz\2.3.2\quartz-2.3.2.jar!\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql,jdbcjobstore中有支援許多種資料庫的指令碼,可以按需執行。

mysql> use quartz;
Database changed
mysql> show tables;
+--------------------------+
| Tables_in_quartz         |
+--------------------------+
| qrtz_blob_triggers       |## blog型別儲存triggers
| qrtz_calendars           |## 以blog型別儲存Calendar資訊
| qrtz_cron_triggers       |## 儲存cron trigger資訊
| qrtz_fired_triggers      |## 儲存已觸發的trigger相關資訊
| qrtz_job_details         |## 儲存每一個已配置的job details
| qrtz_locks               |## 儲存悲觀鎖的資訊
| qrtz_paused_trigger_grps |## 儲存已暫停的trigger組資訊
| qrtz_scheduler_state     |## 儲存Scheduler狀態資訊
| qrtz_simple_triggers     |## 儲存simple trigger資訊
| qrtz_simprop_triggers    |## 儲存其他幾種trigger資訊
| qrtz_triggers            |## 儲存已配置的trigger資訊
+--------------------------+

所有的表中都含有一個SCHED_NAME欄位,對應我們配置的scheduler-name,相同 Scheduler-name的節點,形成一個 Quartz 叢集。

引入mysql相關依賴

    <dependencies>
        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 實現對 Quartz 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>

配置yml

spring:
  datasource:
    quartz:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/quartz?serverTimezone=GMT%2B8
      username: root
      password: 123456
  quartz:
    job-store-type: jdbc # 使用資料庫儲存
    scheduler-name: hyhScheduler # 相同 Scheduler 名字的節點,形成一個 Quartz 叢集
    wait-for-jobs-to-complete-on-shutdown: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true
    jdbc:
      initialize-schema: never # 是否自動使用 SQL 初始化 Quartz 表結構。這裡設定成 never ,我們手動建立表結構。
    properties:
      org:
        quartz:
          # JobStore 相關配置
          jobStore:
            dataSource: quartzDataSource # 使用的資料來源
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 實現類
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_ # Quartz 表字首
            isClustered: true # 是叢集模式
            clusterCheckinInterval: 1000
            useProperties: false
          # 執行緒池相關配置
          threadPool:
            threadCount: 25 # 執行緒池大小。預設為 10 。
            threadPriority: 5 # 執行緒優先順序
            class: org.quartz.simpl.SimpleThreadPool # 執行緒池型別

配置資料來源

@Configuration
public class DataSourceConfiguration {

    private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
        // 建立 HikariDataSource 物件
        HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        // 設定執行緒池名
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }

    /**
     * 建立 quartz 資料來源的配置物件
     */
    @Primary
    @Bean(name = "quartzDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.quartz")
    // 讀取 spring.datasource.quartz 配置到 DataSourceProperties 物件
    public DataSourceProperties quartzDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 建立 quartz 資料來源
     */
    @Bean(name = "quartzDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
    @QuartzDataSource
    public DataSource quartzDataSource() {
        // 獲得 DataSourceProperties 物件
        DataSourceProperties properties = this.quartzDataSourceProperties();
        // 建立 HikariDataSource 物件
        return createHikariDataSource(properties);
    }

}

建立任務

@Component
public class JobInit implements ApplicationRunner {

    private static final String ID = "SUMMERDAY";

    @Autowired
    private Scheduler scheduler;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        JobDetail jobDetail = JobBuilder.newJob(SecondJob.class)
                .withIdentity(ID + " 02")
                .storeDurably()
                .build();
        CronScheduleBuilder scheduleBuilder =
                CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
        // 建立任務觸發器
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(ID + " 02Trigger")
                .withSchedule(scheduleBuilder)
                .startNow() //立即執行一次任務
                .build();
        Set<Trigger> set = new HashSet<>();
        set.add(trigger);
        // boolean replace 表示啟動時對資料庫中的quartz的任務進行覆蓋。
        scheduler.scheduleJob(jobDetail, set, true);
    }
}

啟動測試

啟動測試之後,我們的quartz任務相關資訊就已經成功儲存到mysql中了。

mysql> select * from qrtz_simple_triggers;
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
| SCHED_NAME   | TRIGGER_NAME        | TRIGGER_GROUP | REPEAT_COUNT | REPEAT_INTERVAL | TIMES_TRIGGERED |
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
| hyhScheduler | SUMMERDAY 01Trigger | DEFAULT       |           -1 |            5000 |             812 |
+--------------+---------------------+---------------+--------------+-----------------+-----------------+
1 row in set (0.00 sec)

mysql> select * from qrtz_cron_triggers;
+--------------+---------------------+---------------+------------------+---------------+
| SCHED_NAME   | TRIGGER_NAME        | TRIGGER_GROUP | CRON_EXPRESSION  | TIME_ZONE_ID  |
+--------------+---------------------+---------------+------------------+---------------+
| hyhScheduler | SUMMERDAY 02Trigger | DEFAULT       | 0/10 * * * * ? * | Asia/Shanghai |
+--------------+---------------------+---------------+------------------+---------------+

原始碼下載

本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其他SpringBoot的整合哦。

參考閱讀

相關文章