簡單設計企業級JOB平臺

SimpleWu發表於2019-08-12
前言

在企業級專案中有許多能夠用到定時任務的場景例如:

  1. 在某個時間點統一給某些使用者傳送郵件資訊
  2. 介面表資料傳送
  3. 某月某日更新報表資料
  4. ......
    目前我們使用SpringBoot快速整合Quartz來進行具體的實現。

    Top1.任務指令碼初始化

    首先我們需要建立官方提供的幾張表,指令碼如下:

-- in your Quartz properties file, you'll need to set org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- 你需要在你的quartz.properties檔案中設定org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- StdJDBCDelegate說明支援叢集,所有的任務資訊都會儲存到資料庫中,可以控制事物,還有就是如果應用伺服器關閉或者重啟,任務資訊都不會丟失,並且可以恢復因伺服器關閉或者重啟而導致執行失敗的任務
-- This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM
-- 這是來自quartz的指令碼,在MySQL資料庫中建立以下的表,修改為使用INNODB而不是MYISAM
-- 你需要在資料庫中執行以下的sql指令碼
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
-- 儲存每一個已配置的Job的詳細資訊
CREATE TABLE QRTZ_JOB_DETAILS
(
  SCHED_NAME        VARCHAR(120) NOT NULL,
  JOB_NAME          VARCHAR(200) NOT NULL,
  JOB_GROUP         VARCHAR(200) NOT NULL,
  DESCRIPTION       VARCHAR(250) NULL,
  JOB_CLASS_NAME    VARCHAR(250) NOT NULL,
  IS_DURABLE        VARCHAR(1)   NOT NULL,
  IS_NONCONCURRENT  VARCHAR(1)   NOT NULL,
  IS_UPDATE_DATA    VARCHAR(1)   NOT NULL,
  REQUESTS_RECOVERY VARCHAR(1)   NOT NULL,
  JOB_DATA          BLOB NULL,
  PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
)
  ENGINE=InnoDB;
-- 儲存已配置的Trigger的資訊
CREATE TABLE QRTZ_TRIGGERS
(
  SCHED_NAME     VARCHAR(120) NOT NULL,
  TRIGGER_NAME   VARCHAR(200) NOT NULL,
  TRIGGER_GROUP  VARCHAR(200) NOT NULL,
  JOB_NAME       VARCHAR(200) NOT NULL,
  JOB_GROUP      VARCHAR(200) NOT NULL,
  DESCRIPTION    VARCHAR(250) NULL,
  NEXT_FIRE_TIME BIGINT(13) NULL,
  PREV_FIRE_TIME BIGINT(13) NULL,
  PRIORITY       INTEGER NULL,
  TRIGGER_STATE  VARCHAR(16)  NOT NULL,
  TRIGGER_TYPE   VARCHAR(8)   NOT NULL,
  START_TIME     BIGINT(13) NOT NULL,
  END_TIME       BIGINT(13) NULL,
  CALENDAR_NAME  VARCHAR(200) NULL,
  MISFIRE_INSTR  SMALLINT(2) NULL,
  JOB_DATA       BLOB NULL,
  PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
  FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP)
    REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP)
)
  ENGINE=InnoDB;
-- 儲存已配置的Simple Trigger的資訊
CREATE TABLE QRTZ_SIMPLE_TRIGGERS
(
  SCHED_NAME      VARCHAR(120) NOT NULL,
  TRIGGER_NAME    VARCHAR(200) NOT NULL,
  TRIGGER_GROUP   VARCHAR(200) NOT NULL,
  REPEAT_COUNT    BIGINT(7) NOT NULL,
  REPEAT_INTERVAL BIGINT(12) NOT NULL,
  TIMES_TRIGGERED BIGINT(10) NOT NULL,
  PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
  FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
)
  ENGINE=InnoDB;
