Quartz:基本用法總結

韌卓發表於2020-08-21

OpenSymphony所提供的Quartz是任務排程領域享譽盛名的開源框架。Spring提供了整合Quartz的功能,可以讓開發人員以更面向Spring的方式建立基於Quartz的任務排程應用。任務排程本身設計多執行緒併發、執行時間規則制定及解析、執行現場保持與恢復、執行緒池維護等諸多方面的工作。如果以自定義執行緒池的原始方法開發,難點很大。

1.普通JAVA任務

啟動基本的Quartz任務包含一下流程:

  1. 建立任務類:實現Job介面的void execute(JobExecutionContext context)方法,定義被執行任務的執行邏輯;
  2. 生成JobDetail物件:通過載入任務類(不是例項)來繫結任務邏輯與任務資訊;
  3. 生成Trigger物件:定時器的觸發時間有兩種方式可以定義,分別是CronSchedule和simpleSchedule()。前者使用正規表示式,後者則是簡單封裝後的定時器。
  4. 獲取Scheduler物件:通過StdSchedulerFactory工廠方法初始化scheduler物件,把任務和定時器繫結在一起,並啟動任務。

完整例項程式碼

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;

public class RAMQuartz {
    private static Logger logger = LoggerFactory.getLogger(RAMQuartz.class);

    public static void main(String[] args) throws SchedulerException {
    	//建立scheduler
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();

	//定義一個JobDetail
	//定義Job類為RAMJob類,這是真正的執行邏輯所在
        JobDetail jb = JobBuilder.newJob(RAMJob1.class) 
                .withDescription("this is a ram job")
                .withIdentity("ramJob", "ramGroup")//定義name/group
                .build();
	//通過JobDataMap傳遞引數
        jb.getJobDataMap().put("Test", "This is test parameter value");

        long time = System.currentTimeMillis() + 3*1000L;
        Date startTime = new Date(time);

	//定義一個Trigger
        Trigger trigger = TriggerBuilder.newTrigger()
                .withDescription("")
                .withIdentity("ramTrigger", "ramTriggerGroup")//定義name/group
                .startAt(startTime)//加入scheduler後,在指定時間啟動
                //使用CronTrigger
                .withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
                .build();
	//繫結任務和定時器到排程器
        scheduler.scheduleJob(jb,trigger);

	//啟動
        scheduler.start();
        logger.info("啟動時間 : " + new Date());
    }
}
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Date;

public class RAMJob1 implements Job{

    private static Logger logger = LoggerFactory.getLogger(RAMJob.class);

