21.1 概述

定时任务往往是实际项目中的刚需。

  • 我们想监控一个重点服务的运行状态,可以每隔 1 分钟调用下该服务的心跳接口,调用失败时即发出告警信息;
  • 我们想每天凌晨的时候,将所有商品的库存置满,以免早上忘记添加库存影响销售;
  • 我们想在每个周六的某个时段进行打折促销。

在以上的案例中,或者是指定时间间隔,或者是指定时间节点,按设定的任务进行某种操作,这就是定时任务了。

在 Spring 中实现定时任务简单而灵活,通常只需要应用 @Scheduling 注解即可满足大部分的定时任务场景需求。更复杂或高性能的要求可以选择使用Quartz 框架。

21.2 使用注解设置定时功能

21.2.1 开启定时任务

在启动类上添加 @EnableScheduling 注解,开启定时任务功能。

  1. @EnableScheduling
  2. public class CloudApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(CloudApplication.class, args);
  5. }
  6. }

21.2.2 设置执行周期

新建 TaskScheduling 任务类,添加@Component注解注册 Spring 组件,定时任务方法需要在 Spring 组件类才能生效。

注意类中方法添加了 @Scheduled 注解的fixedRate参数,上它指定在上一次开始执行时间点之后多长时间再执行(也就是任务的执行周期)。如果上一个任务没有执行完毕,则等待该任务执行完成后再立即执行下一个任务。

  1. package com.longser.union.cloud.service.schedule;
  2. import org.springframework.scheduling.annotation.Scheduled;
  3. import org.springframework.stereotype.Component;
  4. import java.util.Date;
  5. @Component
  6. public class TaskScheduling {
  7. @Scheduled(fixedRate = 2000)
  8. public void fixedRateMethod() throws InterruptedException {
  9. System.out.println("fixedRateMethod:" + new Date());
  10. Thread.sleep(1000);
  11. }
  12. }

上面例子执行情况如下,可见是每隔 2 秒执行 1 次方法,和方法内部消耗的时间无关。

  1. fixedRateMethod at 14:43:09.545337
  2. fixedRateMethod at 14:43:11.545777
  3. fixedRateMethod at 14:43:13.544361
  4. fixedRateMethod at 14:43:15.545803
  5. fixedRateMethod at 14:43:17.546549
  6. fixedRateMethod at 14:43:19.545171

还可以使用 fixedRateString 参数来代替 fixedRate。他们两者含义相同,但fixedRateString 支持从配置文件中读取具体的延迟数值

  1. @Scheduled(fixedRateString = "${time.fixedDelay}")
  2. public void fixedRateMethod() throws InterruptedException {
  3. System.out.println("fixedRateMethod:" + new Date());
  4. Thread.sleep(1000);
  5. }

21.2.3 设置执行延迟

接下来我们把@Scheduled注解的 fixedRate 参数换成 fixedDelay,它指示在上一个任务执行完成之后过多长时间再次执行。

  1. @Scheduled(fixedDelay = 2000)
  2. public void fixedDelayMethod() throws InterruptedException {
  3. System.out.println("fixedDelayMethod:" + new Date());
  4. Thread.sleep(1000);
  5. }

修改以后是方法执行结束 2 秒后再次执行任务,由于方法内部等待了 1 秒,所以是每 3 秒打印 1 行内容。

  1. fixedRateMethod at 18:11:06.506638
  2. fixedRateMethod at 18:11:09.516006
  3. fixedRateMethod at 18:11:12.520294
  4. fixedRateMethod at 18:11:15.524249
  5. fixedRateMethod at 18:11:18.532481
  6. fixedRateMethod at 18:11:21.537771

还可以使用 fixedDelayString 参数来代替 fixedDelay。他们两者含义相同,但fixedDelayString 支持从配置文件中读取具体的延迟数值

  1. @Scheduled(fixedDelayString = "${time.fixedDelay}")
  2. public void fixedDelayMethod() throws InterruptedException {
  3. System.out.println("fixedDelayMethod:" + new Date());
  4. Thread.sleep(1000);
  5. }

21.2.4 设置启动延迟

使用 initialDelay 参数可以指定第一次延迟多长时间后再执行:

  1. @Scheduled(initialDelay=1000, fixedRate=5000)
  2. //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次