-- 儲存Cron Trigger,包括Cron表示式和時區資訊
CREATE TABLE QRTZ_CRON_TRIGGERS
(
  SCHED_NAME      VARCHAR(120) NOT NULL,
  TRIGGER_NAME    VARCHAR(200) NOT NULL,
  TRIGGER_GROUP   VARCHAR(200) NOT NULL,
  CRON_EXPRESSION VARCHAR(120) NOT NULL,
  TIME_ZONE_ID    VARCHAR(80),
  PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
  FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
)
  ENGINE=InnoDB;
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
  SCHED_NAME    VARCHAR(120) NOT NULL,
  TRIGGER_NAME  VARCHAR(200) NOT NULL,
  TRIGGER_GROUP VARCHAR(200) NOT NULL,
  STR_PROP_1    VARCHAR(512) NULL,
  STR_PROP_2    VARCHAR(512) NULL,
  STR_PROP_3    VARCHAR(512) NULL,
  INT_PROP_1    INT NULL,
  INT_PROP_2    INT NULL,
  LONG_PROP_1   BIGINT NULL,
  LONG_PROP_2   BIGINT NULL,
  DEC_PROP_1    NUMERIC(13,4) NULL,
  DEC_PROP_2    NUMERIC(13,4) NULL,
  BOOL_PROP_1   VARCHAR(1) NULL,
  BOOL_PROP_2   VARCHAR(1) NULL,
  PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
  FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP)
)
  ENGINE=InnoDB;
--  Trigger作為Blob型別儲存(用於Quartz使用者用JDBC建立他們自己定製的Trigger型別,JobStore並不知道如何儲存例項的時候)
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
  SCHED_NAME    VARCHAR(120) NOT NULL,
  TRIGGER_NAME  VARCHAR(200) NOT NULL,
  TRIGGER_GROUP VARCHAR(200) NOT NULL,
  BLOB_DATA     BLOB NULL,
  PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP),
  INDEX (SCHED_NAME,
  TRIGGER_NAME,
  TRIGGER_GROUP
),
  FOREIGN KEY
(
  SCHED_NAME,
  TRIGGER_NAME,
  TRIGGER_GROUP
)
  REFERENCES QRTZ_TRIGGERS
(
  SCHED_NAME,
  TRIGGER_NAME,
  TRIGGER_GROUP
))
  ENGINE=InnoDB;
-- 以Blob型別儲存Quartz的Calendar日曆資訊,quartz可配置一個日曆來指定一個時間範圍
CREATE TABLE QRTZ_CALENDARS
(
  SCHED_NAME    VARCHAR(120) NOT NULL,
  CALENDAR_NAME VARCHAR(200) NOT NULL,
  CALENDAR      BLOB         NOT NULL,
  PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
)
  ENGINE=InnoDB;
-- 儲存已暫停的Trigger組的資訊
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
  SCHED_NAME    VARCHAR(120) NOT NULL,
  TRIGGER_GROUP VARCHAR(200) NOT NULL,
  PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
)
  ENGINE=InnoDB;
-- 儲存與已觸發的Trigger相關的狀態資訊,以及相聯Job的執行資訊
CREATE TABLE QRTZ_FIRED_TRIGGERS
(
  SCHED_NAME        VARCHAR(120) NOT NULL,
  ENTRY_ID          VARCHAR(95)  NOT NULL,
  TRIGGER_NAME      VARCHAR(200) NOT NULL,
  TRIGGER_GROUP     VARCHAR(200) NOT NULL,
  INSTANCE_NAME     VARCHAR(200) NOT NULL,
  FIRED_TIME        BIGINT(13) NOT NULL,
  SCHED_TIME        BIGINT(13) NOT NULL,
  PRIORITY          INTEGER      NOT NULL,
  STATE             VARCHAR(16)  NOT NULL,
  JOB_NAME          VARCHAR(200) NULL,
  JOB_GROUP         VARCHAR(200) NULL,
  IS_NONCONCURRENT  VARCHAR(1) NULL,
  REQUESTS_RECOVERY VARCHAR(1) NULL,
  PRIMARY KEY (SCHED_NAME, ENTRY_ID)
)
  ENGINE=InnoDB;
-- 儲存少量的有關 Scheduler的狀態資訊,和別的 Scheduler 例項(假如是用於一個叢集中)
CREATE TABLE QRTZ_SCHEDULER_STATE
(
  SCHED_NAME        VARCHAR(120) NOT NULL,
  INSTANCE_NAME     VARCHAR(200) NOT NULL,
  LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
  CHECKIN_INTERVAL  BIGINT(13) NOT NULL,
  PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
)
  ENGINE=InnoDB;
