springboot应用系列6--springboot2整合quartz

光焱      2022-04-12     702

关键词:

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代码

springboot系列springboot初级应用及配置文件(代码片段)

二、SpringBoot初级应用2.1SpringBoot项目目录结构src/main/java:文件目录(启动类和java代码编写)src/main/resource:存放静态资源文件的目录、配置文件、mybatis映射文件、属性文件等;src/test/java:测试代码目录... 查看详情

springboot应用系列6--springboot2整合quartz

...bsp;Scheduler(调度器):负责Job的执行。有两种方式可以实现SpringBoot与Quartz的整合:一、使用Spring 查看详情

docker系列-第七篇docker构建springboot应用

1.基于Dockerfile构建SpringBoot镜像1.1准备工作将SpringBoot项目通过maven打成jar包mvncleanpackage#使用maven打包项目1.2使用Dockerfile构建镜像step1在存放jar所在目录下创建Dockerfile文件touchDockerfilestep2编辑Dockerfile增加以下内容FROMjava:8MAINTAINERniuga 查看详情

docker系列-第七篇docker构建springboot应用

1.基于Dockerfile构建SpringBoot镜像1.1准备工作将SpringBoot项目通过maven打成jar包mvncleanpackage#使用maven打包项目1.2使用Dockerfile构建镜像step1在存放jar所在目录下创建Dockerfile文件touchDockerfilestep2编辑Dockerfile增加以下内容FROMjava:8MAINTAINERniuga 查看详情

springboot应用系列5--springboot2整合logback

上一篇我们梳理了SpringBoot2整合log4j2的配置过程,其中讲到了SpringBoot2原装适配logback,并且在非异步环境下logback和log4j2的性能差别不大,所以对于那些日志量不算太高的项目来说,选择logback更简单方便。1.pom.xmlpom.xml不需要添加... 查看详情

补习系列-springboot-restful应用

一、目标了解Restful是什么,基本概念及风格;能使用SpringBoot实现一套基础的Restful风格接口;利用Swagger生成清晰的接口文档。二、Restful入门什么是REST摘自百科的定义:REST即表述性状态转移(英文:RepresentationalStateTransfer,简称RE... 查看详情

springboot系列

https://my.oschina.net/xiedeshou?tab=newest&catalogId=5936801SpringBoot|第零章:前言SpringBoot|第一章:第一个SpringBoot应用SpringBoot|第二章:lombok介绍及简单使用SpringBoot|第三章:springboot配置详解SpringBoot|第四章:日志配置SpringBoot|第 查看详情

springboot系列springboot介绍和基础pom文件

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Boot致力于在蓬勃发展的快... 查看详情

springboot系列springboot入门(代码片段)

一、SpringBoot入门1.1SpringBoot简介SpringBoot是在Sping(Spring4.0版本)基础上产生的,其中“Boot”的意思就是“引导”,意在简化开发模式,使开发者快速的开发出基于Spring的应用。SpringBoot含有一个内嵌的TomcatWeb服... 查看详情

springboot应用系列4--springboot2整合log4j2

一、背景1.log4j2传承于log4j和logback,它是目前性能最好的日志处理工具,有关它们的性能对比请看:2.除了性能好之外,log4j2有这么几个重要的新features:(1)自动热重载配置文件,而且重新加载期间不会丢失日志请求。logback也可以... 查看详情

springboot系列——配置相关

〇、SpringBoot的全局配置文件  通过上一篇我们可以知道,构建一个SpringBoot项目时,SpringBoot已经对项目进行了默认配置。但在实际应用中,每个项目有其特殊性,不可能仅使用默认的配置,还需自行添加或修改一些配置。  ... 查看详情

重学springboot系列之嵌入式容器的配置与应用(代码片段)

重学SpringBoot系列之嵌入式容器的配置与应用嵌入式容器的运行参数配置调整SpringBoot应用容器的参数两种配置方法配置文件方式常用配置参数tomcat性能优化核心参数自定义配置类方式为Web容器配置HTTPS如何生成自签名证书将SSL应... 查看详情

springboot系列springboot日志框架

注意:本SpringBoot系列文章基于SpringBoot版本v2.1.1.RELEASE进行学习分析,版本不同可能会有细微差别。前言Spring框架选择使用了JCL作为默认日志输出。而SpringBoot默认选择了SLF4J结合LogBack。那我们在项目中该使用哪种日志框架呢?在... 查看详情

springboot学习系列

springboot开发第一个应用程序1、springboot是什么?2、springboot容易上手吗?写这篇文章技术文章,主要是记录日常的学习以及理解。我们重新认识一下spring假设你受命使用spring开发一个简单的helloword的web程序。你该做什么?我能想... 查看详情

springboot系列:springboot使用actuator

Actuator为springboot提供了运行状态监控的功能通过集成它我们可以试试获取到应用程序的运行信息首先,在pom.xml中引入起步依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</arti 查看详情

springboot系列——快速构建项目

  前言  springboot官方参考指南:https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/  SpringBoot是由spring家族提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用“约定大于配... 查看详情

springboot系列springboot整合持久层(代码片段)

这里写目录标题四、SpringBoot整合持久层4.1SpringBoot项目工程搭建4.2MyBatis逆向工程生成pojo和mapper文件1先建立包和文件夹,用于存储配置文件和逆向生成pojo和mapper文件2先引入pom依赖文件4.3应用实现增删改查四、SpringBoot整合持... 查看详情

springboot系列教程web篇之404500异常页面配置(代码片段)

...403无权,500服务器异常时,我们可以如何处理原文友链:SpringBoot系列教程web篇之404、500异常页面配置I.环境搭建首先得搭建一个web应用才有可能继续后续的测试,借助SpringBoot搭建一个web应用属于比较简单的活;创建一个maven项目,p... 查看详情