同样的,可以使用 initialDelayString 参数代替 initialDelay

21.2.5 使用 Cron 表达式

@Scheduled也支持使用 Cron 表达式, Cron 表达式可以非常灵活地设置定时任务的执行时间。以本节开头的两个需求为例:

  • 监控一个服务的运行状态,每隔 1 分钟调用下该服务的心跳接口,调用失败时即发出告警信息;
  • 在每天凌晨的时候,将所有商品的库存置满,以免早上忘记添加库存影响销售。

对应的定时任务实现如下:

  1. @Scheduled(cron = "0 * * * * ?")
  2. public void jump() {
  3. System.out.println("心跳检测:" + LocalDateTime.now());
  4. }
  5. /**
  6. * 在每天的00:00:00执行
  7. */
  8. @Scheduled(cron = "0 0 0 * * ?")
  9. public void stock() {
  10. System.out.println("置满库存:" + LocalDateTime.now());
  11. }

Cron 表达式并不难理解,从左到右一共 6 个位置,分别代表秒、时、分、日、月、星期,以秒为例:

  • 如果该位置上是 0,表示在第 0 秒执行;
  • 如果该位置上是 *,表示每秒都会执行;
  • 如果该位置上是 ?,表示该位置的取值不影响定时任务,由于月份中的日和星期可能会发生意义冲突,所以日、 星期中需要有一个配置为?。

按照上面的理解,cron = “0 ?” 表示在每分钟的 00 秒执行、cron = “0 0 0 ?” 表示在每天的 0点执行。

21.3 Quartz基础

21.3.1 关于Quartz

Spring 的定时任务已经可以满足绝大多数项目的需求,但是在企业级应用这个领域,还有更加强大灵活的 Quartz 框架可供选择。

下面 Quartz 官网 的说明:

What is the Quartz Job Scheduling Library?

Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.

Quartz is freely usable, licensed under the Apache 2.0 license

What Can Quartz Do For You?

If your application has tasks that need to occur at given moments in time, or if your system has recurring maintenance jobs then Quartz may be your ideal solution. Sample uses of job scheduling with Quartz:

  • Driving Process Workflow: As a new order is initially placed, schedule a Job to fire in exactly 2 hours, that will check the status of that order, and trigger a warning notification if an order confirmation message has not yet been received for the order, as well as changing the order’s status to ‘awaiting intervention’.
  • System Maintenance: Schedule a job to dump the contents of a database into an XML file every business day (all weekdays except holidays) at 11:30 PM.
  • Providing reminder services within an application.

21.3.2 核心概念

Quartz 定义了如下核心概念

  • 任务(Job):是想要实现的执行的工作内容的任务本身,每一个 Job 必须实现 org.quartz.Job接口,且只需实现接口定义的execute()方法。
  • 任务细节(JobDetail):JobDetail是用来描述Job实现类以及相关静态信息,比如任务在Scheduler中的组名、标记名、应用数据等信息
  • 触发器(Trigger):是一个任务的定时触发器,定义触发Job执行的时间触发规则。Quartz提 供Trigger类及其子类支持触发器功能。
  • 调度器(Scheduler):是任务的调度器,Quartz提供了Scheduler接口将任务Job及触发器Trigger整合起来,负责基于Trigger设定的时间来执行Job。

image.png

21.3.3 项目依赖

在Spring Boot需要引入 Quartz 框架相关依赖。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-quartz</artifactId>
  4. <version>2.5.6</version>
  5. </dependency>

21.3.4 开启定时任务

同样需要在启动类上添加 @EnableScheduling 注解,开启定时任务功能。

  1. @EnableScheduling
  2. public class CloudApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(CloudApplication.class, args);
  5. }
  6. }

21.3.5 Job任务组件

我们先开发一个 Job 组件,后面各种定时设置都会执行这个任务:

  1. package com.longser.union.cloud.service.schedule;
  2. import org.quartz.Job;
  3. import org.quartz.JobExecutionContext;
  4. import org.quartz.JobExecutionException;
  5. import org.springframework.stereotype.Component;
  6. import java.util.Date;
  7. @Component
  8. public class DiscountJob implements Job {
  9. @Override
  10. public void execute(JobExecutionContext jobExecutionContext) {
  11. String jobDetailStyle = jobExecutionContext.getJobDetail().getJobDataMap().getString("JobDetailStyle");
  12. String triggerType = jobExecutionContext.getTrigger().getJobDataMap().getString("TriggerType");
  13. System.out.println(new Date());
  14. System.out.println("Current Job Detail Style: " + jobDetailStyle);
  15. System.out.println("Current Trigger Type: " + triggerType);
  16. System.out.println("更新数据库中商品价格,统一打5折\n");
  17. }
  18. }

