第三十九章:基于SpringBoot & Quartz完成定時任務(wù)分布式單節(jié)點(diǎn)持久化

定時任務(wù)在企業(yè)項目比較常用到,幾乎所有的項目都會牽扯該功能模塊,定時任務(wù)一般會處理指定時間點(diǎn)執(zhí)行某一些業(yè)務(wù)邏輯、間隔時間執(zhí)行某一些業(yè)務(wù)邏輯等。我們在之前有講過SpringBoot是已經(jīng)集成了定時任務(wù)的,詳見:第二十六章:SpringBoot使用@Scheduled創(chuàng)建定時任務(wù),那么我們本章將會采用外置的quartz定時任務(wù)框架來完成定時任務(wù)的分布式單節(jié)點(diǎn)持久化,我們?yōu)槭裁匆志没〞r任務(wù)呢?

在一些項目中定時任務(wù)可能是必不可少的,由于某種特殊的原因定時任務(wù)可能丟失,如重啟定時任務(wù)服務(wù)項目后,原內(nèi)存中的定時任務(wù)就會被完全釋放!那對于我們來說可能是致命的問題。當(dāng)然也有強(qiáng)制的辦法解決這類問題,但是如果我們把定時任務(wù)持久化到數(shù)據(jù)庫,像維護(hù)普通邏輯數(shù)據(jù)那樣維護(hù)任務(wù),就會避免項目中遇到的種種的特殊情況。

免費(fèi)教程專題

恒宇少年在博客整理三套免費(fèi)學(xué)習(xí)教程專題,由于文章偏多特意添加了閱讀指南,新文章以及之前的文章都會在專題內(nèi)陸續(xù)填充,希望可以幫助大家解惑更多知識點(diǎn)。

本章目標(biāo)

基于SpringBoot架構(gòu)整合定時任務(wù)框架quartz來完成分布式單節(jié)點(diǎn)定時任務(wù)持久化,將任務(wù)持久化到數(shù)據(jù)庫,更好的預(yù)防任務(wù)丟失。

SpringBoot 企業(yè)級核心技術(shù)學(xué)習(xí)專題


專題 專題名稱 專題描述
001 Spring Boot 核心技術(shù) 講解SpringBoot一些企業(yè)級層面的核心組件
002 Spring Boot 核心技術(shù)章節(jié)源碼 Spring Boot 核心技術(shù)簡書每一篇文章碼云對應(yīng)源碼
003 Spring Cloud 核心技術(shù) 對Spring Cloud核心技術(shù)全面講解
004 Spring Cloud 核心技術(shù)章節(jié)源碼 Spring Cloud 核心技術(shù)簡書每一篇文章對應(yīng)源碼
005 QueryDSL 核心技術(shù) 全面講解QueryDSL核心技術(shù)以及基于SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術(shù) 全面講解SpringDataJPA核心技術(shù)
007 SpringBoot核心技術(shù)學(xué)習(xí)目錄 SpringBoot系統(tǒng)的學(xué)習(xí)目錄,敬請關(guān)注點(diǎn)贊?。?

構(gòu)建項目

我們使用idea開發(fā)工具創(chuàng)建一個SpringBoot項目,pom.xml依賴配置如下所示:

...省略部分配置
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <druid.version>1.1.5</druid.version>
        <quartz.version>2.3.0</quartz.version>
    </properties>

    <dependencies>
        <!--spring data jpa相關(guān)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!--web相關(guān)依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--數(shù)據(jù)庫相關(guān)依賴-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--quartz相關(guān)依賴-->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>${quartz.version}</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>${quartz.version}</version>
        </dependency>
        <!--定時任務(wù)需要依賴context模塊-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
...省略部分配置

我們采用的是quartz官方最新版本2.3.0,新版本的任務(wù)調(diào)度框架做出了很多封裝,使用也變得簡易明了。
創(chuàng)建初始化完成,下面我們來創(chuàng)建定時任務(wù)相關(guān)的Configuration配置。

QuartzConfiguration

quartzSpring相關(guān)框架的整合方式有很多種,我們今天采用jobDetail使用Spring Ioc托管方式來完成整合,我們可以在定時任務(wù)實(shí)例中使用Spring注入注解完成業(yè)務(wù)邏輯處理,下面我先把全部的配置貼出來再逐步分析,配置類如下所示:

package com.hengyu.chapter39.configuration;

import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import javax.sql.DataSource;

/**
 * quartz定時任務(wù)配置
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/11/5
 * Time:14:07
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author  恒宇少年
 */
@Configuration
@EnableScheduling
public class QuartzConfiguration
{
    /**
     * 繼承org.springframework.scheduling.quartz.SpringBeanJobFactory
     * 實(shí)現(xiàn)任務(wù)實(shí)例化方式
     */
    public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
            ApplicationContextAware {

        private transient AutowireCapableBeanFactory beanFactory;

        @Override
        public void setApplicationContext(final ApplicationContext context) {
            beanFactory = context.getAutowireCapableBeanFactory();
        }

        /**
         * 將job實(shí)例交給spring ioc托管
         * 我們在job實(shí)例實(shí)現(xiàn)類內(nèi)可以直接使用spring注入的調(diào)用被spring ioc管理的實(shí)例
         * @param bundle
         * @return
         * @throws Exception
         */
        @Override
        protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            /**
             * 將job實(shí)例交付給spring ioc
             */
            beanFactory.autowireBean(job);
            return job;
        }
    }

    /**
     * 配置任務(wù)工廠實(shí)例
     * @param applicationContext spring上下文實(shí)例
     * @return
     */
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext)
    {
        /**
         * 采用自定義任務(wù)工廠 整合spring實(shí)例來完成構(gòu)建任務(wù)
         * see {@link AutowiringSpringBeanJobFactory}
         */
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    /**
     * 配置任務(wù)調(diào)度器
     * 使用項目數(shù)據(jù)源作為quartz數(shù)據(jù)源
     * @param jobFactory 自定義配置任務(wù)工廠
     * @param dataSource 數(shù)據(jù)源實(shí)例
     * @return
     * @throws Exception
     */
    @Bean(destroyMethod = "destroy",autowire = Autowire.NO)
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception
    {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        //將spring管理job自定義工廠交由調(diào)度器維護(hù)
        schedulerFactoryBean.setJobFactory(jobFactory);
        //設(shè)置覆蓋已存在的任務(wù)
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //項目啟動完成后,等待2秒后開始執(zhí)行調(diào)度器初始化
        schedulerFactoryBean.setStartupDelay(2);
        //設(shè)置調(diào)度器自動運(yùn)行
        schedulerFactoryBean.setAutoStartup(true);
        //設(shè)置數(shù)據(jù)源,使用與項目統(tǒng)一數(shù)據(jù)源
        schedulerFactoryBean.setDataSource(dataSource);
        //設(shè)置上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        //設(shè)置配置文件位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }
}

AutowiringSpringBeanJobFactory

可以看到上面配置類中,AutowiringSpringBeanJobFactory我們繼承了SpringBeanJobFactory類,并且通過實(shí)現(xiàn)ApplicationContextAware接口獲取ApplicationContext設(shè)置方法,通過外部實(shí)例化時設(shè)置ApplicationContext實(shí)例對象,在createJobInstance方法內(nèi),我們采用AutowireCapableBeanFactory來托管SpringBeanJobFactory類中createJobInstance方法返回的定時任務(wù)實(shí)例,這樣我們就可以在定時任務(wù)類內(nèi)使用Spring Ioc相關(guān)的注解進(jìn)行注入業(yè)務(wù)邏輯實(shí)例了。

JobFactory

任務(wù)工廠是在本章配置調(diào)度器時所需要的實(shí)例,我們通過jobFactory方法注入ApplicationContext實(shí)例,來創(chuàng)建一個AutowiringSpringBeanJobFactory對象,并且將對象實(shí)例托管到Spring Ioc容器內(nèi)。

SchedulerFactoryBean

我們本章采用的是項目內(nèi)部數(shù)據(jù)源的方式來設(shè)置調(diào)度器的jobSotre,官方quartz有兩種持久化的配置方案。

第一種:采用quartz.properties配置文件配置獨(dú)立的定時任務(wù)數(shù)據(jù)源,可以與使用項目的數(shù)據(jù)庫完全獨(dú)立。
第二種:采用與創(chuàng)建項目統(tǒng)一個數(shù)據(jù)源,定時任務(wù)持久化相關(guān)的表與業(yè)務(wù)邏輯在同一個數(shù)據(jù)庫內(nèi)。

可以根據(jù)實(shí)際的項目需求采取不同的方案,我們本章主要是通過第二種方案來進(jìn)行講解,在上面配置類中可以看到方法schedulerFactoryBean內(nèi)自動注入了JobFactory實(shí)例,也就是我們自定義的AutowiringSpringBeanJobFactory任務(wù)工廠實(shí)例,另外一個參數(shù)就是DataSource,在我們引入spring-starter-data-jpa依賴后會根據(jù)application.yml文件內(nèi)的數(shù)據(jù)源相關(guān)配置自動實(shí)例化DataSource實(shí)例,這里直接注入是沒有問題的。

我們通過調(diào)用SchedulerFactoryBean對象的setConfigLocation方法來設(shè)置quartz定時任務(wù)框架的基本配置,配置文件所在位置:resources/quartz.properties => classpath:/quartz.properties下。

注意:quartz.properties配置文件一定要放在classpath下,放在別的位置有部分功能不會生效。

下面我們來看下quartz.properties文件內(nèi)的配置,如下所示:

#調(diào)度器實(shí)例名稱
org.quartz.scheduler.instanceName = quartzScheduler

#調(diào)度器實(shí)例編號自動生成
org.quartz.scheduler.instanceId = AUTO

#持久化方式配置
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

#持久化方式配置數(shù)據(jù)驅(qū)動,MySQL數(shù)據(jù)庫
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#quartz相關(guān)數(shù)據(jù)表前綴名
org.quartz.jobStore.tablePrefix = QRTZ_

#開啟分布式部署
org.quartz.jobStore.isClustered = true
#配置是否使用
org.quartz.jobStore.useProperties = false

#分布式節(jié)點(diǎn)有效性檢查時間間隔,單位:毫秒
org.quartz.jobStore.clusterCheckinInterval = 20000

#線程池實(shí)現(xiàn)類
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

#執(zhí)行最大并發(fā)線程數(shù)量
org.quartz.threadPool.threadCount = 10

#線程優(yōu)先級
org.quartz.threadPool.threadPriority = 5

#配置為守護(hù)線程,設(shè)置后任務(wù)將不會執(zhí)行
#org.quartz.threadPool.makeThreadsDaemons=true

#配置是否啟動自動加載數(shù)據(jù)庫內(nèi)的定時任務(wù),默認(rèn)true
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

由于我們下一章需要做分布式多節(jié)點(diǎn)自動交付高可用,本章的配置文件加入了分布式相關(guān)的配置。
在上面配置中org.quartz.jobStore.classorg.quartz.jobStore.driverDelegateClass是定時任務(wù)持久化的關(guān)鍵配置,配置了數(shù)據(jù)庫持久化定時任務(wù)以及采用MySQL數(shù)據(jù)庫進(jìn)行連接,當(dāng)然這里我們也可以配置其他的數(shù)據(jù)庫,如下所示:
PostgreSQLorg.quartz.impl.jdbcjobstore.PostgreSQLDelegate
Sybase : org.quartz.impl.jdbcjobstore.SybaseDelegate
MSSQL : org.quartz.impl.jdbcjobstore.MSSQLDelegate
HSQLDB : org.quartz.impl.jdbcjobstore.HSQLDBDelegate
Oracle : org.quartz.impl.jdbcjobstore.oracle.OracleDelegate

org.quartz.jobStore.tablePrefix屬性配置了定時任務(wù)數(shù)據(jù)表的前綴,在quartz官方提供的創(chuàng)建表SQL腳本默認(rèn)就是qrtz_,在對應(yīng)的XxxDelegate驅(qū)動類內(nèi)也是使用的默認(rèn)值,所以這里我們?nèi)绻薷谋砻熬Y,配置可以去掉。