-- 儲存程式的非觀鎖的資訊(假如使用了悲觀鎖)
CREATE TABLE QRTZ_LOCKS
(
  SCHED_NAME VARCHAR(120) NOT NULL,
  LOCK_NAME  VARCHAR(40)  NOT NULL,
  PRIMARY KEY (SCHED_NAME, LOCK_NAME)
)
  ENGINE=InnoDB;
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
commit;

有了這些表,就能夠在程式中很好地儲存JOB資訊,但是下面我還會建立一張表進行儲存我們程式中操作的JOB資訊,指令碼如下:

CREATE TABLE `job_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_name` varchar(100) NOT NULL COMMENT 'job名稱',
  `job_class` varchar(255) NOT NULL COMMENT 'job對應類路徑',
  `job_group_name` varchar(100) NOT NULL COMMENT 'job所在組',
  `job_time` varchar(55) NOT NULL COMMENT 'job執行時間',
  `job_type` varchar(2) NOT NULL COMMENT '執行時間型別 1(CRON表示式) 2(秒)',
  `job_count` int(11) NOT NULL DEFAULT '1' COMMENT '執行次數',
  `is_enable` varchar(2) NOT NULL DEFAULT '1' COMMENT '是否可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

這張表主要是用來實時的記錄我們的JOB,由於這張表還缺少許多欄位,如要使用可以根據業務場景自行增加。

Top2.建立可用工程並且匯入依賴

使用IDEA建立名稱為common-quartz的maven工程,並且匯入依賴:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/>
</parent>

<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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!--quartz相關依賴-->
        <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>
        <!--定時任務需要依賴context模組-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Swagger Api -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.2.2</version>
        </dependency>
</dependencies>
<!-- 打包外掛 -->
<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
</build>

建立啟動類CommonQuartzApplication.java,如下:

/**
 * @author:伍梓濤
 * @version:1.0.0
 * @Modified By:SimpleWu
 * @CopyRright (c)2019-:YUM架構平臺
 */
@SpringBootApplication
//多模組載入掃描
//@ComponentScan(basePackages = "")
public class CommonQuartzApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(CommonQuartzApplication.class, args);
    }
}

建立Application.yml檔案,如下:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=UTC
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update #ddl-auto:設為update表示每次都不會重新建表
    show-sql: true
  application:
    name: common-quartz
server:
  port: 8081

這裡我們配置資料庫連線資訊,使用JPA來運算元據庫。

Top3.主要實現

採用自定義任務工廠 整合spring例項來完成構建任務;建立Quartz配置QuartzConfiguration.java並且載入Quartz.properties配置檔案,如下:

/**
 * @author:伍梓濤
 * @version:1.0.0
 * @Modified By:SimpleWu
 * @CopyRright (c)2019-:YUM架構平臺
 */
@Configuration
@EnableScheduling
public class QuartzConfiguration {
    /**
     * 繼承org.springframework.scheduling.quartz.SpringBeanJobFactory
     * 實現任務例項化方式
     */
    public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
            ApplicationContextAware {

        private transient AutowireCapableBeanFactory beanFactory;

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

        /**
         * 將job例項交給spring ioc託管
         * 我們在job例項實現類內可以直接使用spring注入的呼叫被spring ioc管理的例項
         *
         * @param bundle
         * @return
         * @throws Exception
         */
        @Override
        protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            /**
             * 將job例項交付給spring ioc
             */
            beanFactory.autowireBean(job);
            return job;
        }
    }

    /**
     * 配置任務工廠例項
     *
     * @return
     */
    @Bean
    public JobFactory jobFactory() {
        /**
         * 採用自定義任務工廠 整合spring例項來完成構建任務*/
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        return jobFactory;
    }

    /**
     * 配置任務排程器
     * 使用專案資料來源作為quartz資料來源
     *
     * @param jobFactory 自定義配置任務工廠
     * @param dataSource 資料來源例項
     * @return
     * @throws Exception
     */
    @Bean(destroyMethod = "destroy", autowire = Autowire.NO)
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        //將spring管理job自定義工廠交由排程器維護
        schedulerFactoryBean.setJobFactory(jobFactory);
        //設定覆蓋已存在的任務
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //專案啟動完成後,等待2秒後開始執行排程器初始化
        schedulerFactoryBean.setStartupDelay(2);
        //設定排程器自動執行
        schedulerFactoryBean.setAutoStartup(true);
        //設定資料來源,使用與專案統一資料來源
        schedulerFactoryBean.setDataSource(dataSource);
        //設定上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        //設定配置檔案位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }
}