21.4 让Quartz 自动调度任务

我们可以让应用程序根据设置的时间规则自动调度执行定时任务。为此,我们需要在配置类中设定JobDetail 及 Trigger。

21.4.1 简单的链式调用方法

Quartz的开发也支持之前教程中Thumbnailator那种简单的链式调用方法。

  1. package com.longser.union.cloud.service.schedule;
  2. import org.quartz.*;
  3. import org.quartz.JobDetail;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import java.util.Date;
  7. @Configuration
  8. public class QuartzConfig {
  9. @Bean
  10. public JobDetail jobDetailBuilderStyle() {
  11. return JobBuilder.newJob().ofType(DiscountJob.class)
  12. // 即使没有Trigger关联时,也不需要删除该JobDetail
  13. .storeDurably()
  14. .usingJobData("JobDetailStyle","Builder Style")
  15. .withIdentity("Qrtz_Job_Detail")
  16. .withDescription("Invoke Sample Job service as builder-style ...")
  17. .build();
  18. }
  19. @Bean
  20. public Trigger simpleTrigger(JobDetail jobDetail) {
  21. ScheduleBuilder<SimpleTrigger> scheduleBuilder = SimpleScheduleBuilder
  22. .simpleSchedule()
  23. .withIntervalInSeconds(5)
  24. .repeatForever();
  25. return TriggerBuilder.newTrigger()
  26. .forJob(jobDetail)
  27. .withIdentity("quartzTaskService")
  28. .usingJobData("TriggerType","Simple Trigger")
  29. .withSchedule(scheduleBuilder)
  30. .startNow()
  31. .endAt(new Date(System.currentTimeMillis()+20*1000))
  32. .build();
  33. }
  34. }

注意:在上面的代码中 simpleTrigger() 的参数是自动注入的。此时当前类只能有一个使用了@Bean注解的返回数据类型为JobDetail或其子类的方法。

在上面的代码中设置每隔5秒中执行一次任务,20秒后停止:

  1. Fri Nov 12 21:43:27 CST 2021
  2. Current Job Detail Style: Builder Style
  3. Current Trigger Type: Simple Trigger
  4. 更新数据库中商品价格,统一打5
  5. Fri Nov 12 21:43:31 CST 2021
  6. Current Job Detail Style: Builder Style
  7. Current Trigger Type: Simple Trigger
  8. 更新数据库中商品价格,统一打5
  9. Fri Nov 12 21:43:36 CST 2021
  10. Current Job Detail Style: Builder Style
  11. Current Trigger Type: Simple Trigger
  12. 更新数据库中商品价格,统一打5
  13. Fri Nov 12 21:43:41 CST 2021
  14. Current Job Detail Style: Builder Style
  15. Current Trigger Type: Simple Trigger
  16. 更新数据库中商品价格,统一打5

ScheduleBuilder有很多方便的定义时间间隔的方法,比如我们可以用 withIntervalInHours(1) 类定义每1小时执行一次。

同样的,我们也可以用Cron表达式定义定时任务,把 simpleTrigger() 方法修改如下:

  1. @Bean
  2. public Trigger simpleTrigger(JobDetail jobDetail) {
  3. CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
  4. .cronSchedule("0/5 20 22 * * ?");
  5. return TriggerBuilder.newTrigger()
  6. .forJob(jobDetail)
  7. .withIdentity("quartzTaskService")
  8. .usingJobData("TriggerType","Simple Trigger with Cron Schedule")
  9. .withSchedule(cronScheduleBuilder)
  10. .build();
  11. }

上面代码的设置是在22:20 的时候,每 5 秒钟执行一次。

21.4.2 其它定义jobDetail的方法

