f7b616a353ead89b7809785c54b408fa.png

1)Quartz是什么

Quartz是一款Java编写的开源任务调度框架,同时它也是Spring默认的任务调度框架。它的作用其实类似于Java中的Timer定时器以及JUC中的ScheduledExecutorService调度线程池,当然Quartz作为一个独立的任务调度框架无疑在这方面表现的更为出色,功能更强大,能够定义更为复杂的执行规则。Quartz中主要用到了:Builder建造者模式、Factory工厂模式以及组件模式,我们要知道Quartz是如何调度的,需要知道三个概念:任务(Job,我们需要将具体的业务逻辑写到实现了Job接口的实现类中)、触发器(Trigger,它定义了任务的执行规则),最后是调度器(Scheduler,通过传入的任务Job和触发器Trigger,以指定的规则执行任务)。

要想使用Quartz,我们先来创建一个简单的maven项目,同时有必要了解Quartz中的一些基本的(概念)接口和类。

2)创建一个简单的Maven样例项目

为了方便验证代码,建议创建一个简单的maven管理的普通Java项目(只填写必要信息,不需要勾选项目骨架)。

这里我们先勾选最主要的Quartz依赖与maven编译插件:

<dependencies>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <target>1.8</target>
                    <source>1.8</source>
                </configuration>
            </plugin>
        </plugins>
    </build>

Quartz使用的是slf4j日志门面,但是还没有具体的实现;为了更直观的看到任务的执行时间,这里导入Logback的日志实现,在pom文件中新增依赖:

<dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
</dependency>

默认的日志格式不是很好,我们在maven项目中的resources目录下直接放置一个名为logback.xml的文件,这里只要配置控制台的输出即可:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false" >

    <!-- 日志级别 -->
    <property name="logLevel" value="INFO"/>

    <!-- 异步缓冲队列的深度,该值会影响性能.默认值为256 -->
    <property name="queueSize" value="512" />

    <!-- LOGGER  PATTERN  配置化输出格式 -->
    <property name="logPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger - %msg%n"/>

    <!-- 控制台打印日志的相关配置 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志格式 -->
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${logPattern}</pattern>
        </encoder>
    </appender>

    <root level="${logLevel}">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

下面是对Quartz使用中的几个重要接口和类的说明与使用,它们分别是:JobJobExecutionContextJobDetailJobBuilderTriggerTriggerBuilderJobDataMap以及Scheduler

3)Job任务接口(具体任务的执行入口)

Job接口很简单,只有一个execute方法,这是我们自己的具体业务逻辑的入口,类似TimerTaskrun方法:

public interface Job {
    void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException;
}

要创建一个任务我们需要编写一个实现该接口的具体任务类:

public class QuartzJob implements Job {
    public void execute(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        // 任务逻辑代码
    }
}

但是Job并不能直接被调度器使用,它需要通过JobDetail绑定后传递给Scheduler

要特别注意的是:在任务类中我们必须要有一个无参构造器,因为Quartz是通过反射机制实例化我们的任务类的;一个Job可以对应多个Trigger,每当调度器要执行Jobexecute方法前,会创建一个Job实例,而当调用完成后,该实例会被垃圾收集器回收。

4)JobDetail任务接口

见名知义,它承载了Job对象的详细信息,同时保存(绑定)Job对象,因为Job接口只有一个execute方法,方法中只能有我们具体的业务逻辑方法,而我们一个进程中可能存在多个Job对象实例,所以我们有必要给它命名和分组。所以就有了JobDetail,我们实际传递给调度器的也是JobDetail而不是Job对象。

JobDetail描述了Job对象的基本信息,主要包含四个重要的属性:name(Job的名称)、group(Job的组名称)、jobClass(Job对应的类)以及jobDataMap(存储一些用户自定义的信息或对象)。在SchedulerJob的名称name和组group组合必须是唯一的。

这是JobDetail接口的方法:

32a0d00c8c08e0eb2d9ab182d0b7804b.png

其中对应的属性设置可通过JobBuilder方法设置,下面对几个比较重要的属性进行说明:

  • java public boolean isConcurrentExectionDisallowed();

是否禁止并发运行):如果该属性为true,表示任务不是并发运行的,可通过在Job实现类上标注@DisallowConcurrentExecution注解设置。

  • java public boolean isDurable();

任务是否是可持续的):如果一个任务不是可持续的,则当没有触发器关联它的时候,Quartz会从scheduler中删除它。

  • java public boolean isPersistJobDataAfterExecution();

