Spring Boot 应用系列 6 -- Spring Boot 2 整合Quartz

Posted 光焱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot 应用系列 6 -- Spring Boot 2 整合Quartz相关的知识,希望对你有一定的参考价值。

Quartz是实现定时任务的利器,Quartz主要有四个组成部分,分别是:

1. Job(任务):包含具体的任务逻辑;

2. JobDetail(任务详情):是对Job的一种详情描述;

3. Trigger(触发器):负责管理触发JobDetail的机制;

4. Scheduler(调度器):负责Job的执行。

有两种方式可以实现Spring Boot与Quartz的整合:

一、使用Spring提供的工厂类

spring-context.jar和spring-context-support.jar类库提供了一些org.quartz包的扩展,使用这些扩展并通过注入bean的方式可以实现与Quartz的整合。

1. pom.xml

添加依赖:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

Spring Boot 2.0.4指定的quartz.jar的版本是2.3.0

2. Job

首先定义一个执行器:

import java.util.Date;

import devutility.internal.text.format.DateFormatUtils;

public class Executor {
    public static void execute(String name, long costMillis) {
        Date startDate = new Date();
        Date endDate = new Date(startDate.getTime() + costMillis);

        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(String.format("[%s]: ", DateFormatUtils.format(startDate, "yyyy-MM-dd HH:mm:ss:SSS")));
        stringBuilder.append(String.format("%s executing on %s, ", name, Thread.currentThread().getName()));
        stringBuilder.append(String.format("will finish at %s.", DateFormatUtils.format(endDate, "yyyy-MM-dd HH:mm:ss:SSS")));
        System.out.println(stringBuilder.toString());

        try {
            Thread.sleep(costMillis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

定义Job:

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;

import devutility.test.app.quartz.common.Executor;

@Component
@EnableScheduling
public class SpringJobs {
    public void job1() {
        Executor.execute("Job1", 5000);
    }

    public void job2() {
        Executor.execute("Job2", 6000);
    }
}

@EnableScheduling注解是必须要有的。

3. JobDetail

 1 package devutility.test.app.quartz.config;
 2 
 3 import org.quartz.Trigger;
 4 import org.springframework.beans.factory.annotation.Qualifier;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
 8 import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
 9 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
10 
11 import devutility.test.app.quartz.jobs.SpringJobs;
12 
13 @Configuration
14 public class SpringJobConfiguration {
15     @Bean
16     public MethodInvokingJobDetailFactoryBean jobDetailFactory1(SpringJobs springJobs) {
17         MethodInvokingJobDetailFactoryBean jobDetailFactory = new MethodInvokingJobDetailFactoryBean();
18         jobDetailFactory.setName("Spring-Job1");
19         jobDetailFactory.setGroup("Spring-Group");
20 
21         jobDetailFactory.setTargetObject(springJobs);
22         jobDetailFactory.setTargetMethod("job1");
23 
24         jobDetailFactory.setConcurrent(false);
25         return jobDetailFactory;
26     }
27 
28     @Bean
29     public MethodInvokingJobDetailFactoryBean jobDetailFactory2(SpringJobs springJobs) {
30         MethodInvokingJobDetailFactoryBean jobDetailFactory = new MethodInvokingJobDetailFactoryBean();
31         jobDetailFactory.setName("Spring-Job2");
32         jobDetailFactory.setGroup("Spring-Group");
33 
34         jobDetailFactory.setTargetObject(springJobs);
35         jobDetailFactory.setTargetMethod("job2");
36 
37         jobDetailFactory.setConcurrent(true);
38         return jobDetailFactory;
39     }

MethodInvokingJobDetailFactoryBean是来自spring-context-support.jar的一个工厂类,它实现了FactoryBean<JobDetail>接口,完成了对具体job的封装。

21行指定具体的任务是我们在2中定义的SpringJobs,22行指定我们使用SpringJobs中的job1方法作为定时任务的具体业务实现。

注意24和37行,如果一个任务每隔5秒执行一次,但是每次需要执行10秒,那么它有两种执行方式,串行(执行完上一个再开启下一个)和并行(间隔时间到了就开始并行执行下一个),24和37就分别设置成了并行和串行执行。

4. Trigger

Quartz支持多种trigger,其中配置最为灵活的一种当属CronTrigger,它属于表达式类型的配置方式,有关cron表达式的配置请参考

 1 package devutility.test.app.quartz.config;
 2 
 3 import org.quartz.Trigger;
 4 import org.springframework.beans.factory.annotation.Qualifier;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
 8 import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
 9 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
10 
11 import devutility.test.app.quartz.jobs.SpringJobs;
12 
13 @Configuration
14 public class SpringJobConfiguration {
15     @Bean
16     public CronTriggerFactoryBean cronTriggerFactory1(@Qualifier("jobDetailFactory1") MethodInvokingJobDetailFactoryBean jobDetailFactory1) {
17         CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
18         cronTriggerFactoryBean.setName("cronTriggerFactoryForJobDetailFactory1");
19         cronTriggerFactoryBean.setJobDetail(jobDetailFactory1.getObject());
20         cronTriggerFactoryBean.setCronExpression("0/3 * * * * ?");
21         return cronTriggerFactoryBean;
22     }
23 
24     @Bean
25     public CronTriggerFactoryBean cronTriggerFactory2(@Qualifier("jobDetailFactory2") MethodInvokingJobDetailFactoryBean jobDetailFactory2) {
26         CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
27         cronTriggerFactoryBean.setName("cronTriggerFactoryForJobDetailFactory2");
28         cronTriggerFactoryBean.setJobDetail(jobDetailFactory2.getObject());
29         cronTriggerFactoryBean.setCronExpression("0/4 * * * * ?");
30         return cronTriggerFactoryBean;
31     }

本例使用的Cron表达式非常简单,分别是每隔3秒和每隔4秒执行一次。

5. Scheduler

1     @Bean
2     public SchedulerFactoryBean schedulerFactory1(Trigger cronTriggerFactory1, Trigger cronTriggerFactory2) {
3         SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
4         schedulerFactoryBean.setStartupDelay(2);
5         schedulerFactoryBean.setTriggers(cronTriggerFactory1, cronTriggerFactory2);
6         return schedulerFactoryBean;
7     }

行4指明系统启动之后需要延迟2秒执行;

行5用于注册需要执行的触发器;

SchedulerFactoryBean还有一个叫autoStartup的属性,用于指明任务在系统启动之后是否立即执行,默认是true。

6. 测试

由此可见,Job1是按照我们的预期串行执行的。

Job2则是并行执行的。

二、使用org.quartz原生类和方法

1. pom.xml,只需要添加quartz

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
</dependency>

2. 定义一个Scheduler类型的Bean

package devutility.test.app.quartz.config;

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfiguration {
    @Bean
    public Scheduler scheduler() throws SchedulerException {
        return StdSchedulerFactory.getDefaultScheduler();
    }
}

3. Job

package devutility.test.app.quartz.jobs;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import devutility.test.app.quartz.common.Executor;

@DisallowConcurrentExecution
public class ScheduleJob1 implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        Executor.execute("ScheduleJob1", 5000);

        JobDetail jobDetail = context.getJobDetail();
        System.out.println(String.format("key: %s", jobDetail.getKey()));
    }
}

这种方式每一个Job都需要定义一个类实现org.quartz.Job接口,形式上要比第一种方式更条理。

3. 我们定义一个创建任意Job的公共方法,来实现Job类的定时执行:

 1 package devutility.test.app.quartz.services;
 2 
 3 import org.quartz.CronScheduleBuilder;
 4 import org.quartz.CronTrigger;
 5 import org.quartz.Job;
 6 import org.quartz.JobBuilder;
 7 import org.quartz.JobDetail;
 8 import org.quartz.JobKey;
 9 import org.quartz.Scheduler;
10 import org.quartz.SchedulerException;
11 import org.quartz.TriggerBuilder;
12 import org.quartz.TriggerKey;
13 import org.springframework.beans.factory.annotation.Autowired;
14 import org.springframework.stereotype.Service;
15 
16 import devutility.internal.models.OperationResult;
17 
18 @Service
19 public class JobServiceImpl implements JobService {
20     @Autowired
21     private Scheduler schedulerFactory1;
22 
23     @Autowired
24     private Scheduler scheduler;
25 
26     @Override
27     public void start(String name, String group, String cronExpression, Class<? extends Job> clazz) throws SchedulerException {
28         JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(name, group).build();
29 
30         String triggerName = String.format("trigger_%s", name);
31         CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
32         CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerName, group).withSchedule(scheduleBuilder).build();
33         scheduler.scheduleJob(jobDetail, trigger);
34 
35         if (!scheduler.isStarted()) {
36             scheduler.start();
37         }
38     }

28行创建一个新的JobDetail,并将Job实现类的Class对象赋值给它;

32行创建了一个新的Trigger;

33-37行使用注入进来的scheduler来运行一个定时任务。

三、对Job的CRUD操作

创建和运行Job上面已经讲过了,下面说一下Job的暂停、中断、删除和更新操作。

1. 暂停

scheduler有一个方法public void pauseJob(JobKey jobKey),该方法通过暂停trigger的触发来实现暂停job的功能,调用该方法之后正在运行的job会持续运行到业务逻辑处理完毕,等下一个触发条件满足时不再开启新的job。

    public OperationResult pause(String group, String name) {
        OperationResult result = new OperationResult();
        JobKey jobKey = JobKey.jobKey(name, group);

        try {
            scheduler.pauseJob(jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
            result.setErrorMessage(String.format("Pause Job with name = \\"%s\\" group = \\"%s\\" failed, system error!", name, group));
        }

        return result;
    }

2. 中断

针对需要中断的Job,quartz专门为其定义了一个接口org.quartz.InterruptableJob:

public interface InterruptableJob extends Job {

    /*
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     * 
     * Interface.
     * 
     * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     */

    /**
     * <p>
     * Called by the <code>{@link Scheduler}</code> when a user
     * interrupts the <code>Job</code>.
     * </p>
     * 
     * @throws UnableToInterruptJobException
     *           if there is an exception while interrupting the job.
     */
    void interrupt()
        throws UnableToInterruptJobException;
}

所有需要支持中断的Job都需要实现这个接口:

package devutility.test.app.quartz.jobs;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;

import devutility.test.app.quartz.common.Executor;

@DisallowConcurrentExecution
public class ScheduleJob2 implements InterruptableJob {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        Executor.execute(context, 5000);
    }

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        System.out.println("ScheduleJob2 is interrupting now。。。");
    }
}

然后通过调用Scheduler实例的boolean interrupt(JobKey jobKey)方法,查看StdScheduler的源码,我们发现Scheduler的interrupt方法仅仅是调用了InterruptableJob中的interrupt()方法实现,然后设置了一下自己的interrupted属性就完了,并未做任何其他操作。

所以,如果要实现可以中断的Job,我们需要在InterruptableJob实现类中增加中断的逻辑:

package devutility.test.app.quartz.jobs;

import org.quartz.InterruptableJob;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.UnableToInterruptJobException;

import devutility.test.app.quartz.common.Executor;

public class ScheduleJob3 implements InterruptableJob {
    private boolean interrupted = false;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        Executor.execute("ScheduleJob3 sub-task 1", 1000);

        if (interrupted) {
            return;
        }

        Executor.execute("ScheduleJob3 sub-task 2", 1000);

        if (interrupted) {
            return;
        }

        Executor.execute("ScheduleJob3 sub-task 3", 1000);
    }

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        interrupted = true;
    }
}

3. 删除