除了前一节中给出的简单链式调用方法,Quartz还支持另外两种定义jobDetail的方法(当然这并不是什么好事儿,很多开源软件都这样,“茴香豆的‘茴’字有多少种写法。

  • JobDetailFactoryBean 方法

删除 jobDetailBuilderStyle() 后添加如下的方法:

  1. @Bean
  2. public JobDetailFactoryBean jobDetailBeanStyle() {
  3. JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
  4. jobDetailFactory.setJobClass(DiscountJob.class);
  5. jobDetailFactory.setDescription("Invoke Sample Job service as bean-style ...");
  6. jobDetailFactory.setDurability(true);
  7. JobDataMap jobDataMap = new JobDataMap();
  8. jobDataMap.put("JobDetailStyle","Bean Style");
  9. jobDetailFactory.setJobDataMap(jobDataMap);
  10. return jobDetailFactory;
  11. }
  • MethodInvokingJobDetailFactoryBean 方法

或者删除 jobDetailBuilderStyle() 后添加如下的方法:

  1. @Bean
  2. MethodInvokingJobDetailFactoryBean jobFactoryBean() {
  3. MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
  4. bean.setTargetBeanName("discountJob");
  5. bean.setTargetMethod("execute");
  6. return bean;
  7. }

由于此种方法需要任务类中定义一个无参数的方法,我们需要给DiscountJob类增加一个方法

  1. public void execute() {
  2. System.out.println(new Date());
  3. System.out.println("更新数据库中商品价格,统一打5折\n");
  4. }

另外,MethodInvokingJobDetailFactoryBean的方法对任务类没有父类的要求。

21.4.3 不同的Trigger方法形态

由于前文我们定义的Trigger方法使用了自动注入的参数,所以定义jobDetail的不同方法不能同时存在于一个类中。我们要么每个类只放一种方法,要么就需要去掉Trigger方法的参数,在显式地执行jobDetail方法并从结果中获得JobDetail对象后将其作为参数传递给 forJob():

  1. @Bean
  2. public Trigger simpleTrigger() {
  3. ScheduleBuilder scheduleBuilder = SimpleScheduleBuilder
  4. .simpleSchedule()
  5. .withIntervalInSeconds(5)
  6. .withRepeatCount(5);
  7. return TriggerBuilder.newTrigger()
  8. .forJob( jobDetailBeanStyle().getObject() )
  9. .withIdentity("quartzTaskService")
  10. .usingJobData("TriggerType","Simple Trigger with Cron Schedule")
  11. .withSchedule(scheduleBuilder)
  12. .build();
  13. }

更烦更乱的是,我们其实还有更多的编写Trigger的方法(显然这些方法不可共存)

  • SimpleTriggerFactoryBean

    1. @Bean
    2. SimpleTriggerFactoryBean simpleTriggerBean() {
    3. SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
    4. bean.setRepeatInterval(5000);
    5. bean.setRepeatCount(5);
    6. bean.setJobDetail( jobDetailBeanStyle().getObject() );
    7. JobDataMap jobDataMap = new JobDataMap();
    8. jobDataMap.put("TriggerType","Simple Trigger Factory Bean");
    9. bean.setJobDataMap(jobDataMap);
    10. return bean;
    11. }
  • CronTriggerFactoryBean

      @Bean
      CronTriggerFactoryBean cronTrigger() {
          CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
          // Corn表达式设定执行时间规则
          bean.setCronExpression("0 39 18 ? * 4");
    
          // 执行JobDetail
          bean.setJobDetail( jobDetailBeanStyle().getObject() );
    
          JobDataMap jobDataMap = new JobDataMap();
          jobDataMap.put("TriggerType","Cron Trigger Factory Bean");
          bean.setJobDataMap(jobDataMap);
    
          return bean;
      }
    
  • CronTriggerFactoryBean

      @Bean
      CronTriggerFactoryBean cronTrigger() {
          CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
          // Corn表达式设定执行时间规则
          bean.setCronExpression("0 41 18 ? * 4");
          // 执行JobDetail
          bean.setJobDetail(jobDetailBuilderStyle());
          return bean;
      }
    

好了,谁帮我用重庆话说一句“好烦呦

21.5 编程控制Quartz任务

在前文示例创建Quartz的JobDetail时,我们用withIdentity()方法定义过任务的 Identity ,但没有解释它的作用。本节我们展示一下如何应用它来控制Quartz任务。

21.5.1 创建Quartz任务

在创建了JobDetail和Trigger对象以后,我们需要调用如下方法创建Qzartz任务

Scheduler.scheduleJob(org.quartz.JobDetail jobDetail,
                      org.quartz.Trigger trigger)

示例代码如下

package com.longser.union.cloud.controller;

import com.longser.union.cloud.service.schedule.DiscountJob;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@RestController
@RequestMapping("/api")
public class QuzrtzJobController {

    private final Scheduler scheduler;

    public QuzrtzJobController(@Autowired Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    @PostMapping("/job/create")
    public String quartz(@RequestParam String name) throws Exception {
        Date start = new Date(System.currentTimeMillis() + 7 * 1000);

        /* 通过JobBuilder.newJob()方法获取到当前Job的具体实现(以下均为链式调用)
         * 这里是固定Job创建,所以代码写死XXX.class
         * 如果是动态的,根据不同的类来创建Job,则 ((Job)Class.forName("com.zy.job.TestJob").newInstance()).getClass()
         * 即是 JobBuilder.newJob(((Job)Class.forName("com.zy.job.TestJob").newInstance()).getClass())
         */
        JobDetail jobDetail = JobBuilder.newJob(DiscountJob.class)
                .usingJobData("JobDetailStyle","JobBuilder called by controller")
                // 添加认证信息,有3种重写的方法,这里是其中一种,可以查看源码看其余2种
                .withIdentity(name + "Job")
                .build();

        SimpleScheduleBuilder sechdule = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5)
                .repeatForever();

        Trigger trigger = TriggerBuilder.newTrigger()
                .usingJobData("TriggerType",
                        "TriggerBuilder called by controller " + name)
                .usingJobData("name", name + "Trigger")
                .withIdentity(name + "Trigger")
                //.startNow()
                .startAt(start)
                //.endAt(start)
                .withSchedule(sechdule)
                .build();

        scheduler.scheduleJob(jobDetail, trigger);

        System.err.println("--------定时任务启动成功 "+ (new Date())+" ------------");

        return name + " started";
    }
}

在上面的代码中,我们给JobDetail和Trigger定义了不同的Identity,这个是为了说明他们各自的独立性,其实他们相同也没关系。

21.5.2 暂停执行任务

当期望暂停Quartz任务的时候,我们可以暂停Job或者Trigger。

下面是暂停Job的代码

    @PostMapping("/job/pause")
    public RestfulResult<String> pauseJob(@RequestParam  String name) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(name + "Job");
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);

        if (jobDetail == null) {
            return RestfulResult.fail(404,"Job " + name + " not found");
        }
        scheduler.pauseJob(jobKey);

        System.err.println("--------定时任务暂停运行 "+ LocalDateTime.now() +" ------------");

        return RestfulResult.success("Job " + name + " paused");
    }