應為在企業中有些會單獨做個任務管理平臺的UI介面,在這裡我們已經匯入了Swagger的依賴,那麼我們就使用Swagger來管理我們的任務達到目的。
現在我們建立Swagger配置,如下:

/**
 * @author:伍梓濤
 * @version:1.0.0
 * @Modified By:SimpleWu
 * @CopyRright (c)2019-:YUM架構平臺
 */
@Configuration
@EnableSwagger2
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                ////為當前包路徑
                .apis(RequestHandlerSelectors.basePackage("com.boot.quartz"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Yum平臺任務管理中心")
                .version("1.0")
                .build();
    }

}

在這裡basePackage在多模組情況,都會有個統一名稱,如com.boot我們直接這樣掃描就好;
同時在啟動類也需要使用@ComponentScan(basePackages = "")來進行多模組bean掃描。
接下來建立任務資訊實體類(對應我們自己建立的那張表,來進行操作任務),如下:

@Table(name = "JOB_INFO")
@Entity
public class JobPojo implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;


    @Column(name = "job_name")
    private String jobName;
    @Column(name = "job_class")
    private String jobClass;
    @Column(name = "job_group_name")
    private String jobGroupName;
    @Column(name = "job_time")
    private String jobTime;
    @Column(name = "is_enable")
    private String isEnable;
    @Column(name = "job_count")
    private Integer jobCount;
    @Column(name = "job_type")
    private String jobType;
    
    //省略GET SET 方法
}

@Table:指定我們實體類對應的表
@Id:主鍵
GeneratedValue(strategy = GenerationType.AUTO):主鍵策略,自增
@Column:欄位對應列名稱
這個張表只是一個半成品,如果還缺少什麼欄位可以自行加入,例如什麼 JOB狀態啊,JOB對應的編碼啊,根據業務需要進行加入即可。
接下來我們建立D層,只需要實現Jpa的介面即可,簡單粗暴:

public interface JobRepository extends JpaRepository<JobPojo, Integer> {
}

接下來我們建立Quartz管理工具類,這個類主要是用來管理JOB的。

@Service
public class QuartzService {

    @Autowired
    private Scheduler scheduler;