    public OperationResult delete(String jobName, String jobGroup) {
        OperationResult result = new OperationResult();
        result.append(String.format("Removing Quartz job: %s", jobName));

        try {
            if (!schedulerFactory1.deleteJob(JobKey.jobKey(jobName, jobGroup))) {
                result.setErrorMessage(String.format("Removing job %s failed!", jobName));
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
            result.setErrorMessage(String.format("Removing job %s failed with error!", jobName));
        }

        return result;
    }

4. 更新

    public OperationResult update(CronTrigger cronTrigger, String cronExpression) {
        OperationResult result = new OperationResult();
        TriggerKey triggerKey = cronTrigger.getKey();
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
        CronTrigger newTrigger = cronTrigger.getTriggerBuilder().withSchedule(scheduleBuilder).build();

        try {
            schedulerFactory1.rescheduleJob(triggerKey, newTrigger);
            result.append(String.format("Update cron trigger %s succeeded!", triggerKey.getName()));
        } catch (SchedulerException e) {
            e.printStackTrace();
            result.setErrorMessage(String.format("Update cron trigger %s failed!", triggerKey.getName()));
        }

        return result;
    }

Demo代码

以上是关于Spring Boot 应用系列 6 -- Spring Boot 2 整合Quartz的主要内容,如果未能解决你的问题,请参考以下文章

源码分析:通过Spring Boot构建一个购物车微服务 | 云原生应用开发系列6

Spring Boot 集成MyBatis

Spring Boot --- 认识Spring Boot

Spring boot快速入门

三分钟搭建Spring Boot项目框架以及框架的应用

三分钟搭建Spring Boot项目框架以及框架的应用