下面是暂停Trigger的代码

    @PostMapping("/trigger/pause")
    public RestfulResult<String> pauseTrigger(@RequestParam String name) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(name + "Trigger");
        Trigger trigger = scheduler.getTrigger(triggerKey);

        if (trigger == null) {
            return RestfulResult.fail(404,"Trigger " + name + " not found");
        }

        scheduler.pauseTrigger(triggerKey);
        System.err.println("--------定时触发器暂停运行 "+ LocalDateTime.now()+" ------------");

        return RestfulResult.success("Trigger " + name + " paused");
    }

21.5.3 恢复执行任务

同样的,我们可以用两种不同的方法恢复执行任务的


    @PostMapping("/job/resume")
    public RestfulResult<String> resumeJob(@RequestParam String name) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(name + "Job");
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);

        if (jobDetail == null) {
            return RestfulResult.fail(404,"Job " + name + " not found");
        }
        scheduler.resumeJob(jobKey);

        System.err.println("--------定时任务暂停运行 "+ LocalDateTime.now() +" ------------");

        return RestfulResult.success("Job " + name + " resumed");
    }

    @PostMapping("/trigger/resume")
    public RestfulResult<String> resumeTrigger(@RequestParam String name) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(name + "Trigger");
        Trigger trigger = scheduler.getTrigger(triggerKey);

        if (trigger == null) {
            return RestfulResult.fail(404,"Trigger " + name + " not found");
        }

        scheduler.resumeTrigger(triggerKey);
        System.err.println("--------定时任务恢复运行 "+ LocalDateTime.now() +" ------------");

        return RestfulResult.success("Trigger " + name + " resumed");
    }