    @Override
    public void execute(JobExecutionContext jobExecutionContext) 
    				throws JobExecutionException {

        try {
            JobDataMap dataMap = jobExecutionContext.getJobDetail().getJobDataMap();
            String str = dataMap.getString("Test");
            logger.info("Quartz dataMap : " + new Date() + "\n" + str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.物件注入

在Spring的WEB應用中使用定時器,通常都會用到spring的特性——物件注入。前面的程式碼雖然能夠很好地執行簡單的定時器任務,但是遇到複雜的執行邏輯(如資料庫讀寫等),就不能應付了。

下面程式碼可以看出,任務2需要執行myBatis的資料庫插入語句:

public class RAMJob2 implements Job{

    @Autowired
    private TestQuartzMapper testQuartzMapper;

    private static Logger logger = LoggerFactory.getLogger(RAMJob.class);

    @Override
    public void execute(JobExecutionContext jobExecutionContext)
    			 throws JobExecutionException {

        try {
            testQuartzMapper.insertSelective(testQuartz);
            logger.info("Insert MyBatis Success!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行這個業務邏輯,就不得不注入物件。如果仍然延用上面的方法,我們會發現執行的時候,testQuartzMapper的物件為null,結果自然毫無懸念地不斷報錯。

如何為我們的定時器注入Spring的物件,下面介紹一下思路:

  1. 自定義JobFactory工廠方法,擴充套件AdaptableJobFactory,重寫其createJobInstance方法;
  2. 宣告SchedulerFactoryBean,傳入自定義的JobFactory工廠方法;
  3. 通過新的SchedulerFactoryBean獲取scheduler例項,用注入的方式在需要的地方使用。

完整示例

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;

@Component
public class MyJobFactory extends AdaptableJobFactory {

    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        // 呼叫父類的方法
        Object jobInstance = super.createJobInstance(bundle);
        // 進行注入
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}
@Configuration
public class QuartzConfig {

    @Autowired
    private MyJobFactory myJobFactory;

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();

        // 載入quartz資料來源配置
        factory.setQuartzProperties(quartzProperties());

        // 自定義Job Factory,用於Spring注入
        factory.setJobFactory(myJobFactory);

        return factory;
    }

    @Bean
    public Scheduler scheduler() throws IOException, SchedulerException {
        Scheduler scheduler = schedulerFactoryBean().getScheduler();
        scheduler.start();
        return scheduler;
    }
}

3.Spring簡單任務

Spring對Quartz進行了封裝,方便開發者呼叫。下面以Spring Boot為例,介紹一下簡單任務在Spring的執行方式。

任務類定義

仔細觀察可以發現,與普通Java任務的區別在於使用了@Component和@EnableScheduling的註釋,相應的,就不用宣告implements Job,以及重寫execute方法。這是Spring提供的一種便利。

@Component
@EnableScheduling
public class SpringJob {
    @Autowired
    WriteService writeService;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    public void myJobBusinessMethod() {
        this.logger.info("MyFirstExerciseJob哇被觸發了哈哈哈哈哈");
        writeService.writeMSG("張三");
    }
}

配置JobDetail和Trigger的Bean

MethodInvokingJobDetailFactoryBean是Spring提供的JobDetail工廠方法,使用它可以快速地定義JobDetail。然而,缺點是生成的任務無法持久化儲存,也就是說,無法管理任務的啟動、暫停、恢復、停止等操作。
CronTriggerFactoryBean為表示式型觸發器。

@Configuration
public class QuartzJobConfig {

    /**
     * 方法呼叫任務明細工廠Bean
     */
    @Bean(name = "SpringJobBean")
    public MethodInvokingJobDetailFactoryBean myFirstExerciseJobBean(SpringJob springJob) {
        MethodInvokingJobDetailFactoryBean jobDetail = new MethodInvokingJobDetailFactoryBean();
        jobDetail.setConcurrent(false); // 是否併發
        jobDetail.setName("general-springJob"); // 任務的名字
        jobDetail.setGroup("general"); // 任務的分組
        jobDetail.setTargetObject(springJob); // 被執行的物件
        jobDetail.setTargetMethod("myJobBusinessMethod"); // 被執行的方法
        return jobDetail;
    }
    

    /**
     * 表示式觸發器工廠Bean
     */
    @Bean(name = "SpringJobTrigger")
    public CronTriggerFactoryBean myFirstExerciseJobTrigger(@Qualifier("SpringJobBean") MethodInvokingJobDetailFactoryBean springJobBean) {
        CronTriggerFactoryBean tigger = new CronTriggerFactoryBean();
        tigger.setJobDetail(springJobBean.getObject());
        tigger.setCronExpression("0/10 * * * * ?"); // 什麼是否觸發,Spring Scheduler Cron表示式
        tigger.setName("general-springJobTrigger");
        return tigger;
    }
}

排程器

下面將任務和觸發器註冊到排程器

@Configuration
public class QuartzConfig {

    /**
     * 排程器工廠Bean
     */
    @Bean(name = "schedulerFactory")
    public SchedulerFactoryBean schedulerFactory(@Qualifier("SpringJobTrigger") Trigger springJobTrigger) {
        SchedulerFactoryBean bean = new SchedulerFactoryBean();
        // 覆蓋已存在的任務
        bean.setOverwriteExistingJobs(true);
        // 延時啟動定時任務,避免系統未完全啟動卻開始執行定時任務的情況
        bean.setStartupDelay(15);
        // 註冊觸發器
        bean.setTriggers(SpringJobTrigger);
        return bean;
    }
}

完成上述配置後,啟動spring boot就可以出發定時器任務了。而且,仔細觀察上面的程式碼,在執行過程中有WriteService的spring物件注入,而無需我們自己去自定義JobFactory的Spring物件。

4.持久化

任務持久化需要用到資料庫,而初始化資料庫的SQL可以從下載的釋出版的檔案中找到,比如,我在官網的Download頁下載了當前版本的Full Distribution:Quartz 2.2.3 .tar.gz,解壓後在quartz-2.2.3\docs\dbTables能找到初始化指令碼,因我用的是MySQL的Innodb引擎,所以我用此指令碼tables_mysql_innodb.sql

配置

預設情況下,排程器的詳情資訊會被儲存在記憶體,模式為:RAMJobStore ,而且也不需要填寫quartz.properties的配置。然而,如果是持久化的模式,那麼quartz.properties就必須填寫,因為檔案中制定了資訊儲存模式和資料來源資訊。

# 執行緒排程器例項名
org.quartz.scheduler.instanceName = quartzScheduler
# 執行緒池的執行緒數,即最多3個任務同時跑
org.quartz.threadPool.threadCount = 3

# 如何儲存任務和觸發器等資訊
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 驅動代理
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 表字首
org.quartz.jobStore.tablePrefix = qrtz_ 
# 資料來源
org.quartz.jobStore.dataSource = quartzDataSource
# 是否叢集
org.quartz.jobStore.isClustered = false

# 資料來源
# 驅動
org.quartz.dataSource.quartzDataSource.driver = com.mysql.cj.jdbc.Driver
# 連線URL
org.quartz.dataSource.quartzDataSource.URL = jdbc:mysql://localhost:3306/quartz?characterEncoding=utf-8&useSSL=true&&serverTimezone=Asia/Shanghai
# 使用者名稱
org.quartz.dataSource.quartzDataSource.user = root
# 密碼
org.quartz.dataSource.quartzDataSource.password = 123456
# 最大連線數
org.quartz.dataSource.quartzDataSource.maxConnections = 5

其他內容和RAMJobStore模式相同。