    @PostConstruct
    public void startScheduler() {
        try {
            scheduler.start();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 增加一個job
     *
     * @param jobClass
     *            任務實現類
     * @param jobName
     *            任務名稱
     * @param jobGroupName
     *            任務組名
     * @param jobTime
     *            時間表示式 (這是每隔多少秒為一次任務)
     * @param jobTimes
     *            執行的次數 (<0:表示不限次數)
     */
    public void addJob(Class<? extends QuartzJobBean> jobClass, String jobName, String jobGroupName, int jobTime,
                       int jobTimes) {
        try {
            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName)// 任務名稱和組構成任務key
                    .build();
            // 使用simpleTrigger規則
            Trigger trigger = null;
            if (jobTimes < 0) {
                trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)
                        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(1).withIntervalInSeconds(jobTime))
                        .startNow().build();
            } else {
                trigger = TriggerBuilder
                        .newTrigger().withIdentity(jobName, jobGroupName).withSchedule(SimpleScheduleBuilder
                                .repeatSecondlyForever(1).withIntervalInSeconds(jobTime).withRepeatCount(jobTimes))
                        .startNow().build();
            }
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 增加一個job
     *
     * @param jobClass
     *            任務實現類
     * @param jobName
     *            任務名稱
     * @param jobGroupName
     *            任務組名
     * @param jobTime
     *            時間表示式 (如:0/5 * * * * ? )
     */
    public void addJob(Class<? extends QuartzJobBean> jobClass, String jobName, String jobGroupName, String jobTime) {
        try {
            // 建立jobDetail例項,繫結Job實現類
            // 指明job的名稱,所在組的名稱,以及繫結job類
            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName)// 任務名稱和組構成任務key
                    .build();
            // 定義排程觸發規則
            // 使用cornTrigger規則
            Trigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)// 觸發器key
                    .startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
                    .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).startNow().build();
            // 把作業和觸發器註冊到任務排程中
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 修改 一個job的 時間表示式
     *
     * @param jobName
     * @param jobGroupName
     * @param jobTime
     */
    public void updateJob(String jobName, String jobGroupName, String jobTime) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(jobTime)).build();
            // 重啟觸發器
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 刪除任務一個job
     *
     * @param jobName
     *            任務名稱
     * @param jobGroupName
     *            任務組名
     */
    public void deleteJob(String jobName, String jobGroupName) {
        try {
            scheduler.deleteJob(new JobKey(jobName, jobGroupName));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 暫停一個job
     *
     * @param jobName
     * @param jobGroupName
     */
    public void pauseJob(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.pauseJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 恢復一個job
     *
     * @param jobName
     * @param jobGroupName
     */
    public void resumeJob(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.resumeJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 立即執行一個job
     *
     * @param jobName
     * @param jobGroupName
     */
    public void runAJobNow(String jobName, String jobGroupName) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroupName);
            scheduler.triggerJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    /**
     * 獲取所有計劃中的任務列表
     *
     * @return
     */
    public List<Map<String, Object>> queryAllJob() {
        List<Map<String, Object>> jobList = null;
        try {
            GroupMatcher<JobKey> matcher = GroupMatcher.anyJobGroup();
            Set<JobKey> jobKeys = scheduler.getJobKeys(matcher);
            jobList = new ArrayList<Map<String, Object>>();
            for (JobKey jobKey : jobKeys) {
                List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
                for (Trigger trigger : triggers) {
                    Map<String, Object> map = new HashMap<>();
                    map.put("jobName", jobKey.getName());
                    map.put("jobGroupName", jobKey.getGroup());
                    map.put("description", "觸發器:" + trigger.getKey());
                    Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                    map.put("jobStatus", triggerState.name());
                    if (trigger instanceof CronTrigger) {
                        CronTrigger cronTrigger = (CronTrigger) trigger;
                        String cronExpression = cronTrigger.getCronExpression();
                        map.put("jobTime", cronExpression);
                    }
                    jobList.add(map);
                }
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return jobList;
    }

    /**
     * 獲取所有正在執行的job
     *
     * @return
     */
    public List<Map<String, Object>> queryRunJob() {
        List<Map<String, Object>> jobList = null;
        try {
            List<JobExecutionContext> executingJobs = scheduler.getCurrentlyExecutingJobs();
            jobList = new ArrayList<Map<String, Object>>(executingJobs.size());
            for (JobExecutionContext executingJob : executingJobs) {
                Map<String, Object> map = new HashMap<String, Object>();
                JobDetail jobDetail = executingJob.getJobDetail();
                JobKey jobKey = jobDetail.getKey();
                Trigger trigger = executingJob.getTrigger();
                map.put("jobName", jobKey.getName());
                map.put("jobGroupName", jobKey.getGroup());
                map.put("description", "觸發器:" + trigger.getKey());
                Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
                map.put("jobStatus", triggerState.name());
                if (trigger instanceof CronTrigger) {
                    CronTrigger cronTrigger = (CronTrigger) trigger;
                    String cronExpression = cronTrigger.getCronExpression();
                    map.put("jobTime", cronExpression);
                }
                jobList.add(map);
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
        return jobList;
    }

}

@PostConstruct說明:被@PostConstruct修飾的方法會在伺服器載入Servlet的時候執行,並且只會被伺服器呼叫一次,類似於Serclet的inti()方法。被@PostConstruct修飾的方法會在建構函式之後,init()方法之前執行。
好了接下來回到我們之前的那個Job資訊管理上,之前已經建立好D層了,現在我們繼續完善它,給他增加一個業務邏輯層

@Service
public class JobServiceImpl implements JobService {

    private Logger logger = LoggerFactory.getLogger(JobServiceImpl.class);

    @Autowired
    private JobRepository jobRepository;

    @Autowired
    private QuartzService quartzService;

    /**
     * 初始化Job資料
     */
    @Override
    @PostConstruct
    public void initJob(){
        List<JobPojo> jobAll = jobRepository.findAll();
        jobAll.forEach(job->{
            String jobClass = job.getJobClass();
            try {
                Class clazz = Class.forName(jobClass);
                if("1".equals(job.getJobType())){
                    //CRON表示式方式
                    quartzService.addJob(clazz,job.getJobName(),job.getJobGroupName(),job.getJobTime());
                    logger.info("INIT JOB CRON JOB_NAME = " + job.getJobName());
                }else if("2".equals(job.getJobType())){
                    //秒形式
                    quartzService.addJob(clazz,job.getJobName(),job.getJobGroupName(),Integer.valueOf(job.getJobTime()),job.getJobCount());
                    logger.info("INIT JOB JOB_NAME = " + job.getJobName());
                }
            } catch (ClassNotFoundException e) {
                logger.error("JOB INIT ERROR:{}", job);
                logger.error("JOB INIT ERROR MSG:{}", e);
            }
        });
        logger.info("SYSTEM DB JOB INIT ALL SUCCESS!!!");
    }

    @Override
    @Transactional
    public int deleteAlljob() {
        List<JobPojo> jobAll = jobRepository.findAll();
        jobAll.forEach(job->{
            quartzService.deleteJob(job.getJobName(),job.getJobGroupName());
        });
        return 1;
    }
}

在這裡我們呼叫Quartz的工具類進行操作Quartz資訊,initJob主要是為了在啟動的時候立即載入所有的JOB(可根據業務場景操作),之前為什麼說這個表結構的設計是半成品呢,在這裡面
我們通過資料庫欄位來儲存JOB的類路徑反射載入類物件進行任務物件的建立,但是在Quartz中如果已經載入過一次的我們在進行載入重複的JOB是會報錯的,所以我在這裡加了個try catch,可以通過設定一個狀態來控制是否被載入過。
到這裡我們基本的已經完成的差不多了現在來加入訪問介面配合Swagger 進行呼叫如下:

@RestController
@RequestMapping("/quartz/")
@Api(description = "* Quartz任務管理中心", value = "job服務")
public class JobApiController {

    private final Logger logger = LoggerFactory.getLogger(JobApiController.class);

    @Autowired
    private QuartzService quartzService;

    @Autowired
    private JobService jobService;

    @Value("${job.default.group}")
    private String JOB_DEFAULT_GROUP;

    @ApiOperation(value = "* 獲取所有JOB" )
    @GetMapping("/getAllJob")
    public List<Map<String, Object>> getAllJob() {
        return quartzService.queryAllJob();
    }

    @ApiOperation(value = "* 獲取所有正在執行的JOB")
    @GetMapping("/queryRunJob")
    public List<Map<String, Object>> queryRunJob() {
        return quartzService.queryRunJob();
    }

    @ApiOperation(value = "* 立即執行一個JOB")
    @PostMapping("/runJob")
    public String getJobByName(
            @RequestParam(name = "JOB_NAME", required = true) String jobName
            //@RequestParam(name = "JOB_GROUP" , required =  false) String jobGroup
    ) {
        /*if(StringHelper.isNullOrEmptyString(jobGroup)){
            jobGroup = JOB_DEFAULT_GROUP;
        }*/
        quartzService.runAJobNow(jobName, JOB_DEFAULT_GROUP);
        logger.info("run job success jobName={}", jobName);
        return "SUCCESS!!";
    }

    @ApiOperation(value = "* 暫停一個JOB")
    @PostMapping("/pauseJob")
    public String pauseJob(
            @RequestParam(name = "JOB_NAME", required = true) String jobName
            //@RequestParam(name = "JOB_GROUP" , required =  false) String jobGroup
    ) {
        /*if(StringHelper.isNullOrEmptyString(jobGroup)){
            jobGroup = JOB_DEFAULT_GROUP;
        }*/
        quartzService.pauseJob(jobName, JOB_DEFAULT_GROUP);
        logger.info("pause job success jobName={}", jobName);
        return "SUCCESS!!";
    }


    @ApiOperation(value = "* 恢復一個JOB")
    @PostMapping("/resumeJob")
    public String resumeJob(
            @RequestParam(name = "JOB_NAME", required = true) String jobName
            //@RequestParam(name = "JOB_GROUP" , required =  false) String jobGroup
    ) {
        /*if(StringHelper.isNullOrEmptyString(jobGroup)){
            jobGroup = JOB_DEFAULT_GROUP;
        }*/
        quartzService.pauseJob(jobName, JOB_DEFAULT_GROUP);
        logger.info("resume job success jobName={}", jobName);
        return "SUCCESS!!";
    }

    @ApiOperation(value = "* 刪除一個JOB")
    @PostMapping("/deleteJob")
    public String deleteJob(
            @RequestParam(name = "JOB_NAME", required = true) String jobName
            //@RequestParam(name = "JOB_GROUP" , required =  false) String jobGroup
    ) {
        /*if(StringHelper.isNullOrEmptyString(jobGroup)){
            jobGroup = JOB_DEFAULT_GROUP;
        }*/
        quartzService.deleteJob(jobName, JOB_DEFAULT_GROUP);
        logger.info("delete job success jobName={}", jobName);
        return "SUCCESS!!";
    }


    @ApiOperation(value = "* 刪除所有Job")
    @PostMapping("/deleteJobAll")
    public String deleteJobAll() {
        jobService.deleteAlljob();
        logger.info("delete job all success");
        return "SUCCESS!!";
    }

    @ApiOperation(value = "* 重新載入所有Job")
    @PostMapping("/init")
    public String init() {
        jobService.deleteAlljob();
        jobService.initJob();
        logger.info("init job all success");
        return "SUCCESS!!";
    }
}

在我這裡所有的JOB都是同一個GROUP NAME ,因為我這裡沒有給使用者增,改,刪任務的訪問介面只是專案釋出時統一載入的配置,所以我沒必要設計的那麼複雜,如果大家有需要可以基於上面擴充套件一下就行。
我在application.yml中加入了一個預設的GROUP_NAME如下:

job:
  default:
    group: DEFAULT_JOB_GROUP
Top4.使用定時任務實現業務邏輯

建立使用者JOB:

@Component
public class UserJob extends QuartzJobBean {

    //這裡可以注入業務邏輯BEAN
    //如:
    //@Autowired
    //private UserService userService;

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("user job 業務塊");
    }
}

然後編寫資料庫指令碼:

INSERT INTO `quartz`.`job_info` (`id`, `job_name`, `job_class`, `job_group_name`, `job_time`, `job_type`, `job_count`, `is_enable`) VALUES ('1', 'USER_JOB', 'com.boot.quartz.job.UserJob', 'DEFAULT_JOB_GROUP', '0/10 * * * * ? ', '1', '0', '1');

在這裡注意job_name一定不能重複,因為我們用的group name全是一個,並且job_class一定要包名到類名,我們是反射載入的。
我們將工程打包成JAR包,然後首先執行資料庫變更指令碼,然後啟動JAR包。啟動後我們可以看到控制檯每十秒進行一次列印:

2019-08-12 17:20:21.207  INFO 9068 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
2019-08-12 17:20:21.210  INFO 9068 --- [           main] com.boot.quartz.CommonQuartzApplication  : Started CommonQuartzApplication in 7.888 seconds (JVM running for 9.171)
user job 業務塊
user job 業務塊

現在我們訪問Swagger來操作任務:http://localhost:8081/swagger-ui.html
我們執行/quartz/runJob 輸入JOB名稱主動呼叫,可以看到控制檯立馬列印了我們上面寫的那一句話,在這裡其他的我就不試了。
該文原始碼:https://github.com/450255266/open-doubi/tree/master/SpringBoot/common-quartz

相關文章