对于任务的执行状态来说,这两种方法是等价的。也就是说用暂停Job之后,可以通过恢复Trigger让他继续执行。

21.5.4 删除指定任务

下面是删除指定任务的代码。为简化起见,我们只展示一种写法,并且没有判断该任务是否存在(这其实是不应该的)

    @PostMapping("/job/remove")
    public RestfulResult<String>  del(@RequestParam String name) throws SchedulerException {
        scheduler.pauseTrigger(TriggerKey.triggerKey(name + "Trigger"));
        scheduler.unscheduleJob(TriggerKey.triggerKey(name + "Trigger"));
        scheduler.deleteJob(JobKey.jobKey(name + "Job"));

        System.err.println("--------定时任务已经被删除 "+ (new Date())+" ------------");

        return RestfulResult.success(name + " has been removed");
    }

21.6 监控Quartz的运行状态

我们既可以分别监控Job和Trigger的运行状况

21.6.1 设计Job监控器

package com.longser.union.cloud.service.schedule;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;

public class QuartzJobListener implements JobListener {

    /**
     * 用于获取该JobListener的名称
     */
    @Override
    public String getName() {
        return "Longser QuartzJobListener";
    }

    /**
     * Scheduler在JobDetail将要被执行时调用这个方法
     */
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("[" + jobName + "] jobToBeExecuted.....");
    }

    /**
     * Scheduler在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法
     */
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("[" + jobName + "] jobExecutionVetoed.....\n");
    }

    /**
     * Scheduler在JobDetail被执行之后调用这个方法
     */
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        String jobName = context.getJobDetail().getKey().getName();
        System.out.println("[" + jobName + "] jobWasExecuted....");
    }
}

21.6.2 设计Trigger监控器

package com.longser.union.cloud.service.schedule;

import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;

import java.time.LocalDateTime;
import java.util.Date;

public class QuartzTriggerListener implements TriggerListener {

    @Override
    public String getName() {
        return "Longser QuartzTriggerListener";
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        JobDataMap dataMap = trigger.getJobDataMap();

        System.out.println("[" + jobName + "," + dataMap.get("name") + "] triggerFired.....");
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        JobDataMap dataMap = trigger.getJobDataMap();


        if(LocalDateTime.now().getSecond() > 30) {
            System.out.println("Current time is " + (new Date()));
            System.out.println("[" + jobName + "," + dataMap.get("name") + "] rejected.....");

            return true;
        } else {
            System.out.println("[" + jobName + "," + dataMap.get("name") + "] permitted.....");

            return false;
        }
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        JobDataMap dataMap = trigger.getJobDataMap();

        System.out.println("[" + dataMap.get("name") + "] triggerMisfired.....\n");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
                                Trigger.CompletedExecutionInstruction instruction) {
        String jobName = context.getJobDetail().getKey().getName();
        JobDataMap dataMap = trigger.getJobDataMap();

        System.out.println("[" + jobName + "," + dataMap.get("name") + "] triggerComplete.....\n");
    }
}

上面代码的各方法的作用如下:

  • getName

    用于获取触发器的名称

  • triggerFired

    当与监听器相关联的Trigger被触发,Job上的execute()方法将被执行时,Scheduler就调用该方法。

  • vetoJobExecution

    在Trigger触发后,Job将要被执行时由Scheduler调用这个方法。TriggerListener给了一个选择去否决Job的执行。假如这个方法返回true,这个 Job将不会为此次Trigger触发而得到执行。

  • triggerMisfired

    Scheduler调用这个方法是在Trigger错过触发时。应该关注此方法中持续时间长的逻辑:在出现许多错过触发的Trigger时,长逻辑会导致骨牌效应,应当保持这方法尽量的小。

  • triggerComplete

    Trigger被触发并且完成了Job的执行时, Scheduler调用这个方法。

注意在上面的代码中,我们为了展示拒绝Trigger执行的效果,设计了一个测试性质的机制:每分钟的后30秒不执行。

21.6.3 添加监听器

在Scheduler创建任务之后添加监听器

        scheduler.scheduleJob(jobDetail, trigger);