是否在任务执行后持久化Job数据):true持久化,false不做操作;可通过在Job实现类上标注@PersistJobDataAfterExecution注解设置。

  • java public boolean requestsRecovery();

是否能请求恢复):如果一个任务请求恢复,一般是该任务执行期间发生了系统崩溃或者其他关闭进程的操作,当服务再次启动的时候,会再次执行该任务。这种情况下,JobExecutionContext.isRecovering()会返回true

调度器需要借助JobDetail对象来添加Job实例,Job接口的实例要通过JobBuilder类构建(Builder建造者模式):

public class QuartzScheduler {
    public static void main(String[] args) throws SchedulerException {
        // 构建一个JobDetail实例,通过newJob方法绑定QuartzJob类,之后指定Job的名称和组名,但这不是必须的
        JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                .withIdentity("job1", "group1").build();
    }
}

5)JobExecutionContext任务上下文接口

可以看到在上面Job接口的execute方法中有一个参数JobExecutionContext,这其实也是一个接口;当sheduler调用一个Job时,就会将JobExecutionContext传递给Jobexecute方法,Job对象里能通过JobExecutionContext对象访问到Quartz运行时的环境和Job本身的一些信息如SchedulerTriggerCalendar以及JobDetail等等一些相关的对象信息,还有一个很重要的类型JobDataMap,它可以以Map(键值对)的形式传递我们的一些需要的信息(下面再细说)。除此之外JobExecutionContext还有很多其他的一些获取信息的方法,这里不再细讲,具体查看接口的源码即可。

6)Trigger触发器(规则)接口

如果说JobDetail(或Job)代表的是具体的任务,那么Trigger就是这些任务的执行规则。比如你需要任务什么时候开始执行,以及执行的频度和次数,都是通过这个接口的实例来描述的。多个触发器可以指向同一个任务,但一个触发器只能指向一个任务

Trigger接口主要描述了一个任务(Job)的调度优先级、开始时间、结束时间、Calendar的名称等,此外里面还有两个比较重要的类型:JobKeyTriggerKey,这两个类都是org.quartz.utils.Key的子类型,Key类型中仅有的两个字段是namegroup。毫无疑问,JobKeyTrigger分别描述的是JobTrigger的名称和组。

Quartz中的Trigger的具体实现类如下,其中最常用的还是CronTriggerImplSimpleTriggerImpl

292f597d06474d045f1acec8c3356257.png

SimpleTriggerImpl主要是一些简单的执行规则,而CronTriggerImpl则更为灵活强大,能够胜任更为复杂的规则。

同样,跟JobDetail类似,Trigger接口的实例要通过TriggerBuilder来构造:

public class QuartzScheduler {
    public static void main(String[] args) throws SchedulerException {
        // 构建一个JobDetail实例...

        // 构建一个Trigger,指定Trigger名称和组,规定该Job立即执行,且两秒钟重复执行一次
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                .startNow() // 执行的时机,立即执行
            .withIdentity("trigger1", "group1") // 不是必须的
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(2).repeatForever()).build();  // build之后返回的类型是SimpleTrigger,具体的类型是SimpleTriggerImpl
    }
}

上面最重要的是withSchedule这个方法,它制定了任务的执行规则,这里使用到了SimpleScheduleBuilder来构建了一个简单的任务调度规则,最终导致build之后的Trigger实例属于SimpleTrigger类型。

我们有必要看看SimpleScheduleBuilder这个类,通过继承结构可以看到它属于ScheduleBuilder这个类的子类,查看ScheduleBuilder的继承树:

a5d92219c3919e9f35b80c6487f6b63c.png

看到CronScheduleBuilder是不是有一种熟悉的味道?上面我说了Trigger只是一个接口,而CronTriggerImpl是很常用的一个具体类型,而CronScheduleBuilder就是用来构建这种规则的,在withSchedule方法中使用CronScheduleBuilder,那么调用build方法之后返回的将是CronTrigger(注意这只是接口类型,对应具体类型是CronTriggerImpl)。

Trigger这里用了较多篇幅,这无可厚非,因为这是任务调度中最核心的,使用任务调度框架就是因为它能根据规则执行我们的代码。

7)JobDataMap数据存储类

通过查看JobDetailTriggerJobExecutionContext的源码可以发现,他们中都存在JobDataMap这个类型,它是Map的形式存储我们的一些自定义数据的。当Job对象的execute方法被调用时,JobDataMap会通过JobExecutionContext传递给execute方法,它可以用来装载任何可序列化的数据对象。JobDataMap实现了Java中的Map接口,提供了一些自己的方法来存储数据。