org.quartz.jobStore.isClustered屬性配置了開啟定時任務(wù)分布式功能,再開啟分布式時對應(yīng)屬性org.quartz.scheduler.instanceId 改成Auto配置即可,實(shí)例唯一標(biāo)識會自動生成,這個標(biāo)識具體生成的內(nèi)容,我們一會在運(yùn)行的控制臺就可以看到了,定時任務(wù)分布式準(zhǔn)備好后會輸出相關(guān)的分布式節(jié)點(diǎn)配置信息。

創(chuàng)建表SQL會在本章源碼resources目錄下,源碼地址https://gitee.com/hengboy/spring-boot-chapter。

準(zhǔn)備測試

我們先來創(chuàng)建一個簡單的商品數(shù)據(jù)表,建表SQL如下所示:

DROP TABLE IF EXISTS `basic_good_info`;
CREATE TABLE `basic_good_info` (
  `BGI_ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品編號',
  `BGI_NAME` varchar(20) DEFAULT NULL COMMENT '商品名稱',
  `BGI_PRICE` decimal(8,2) DEFAULT NULL COMMENT '單價',
  `BGI_UNIT` varchar(10) DEFAULT NULL COMMENT '單位',
  PRIMARY KEY (`BGI_ID`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='商品基本信息';

GoodEntity

我們先來針對表basic_good_info創(chuàng)建一個實(shí)體,并且添加JPA相關(guān)的配置,如下所示:

package com.hengyu.chapter39.good.entity;

import lombok.Data;

import javax.persistence.*;
import java.math.BigDecimal;

/**
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:14:59
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Entity
@Table(name = "basic_good_info")
@Data
public class GoodInfoEntity
{
    /**
     * 商品編號
     */
    @Id
    @GeneratedValue
    @Column(name = "bgi_id")
    private Long id;
    /**
     * 商品名稱
     */
    @Column(name = "bgi_name")
    private String name;
    /**
     * 商品單位
     */
    @Column(name = "bgi_unit")
    private String unit;
    /**
     * 商品單價
     */
    @Column(name = "bgi_price")
    private BigDecimal price;
}

下面我們根據(jù)商品實(shí)體來創(chuàng)建JPA接口,如下所示:

/**
 * ========================
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:14:55
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author 恒宇少年
 */
public interface GoodInfoRepository
    extends JpaRepository<GoodInfoEntity,Long>
{
}

接下來我們再來添加一個商品添加的控制器方法,如下所示:

/**
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:02
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@RestController
@RequestMapping(value = "/good")
public class GoodController
{
    /**
     * 商品業(yè)務(wù)邏輯實(shí)現(xiàn)
     */
    @Autowired
    private GoodInfoService goodInfoService;
    /**
     * 添加商品
     * @return
     */
    @RequestMapping(value = "/save")
    public Long save(GoodInfoEntity good) throws Exception
    {
        return goodInfoService.saveGood(good);
    }
}

在請求商品添加方法時,我們調(diào)用了GoodInfoService內(nèi)的saveGood方法,傳遞一個商品的實(shí)例作為參數(shù)。我們接下來看看該類內(nèi)相關(guān)代碼,如下所示:

/**
 * 商品業(yè)務(wù)邏輯
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:04
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class GoodInfoService
{
    /**
     * 注入任務(wù)調(diào)度器
     */
    @Autowired
    private Scheduler scheduler;
    /**
     * 商品數(shù)據(jù)接口
     */
    @Autowired
    private GoodInfoRepository goodInfoRepository;

    /**
     * 保存商品基本信息
     * @param good 商品實(shí)例
     * @return
     */
    public Long saveGood(GoodInfoEntity good) throws Exception
    {
        goodInfoRepository.save(good);
        return good.getId();
    }

我們只是作為保存商品的操作,下面我們來模擬一個需求,在商品添加完成后1分鐘我們通知后續(xù)的邏輯進(jìn)行下一步處理,同時開始商品庫存定時檢查的任務(wù)。

定義商品添加定時任務(wù)

我們先來創(chuàng)建一個任務(wù)實(shí)例,并且繼承org.springframework.scheduling.quartz.QuartzJobBean抽象類,重寫父抽象類內(nèi)的executeInternal方法來實(shí)現(xiàn)任務(wù)的主體邏輯。如下所示:

/**
 * 商品添加定時任務(wù)實(shí)現(xiàn)類
 * ========================
 * Created with IntelliJ IDEA.
 * User:恒宇少年
 * Date:2017/11/5
 * Time:14:47
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 * @author 恒宇少年
 */
public class GoodAddTimer
    extends QuartzJobBean
{
    /**
     * logback
     */
    static Logger logger = LoggerFactory.getLogger(GoodAddTimer.class);
    /**
     * 定時任務(wù)邏輯實(shí)現(xiàn)方法
     * 每當(dāng)觸發(fā)器觸發(fā)時會執(zhí)行該方法邏輯
     * @param jobExecutionContext 任務(wù)執(zhí)行上下文
     * @throws JobExecutionException
     */
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        logger.info("商品添加完成后執(zhí)行任務(wù),任務(wù)時間:{}",new Date());
    }

在任務(wù)主體邏輯內(nèi),我們只是做了一個簡單的輸出任務(wù)執(zhí)行的時間,下面我們再來創(chuàng)建庫存定時檢查任務(wù)。

定義商品庫存檢查任務(wù)

同樣需要繼承org.springframework.scheduling.quartz.QuartzJobBean抽象類實(shí)現(xiàn)抽象類內(nèi)的executeInternal方法,如下所示:

/**
 * 商品庫存檢查定時任務(wù)
 * ========================
 *
 * @author 恒宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/5
 * Time:15:47
 * 碼云:http://git.oschina.net/jnyqy
 * ========================
 */
public class GoodStockCheckTimer
    extends QuartzJobBean
{
    /**
     * logback
     */
    static Logger logger = LoggerFactory.getLogger(GoodStockCheckTimer.class);

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        logger.info("執(zhí)行庫存檢查定時任務(wù),執(zhí)行時間:{}",new Date());
    }
}

都是簡單的做了下日志的輸出,下面我們需要重構(gòu)GoodInfoService內(nèi)的saveGood方法,對應(yīng)的添加上面兩個任務(wù)的創(chuàng)建。

設(shè)置商品添加任務(wù)到調(diào)度器

GoodInfoService類內(nèi)添加buildCreateGoodTimer方法用于實(shí)例化商品添加任務(wù),如下所示:

/**
     * 構(gòu)建創(chuàng)建商品定時任務(wù)
     */
    public void buildCreateGoodTimer() throws Exception
    {
        //設(shè)置開始時間為1分鐘后
        long startAtTime = System.currentTimeMillis() + 1000 * 60;
        //任務(wù)名稱
        String name = UUID.randomUUID().toString();
        //任務(wù)所屬分組
        String group = GoodAddTimer.class.getName();
        //創(chuàng)建任務(wù)
        JobDetail jobDetail = JobBuilder.newJob(GoodAddTimer.class).withIdentity(name,group).build();
        //創(chuàng)建任務(wù)觸發(fā)器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name,group).startAt(new Date(startAtTime)).build();
        //將觸發(fā)器與任務(wù)綁定到調(diào)度器內(nèi)
        scheduler.scheduleJob(jobDetail, trigger);
    }

在上面方法中我們定義的GoodAddTimer實(shí)例只運(yùn)行一次,在商品添加完成后延遲1分鐘進(jìn)行調(diào)用任務(wù)主體邏輯。

其中任務(wù)的名稱以及任務(wù)的分組是為了區(qū)分任務(wù)做的限制,在同一個分組下如果加入同樣名稱的任務(wù),則會提示任務(wù)已經(jīng)存在,添加失敗的提示。

我們通過JobDetail來構(gòu)建一個任務(wù)實(shí)例,設(shè)置GoodAddTimer類作為任務(wù)運(yùn)行目標(biāo)對象,當(dāng)任務(wù)被觸發(fā)時就會執(zhí)行GoodAddTimer內(nèi)的executeInternal方法。

一個任務(wù)需要設(shè)置對應(yīng)的觸發(fā)器,觸發(fā)器也分為很多種,該任務(wù)中我們并沒有采用cron表達(dá)式來設(shè)置觸發(fā)器,而是調(diào)用startAt方法設(shè)置任務(wù)開始執(zhí)行時間。

最后將任務(wù)以及任務(wù)的觸發(fā)器共同交付給任務(wù)調(diào)度器,這樣就完成了一個任務(wù)的設(shè)置。

設(shè)置商品庫存檢查到任務(wù)調(diào)度器

GoodInfoService類內(nèi)添加buildGoodStockTimer方法用于實(shí)例化商品添加任務(wù),如下所示:

/**
     * 構(gòu)建商品庫存定時任務(wù)
     * @throws Exception
     */
    public void buildGoodStockTimer() throws Exception
    {
        //任務(wù)名稱
        String name = UUID.randomUUID().toString();
        //任務(wù)所屬分組
        String group = GoodStockCheckTimer.class.getName();

        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/30 * * * * ?");
        //創(chuàng)建任務(wù)
        JobDetail jobDetail = JobBuilder.newJob(GoodStockCheckTimer.class).withIdentity(name,group).build();
        //創(chuàng)建任務(wù)觸發(fā)器
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity(name,group).withSchedule(scheduleBuilder).build();
        //將觸發(fā)器與任務(wù)綁定到調(diào)度器內(nèi)
        scheduler.scheduleJob(jobDetail, trigger);
    }

該任務(wù)的觸發(fā)器我們采用了cron表達(dá)式來設(shè)置,每隔30秒執(zhí)行一次任務(wù)主體邏輯。

任務(wù)觸發(fā)器在創(chuàng)建時cron表達(dá)式可以搭配startAt方法來同時使用。

下面我們修改GoodInfoService內(nèi)的saveGood方法,分別調(diào)用設(shè)置任務(wù)的兩個方法,如下所示:

/**
     * 保存商品基本信息
     * @param good 商品實(shí)例
     * @return
     */
    public Long saveGood(GoodInfoEntity good) throws Exception
    {
        goodInfoRepository.save(good);
        //構(gòu)建創(chuàng)建商品定時任務(wù)
        buildCreateGoodTimer();
        //構(gòu)建商品庫存定時任務(wù)
        buildGoodStockTimer();
        return good.getId();
    }

下面我們就來測試下任務(wù)是否可以順序的被持久化到數(shù)據(jù)庫,并且是否可以在重啟服務(wù)后執(zhí)行重啟前添加的任務(wù)。

測試

下面我們來啟動項目,啟動成功后,我們來查看控制臺輸出的分布式節(jié)點(diǎn)的信息,如下所示:

2017-11-05 18:09:40.052  INFO 7708 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時任務(wù)分布式節(jié)點(diǎn) - 1 已啟動】】】】】】
2017-11-05 18:09:42.005  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-05 18:09:42.027  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-05 18:09:42.027  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1509876084785"'s failed in-progress jobs.
2017-11-05 18:09:42.031  INFO 7708 --- [lerFactoryBean]] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: ......Freed 1 acquired trigger(s).
2017-11-05 18:09:42.033  INFO 7708 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1509876579404 started.

定時任務(wù)是在項目啟動后2秒進(jìn)行執(zhí)行初始化,并且通過ClusterManager來完成了instance的創(chuàng)建,創(chuàng)建的節(jié)點(diǎn)唯一標(biāo)識為yuqiyu1509876084785。

編寫商品控制器請求方法測試用例,如下所示:

@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter39ApplicationTests {
    /**
     * 模擬mvc測試對象
     */
    private MockMvc mockMvc;

    /**
     * web項目上下文
     */
    @Autowired
    private WebApplicationContext webApplicationContext;

    /**
     * 所有測試方法執(zhí)行之前執(zhí)行該方法
     */
    @Before
    public void before() {
        //獲取mockmvc對象實(shí)例
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    /**
     * 測試添加商品
     * @throws Exception
     */
    @Test
    public void addGood() throws Exception
    {
        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/good/save")
                .param("name","西瓜")
                .param("unit","斤")
                .param("price","12.88")
        )
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().is(200))
                .andReturn();
        result.getResponse().setCharacterEncoding("UTF-8");
        System.out.println(result.getResponse().getContentAsString());
    }

測試用例相關(guān)文章請訪問第三十五章:SpringBoot與單元測試的小秘密,我們來執(zhí)行addGood測試方法,查看控制臺輸出,如下所示:

....省略部分輸出
Hibernate: insert into basic_good_info (bgi_name, bgi_price, bgi_unit) values (?, ?, ?)
2017-11-05 18:06:35.699 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [西瓜]
2017-11-05 18:06:35.701 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [NUMERIC] - [12.88]
2017-11-05 18:06:35.701 TRACE 7560 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [斤]
....省略部分輸出
8
....省略部分輸出

可以看到我們的商品已被成功的寫入到數(shù)據(jù)庫并且輸出的主鍵值,我們的任務(wù)是否也成功的被寫入到數(shù)據(jù)庫了呢?我們來查看qrtz_job_details表內(nèi)任務(wù)列表,如下所示:

schedulerFactoryBean    7567c9d7-76f5-47f3-bc5d-b934f4c1063b    com.hengyu.chapter39.timers.GoodStockCheckTimer     com.hengyu.chapter39.timers.GoodStockCheckTimer 0   0   0   0   0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800
schedulerFactoryBean    e5e08ab0-9be3-43fb-93b8-b9490432a5d7    com.hengyu.chapter39.timers.GoodAddTimer        com.hengyu.chapter39.timers.GoodAddTimer    0   0   0   0   0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787000737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000010770800000010000000007800

任務(wù)已經(jīng)被成功的持久化到數(shù)據(jù)庫內(nèi),等待1分鐘后查看控制臺輸出內(nèi)容如下所示:

2017-11-05 18:12:30.017  INFO 7708 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時任務(wù),執(zhí)行時間:Sun Nov 05 18:12:30 CST 2017
2017-11-05 18:13:00.009  INFO 7708 --- [ryBean_Worker-2] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時任務(wù),執(zhí)行時間:Sun Nov 05 18:13:00 CST 2017
2017-11-05 18:13:02.090  INFO 7708 --- [ryBean_Worker-3] c.hengyu.chapter39.timers.GoodAddTimer   : 商品添加完成后執(zhí)行任務(wù),任務(wù)時間:Sun Nov 05 18:13:02 CST 2017

根據(jù)輸出的內(nèi)容來判定完全吻合我們的配置參數(shù),庫存檢查為30秒執(zhí)行一次,而添加成功后的提醒則是1分鐘后執(zhí)行一次。執(zhí)行完成后就會被直接銷毀,我們再來查看數(shù)據(jù)庫表qrtz_job_details,這時就可以看到還剩下1個任務(wù)

重啟服務(wù)任務(wù)是否自動執(zhí)行?

下面我們把項目重啟下,然后觀察控制臺的輸出內(nèi)容,如下所示:

2017-11-05 18:15:54.018  INFO 7536 --- [           main] c.hengyu.chapter39.Chapter39Application  : 【【【【【【定時任務(wù)分布式節(jié)點(diǎn) - 1 已啟動】】】】】】
2017-11-05 18:15:55.975  INFO 7536 --- [lerFactoryBean]] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now, after delay of 2 seconds
2017-11-05 18:15:56.000  INFO 7536 --- [lerFactoryBean]] org.quartz.core.QuartzScheduler          : Scheduler schedulerFactoryBean_$_yuqiyu1509876953202 started.
2017-11-05 18:16:15.999  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: detected 1 failed or restarted instances.
2017-11-05 18:16:16.000  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: Scanning for instance "yuqiyu1509876579404"'s failed in-progress jobs.
2017-11-05 18:16:16.005  INFO 7536 --- [_ClusterManager] o.s.s.quartz.LocalDataSourceJobStore     : ClusterManager: ......Freed 1 acquired trigger(s).
2017-11-05 18:16:16.041  INFO 7536 --- [ryBean_Worker-1] c.h.c.timers.GoodStockCheckTimer         : 執(zhí)行庫存檢查定時任務(wù),執(zhí)行時間:Sun Nov 05 18:16:16 CST 2017

可以看到成功的自動執(zhí)行了我們在重啟之前配置的任務(wù)。

總結(jié)

本章主要講解了SpringBoot整合quartz定時任務(wù)框架,完成了分布式單節(jié)點(diǎn)任務(wù)持久化,下一章我們會講解任務(wù)參數(shù)傳遞以及分布式多節(jié)點(diǎn)任務(wù)自動負(fù)載。

本章源碼已經(jīng)上傳到碼云:
SpringBoot配套源碼地址:https://gitee.com/hengboy/spring-boot-chapter
SpringCloud配套源碼地址:https://gitee.com/hengboy/spring-cloud-chapter

作者個人 博客
使用開源框架 ApiBoot 助你成為Api接口服務(wù)架構(gòu)師

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內(nèi)容