+       ListenerManager listenerManager = scheduler.getListenerManager();
+
+       listenerManager.addJobListener(new QuartzJobListener(),
+               KeyMatcher.keyEquals(JobKey.jobKey(name + "Job")));
+
+       listenerManager.addTriggerListener(new QuartzTriggerListener(),
+               KeyMatcher.keyEquals(TriggerKey.triggerKey(name + "Trigger")));

下面是控制台上完整的输入内容,可以看到这些方法的调用过程

--------定时任务启动成功 Thu Aug 19 16:04:20 CST 2021 ------------
[testJob,testTrigger] triggerFired.....
[testJob,testTrigger] permitted.....
[testJob] jobToBeExecuted.....
Thu Aug 19 16:04:27 CST 2021
Current Job Detail Style: JobBuilder called by controller
Current Trigger Type: TriggerBuilder called by controller test
更新数据库中商品价格,统一打5折
[testJob] jobWasExecuted....
[testJob,testTrigger] triggerComplete.....

[testJob,testTrigger] triggerFired.....
Current time is Thu Aug 19 16:04:32 CST 2021
[testJob,testTrigger] rejected.....
[testJob] jobExecutionVetoed.....

[testJob,testTrigger] triggerFired.....
Current time is Thu Aug 19 16:04:37 CST 2021
[testJob,testTrigger] rejected.....
[testJob] jobExecutionVetoed.....

--------定时任务暂停运行 Thu Aug 19 16:04:40 CST 2021 ------------

21.7 cron表达式书写规范

21.7.1 格式说明

Cron的表达式是字符串,实际上是由七子表达式,描述个别细节的时间表。这些子表达式是分开的空白,代表:

  • Seconds
  • Minutes
  • Hours
  • Day-of-Month
  • Month
  • Day-of-Week
  • Year (可选字段)

例:0 0 9 ? * FRI 在每星期五上午9:00 执行

名称 是否必须 允许值 特殊字符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W C
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L C #
空 或 1970-2099 , - * /
  • 月份和星期的名称是不区分大小写的,FRI 和 fri 是一样的
  • 域之间有空格分隔 ? *
  • 这个表达会每秒钟(每分种的、每小时的、每天的)激发一个部署的 job

    21.7.2 特殊字符的解释

    *:代表所有可能的值
    -:指定范围
    ,:列出枚举??例如在分钟里,"5,15"表示5分钟和20分钟触发
    /:指定增量??例如在分钟里,"3/15"表示从3分钟开始,没隔15分钟执行一次
    ?:表示没有具体的值,使用?要注意冲突
    L:表示last,例如星期中表示7或SAT,月份中表示最后一天31或30,6L表示这个月倒数第6天,FRIL表示这个月的最后一个星期五
    W:只能用在月份中,表示最接近指定天的工作日
    #:只能用在星期中,表示这个月的第几个周几,例如6#3表示这个月的第3个周五
    

    (1)星号 *

  • 使用星号(*) 指示着你想在这个域上包含所有合法的值。例如,在月份域上使用星号意味着每个月都会触发这个 trigger。

  • 表达式样例:0 9 * ?
  • 意义:每天从上午9点到上午9:59中的每分钟激发一次 trigger。它停在上午 9:59 是因为值 9 在小时域上,在上午 10 点时,小时变为 10 了,也就不再理会这个 trigger,直到下一天的上午 9 点。在你希望 trigger 在该域的所有有效值上被激发时使用 * 字符。

(2)问号 ?

  • ? 号只能用在日和周域上,但是不能在这两个域上同时使用。你可以认为 ? 字符是 “我并不关心在该域上是什么值。” 这不同于星号,星号是指示着该域上的每一个值。? 是说不为该域指定值。
  • 不能同时这两个域上指定值的理由是难以解释甚至是难以理解的。基本上,假定同时指定值的话,意义就会变得含混不清了:考虑一下,如果一个表达式在日域上有值11,同时在周域上指定了 WED。那么是要 trigger 仅在每个月的11号,且正好又是星期三那天被激发?还是在每个星期三的11号被激发呢?要去除这种不明确性的办法就是不能同时在这两个域上指定值。
  • 只要记住,假如你为这两域的其中一个指定了值,那就必须在另一个字值上放一个 ?。
  • 表达式样例:0 10,44 14 ? 3 WEB
  • 意义:在三月中的每个星期三的14:10 和 14:44 被触发。