这是JobDataMap的继承树:

92049bc38ae98458dfa91b5eebd4bcb4.png

可以看到JobDataMapDirtyFlagMap的子类,而DirtyFlagMap实际实现了Java中的java.util.Map类型:

// DirtyFlagMap是java.util.Map接口的子类
public class DirtyFlagMap<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { }

一句话:把它当Java中的map来用就对了!

8)Scheduler任务调度器

任务和调度规则都有了,那么谁来调用他们?这就是任务调度器(Scheduler)的工作了。

任务调度器(Scheduler)也是一个接口,它定义了调度任务中的基本操作骨架,既然是接口那就会有具体实现,这是Scheduler的实现结构:

e64dee8780f2d456e31e7a6ec99c758b.png

那如何获取Scheduler的实例?可以通过SchedulerFactory这个调度器构建工厂的接口子工厂实例来完成,它有两个具体的工厂实现:

6964a9c42d25d93acc5acbc7fcd583dc.png

一般我们使用的是StdSchedulerFactory工厂来获取Scheduler实例,因为它可以使用配置文件方式配置参数,而DirectSchedulerFactory则是使用硬编码(通过调用方法)的方式来设置参数;下面下面先说StdSchedulerFactory工厂获取Scheduler实例的方法(`DirectSchedulerFactory在后篇补充):

// StdSchedulerFactory
Scheduler scheduler = new StdSchedulerFactory().getScheduler();

这里返回的是一个Scheduler类型,StdSchedulerScheduler的子类型。既然所有的组件都集齐了,我们就可以实际来使用Quartz完成自定义的任务了,

首先创建一个Quartz任务,任务中从JobExecutionContext中获取到了JobDetailTrigger中的JobDataMap,并从中取到了客户端QuartzScheduler中传入的数据:

public class QuartzJob implements Job {
    public void execute(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        // 获取JobDetail中的JobDataMap
        JobDataMap jobDetailDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        // 获取Trigger中的JobDataMap
        JobDataMap triggerDataMap = jobExecutionContext.getTrigger().getJobDataMap();
        log.info(jobDetailDataMap.get("message")); 
        log.info(triggerDataMap.get("number"));
    }
}

创建Quartz客户端,构建JobDetailTrigger并使用Scheduler开始任务调度(这里要注意的是Scheduler实例创建后处于“待机”状态,所以别忘了调用start方法启动调度器,否则任务是不会执行的!):

public class QuartzScheduler {
    public static void main(String[] args) throws SchedulerException {
        // 创建一个JobDetail实例
        JobDetail jobDetail = JobBuilder.newJob(QuartzJob.class)
                // 指定JobDetail的名称和组名称
                .withIdentity("job1", "group1")
                // 使用JobDataMap存储用户数据
                .usingJobData("message", "JobDetail传递的文本数据").build();

        // 创建一个SimpleTrigger,规定该Job立即执行,且两秒钟重复执行一次
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                // 设置立即执行,并指定Trigger名称和组名称
                .startNow().withIdentity("trigger1", "group1")
                // 使用JobDataMap存储用户数据
                .usingJobData("number", 128)
                // 设置运行规则,每隔两秒执行一次,一直重复下去
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(2).repeatForever()).build();

        // 得到Scheduler调度器实例
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.scheduleJob(jobDetail, trigger); // 绑定JobDetail和Trigger
        scheduler.start();                         // 开始任务调度
    }
}

可以看到程序正确的执行了,也读取到了JobDetailTrigger中传递的数据并打印到了控制台:

386ae6a45a42d40b244c66cae56a06d3.png

不知你是否觉得在QuartzJob类中的获取数据的方法有点繁琐?既要拿到JobDetail中的JobDataMap也要获取Trigger的,有没有简单点的方法呢?其实也很简单:

@Slf4j
public class QuartzJob implements Job {
    public void execute(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        // 获取JobDetail与Trigger合并后的JobDataMap
        JobDataMap mergedJobDataMap = jobExecutionContext.getMergedJobDataMap();
       log.info(mergedJobDataMap.get("message"));
        log.info(mergedJobDataMap.get("number"));
    }
}

执行结果和前面完全一致,但代码是不是清爽了很多?JobDetailTrigger中的数据都被合并到一个JobDataMap中了,简洁是简洁了,但注意不要出现数据键重名的情况,否则会发生数据覆盖!

9)结语

本文只介绍了Quartz的简单使用方法,即便如此也用了很大的篇幅,更多内容将在下篇讲述,移步《入门Java开源任务调度框架-Quartz(后篇)》了解更多!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