(3)逗号 ,

  • 逗号 (,) 是用来在给某个域上指定一个值列表的。例如,使用值 0,15,30,45 在秒域上意味着每15秒触发一个 trigger。
  • 表达式样例:0 0,15,30,45 * ?
  • 意义:每刻钟触发一次 trigger。

(4) 斜杠 /

  • 斜杠 (/) 是用于时间表的递增的。我们刚刚用了逗号来表示每15分钟的递增,但是我们也能写成这样 0/15。
  • 表达式样例:0/15 0/30 * ?
  • 意义:在整点和半点时每15秒触发 trigger。

(5)中划线 -

  • 中划线 (-) 用于指定一个范围。例如,在小时域上的 3-8 意味着 “3,4,5,6,7 和 8 点。” 域的值不允许回卷,所以像 50-10 这样的值是不允许的。
  • 表达式样例:0 45 3-8 ?
  • 意义:在上午的3点至上午的8点的45分时触发 trigger。

(6)字母 L

  • L 说明了某域上允许的最后一个值。它仅被日和周域支持。当用在日域上,表示的是在月域上指定的月份的最后一天。例如,当月域上指定了 JAN 时,在日域上的 L 会促使 trigger 在1月31号被触发。假如月域上是 SEP,那么 L 会预示着在9月30号触发。换句话说,就是不管指定了哪个月,都是在相应月份的时最后一天触发 trigger。
  • 表达式 0 0 8 L ? 意义是在每个月最后一天的上午 8:00 触发 trigger。在月域上的 说明是 “每个月”。
  • 当 L 字母用于周域上,指示着周的最后一天,就是星期六 (或者数字7)。所以如果你需要在每个月的最后一个星期六下午的 11:59 触发 trigger,你可以用这样的表达式 0 59 23 ? * L。
  • 当使用于周域上,你可以用一个数字与 L 连起来表示月份的最后一个星期 X。例如,表达式 0 0 12 ? * 2L 说的是在每个月的最后一个星期一触发 trigger。
  • 不要让范围和列表值与 L 连用,虽然你能用星期数(1-7)与 L 连用,但是不允许你用一个范围值和列表值与 L 连用。这会产生不可预知的结果。

(7)字母 W

  • W 字符代表着平日 (Mon-Fri),并且仅能用于日域中。它用来指定离指定日的最近的一个平日。大部分的商业处理都是基于工作周的,所以 W 字符可能是非常重要的。例如,日域中的 15W 意味着 “离该月15号的最近一个平日。” 假如15号是星期六,那么 trigger 会在14号(星期五)触发,如果15号当天为平日直接就会当日执行。
  • W 只能用在指定的日域为单天,不能是范围或列表值。
  • 当LW连用,表示本月的最后一个平日

(8)井号 #

  • 字符仅能用于周域中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。另一个例子 2#1 意思是某月的第一个星期一 (2=星期一,#1意味着月份中的第一周)。

  • 注意,假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。

21.7.3 各种示例

表达式 意义
/5 * ? 每隔5秒执行一次
0 ? 每1分钟触发一次
0 /1 ? 每隔1分钟执行一次
0 0/3 * ? 每三分钟触发一次
0 0 * ? 每天每1小时触发一次
0 0 10 ? 每天10点触发一次
0 15 10 ? 每天上午10:15触发
0 15 10 ? 每天上午10:15触发
0 15 10 ? * 每天上午10:15触发
0 14 * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 0 5-15 ? 每天5-15点整点触发
0 30 9 1 * ? 每月1号上午9点半触发一次
0 0 0 1 * ?? 每月1号凌晨执行一次
0 15 10 15 * ? 每月15日上午10:15触发
0 15 10 ? 2005 2005年的每天上午10:15触发
0 14 * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 0/5 14 1 在每星期天下午2点到下午2:55期间的每5分钟触发
0 0-5 14 ? 在每天下午2点到下午2:05期间的每1分钟触发
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 15 10 15 * ? 每月15日上午10:15触发
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

21.8 小结

Spring 可以利用一个简单的注解,快速实现定时任务的功能。如果感觉 Spring 提供的定时任务机制还不足以满足需求,还可以方便地集成 Quartz 框架。

下面是一些可以进一步了解Quartz用法的链接

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。