SpringBoot之Quartz

510 阅读13分钟

SpringBoot之Quartz

pom.xml

SpringBoot版本为2.7.5

<!-- Quartz任务调度【定时任务】-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

配置定时任务

  • 创建定时任务配置类。
  • 初始化定时任务详情,设置需要执行的服务类和指定方法并将定时任务详情注册到Spring容器。
  • 初始化定时任务触发器,设置对应的定时任务详情以及触发时间并将定时任务触发器注册到Spring容器。
  • 初始化定时任务总控制器,设置所有定时任务触发器,设置是否启动定时任务,将定时任务总控制器注册到Spring容器。
  • 启动应用后如果定时任务设置为启动,则会根据定时任务触发器触发执行对应的方法。

定时任务配置类:

/**
 * 定时任务-配置类
 * @author sword
 * @date 2022/11/17 17:07
 */
@Configuration
@Slf4j
@RequiredArgsConstructor
public class SchedulerConfig {

    /**
     * 定时任务-参数
     */
    private final SchedulerProperties schedulerProperties;

    /**
     * 时钟-定时任务详情
     * @param clockService 时钟服务
     * @return 时钟-定时任务详情
     * @author wukongjian
     * @date 2019/10/16 10:10
     */
    @Bean
    public MethodInvokingJobDetailFactoryBean clockJobDetail(ClockService clockService) {
        return QuartzUtil.createJobDetail(clockService, "clock");
    }

    /**
     * 时钟-定时任务触发器
     * @param clockJobDetail 时钟-定时任务详情
     * @return org.springframework.scheduling.quartz.CronTriggerFactoryBean 时钟-定时任务触发器
     * @author wukongjian
     * @date 2019/10/16 10:10
     */
    @Bean
    public CronTriggerFactoryBean clockCronTrigger(JobDetail clockJobDetail) {
        return QuartzUtil.createCronTrigger(clockJobDetail, "* * * * * ?");
    }

    /**
     * Quartz定时任务总控制器
     * @param cronTriggers 配置的所有定时任务触发器
     * @return Quartz定时任务总控制器
     * @author wukongjian
     * @date 2019/10/16 9:35
     */
    @Bean
    public SchedulerFactoryBean scheduler(Trigger[] cronTriggers) {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        // 设置定时任务触发器
        schedulerFactoryBean.setTriggers(cronTriggers);
        // 设置自动启动定时任务
        schedulerFactoryBean.setAutoStartup(schedulerProperties.isEnabled());
        log.info("定时任务{}启动", schedulerProperties.isEnabled() ? "已" : "未");
        return schedulerFactoryBean;
    }
}

定时任务详情设置的服务类和对应方法:

/**
 * 时钟服务
 * @author sword
 * @date 2022/11/17 17:10
 */
public interface ClockService {
    /**
     * 滴答一下
     * @author sword
     * @date 2022/11/17 17:11
     */
    void clock();
}

/**
 * 时钟服务实现类
 * @author sword
 * @date 2022/11/17 17:11
 */
@Service
@Slf4j
public class ClockServiceImpl implements ClockService {
    @Override
    public void clock() {
        log.info(LocalDateTime.now().toString());
    }
}

可以在 application.yml 中设置定时任务是否开启,如果不设置,则默认不开启。

application.yml:

scheduler:
  enabled: true

定时任务参数类:

/**
 * 定时任务-参数
 * @author sword
 * @date 2022/12/6 13:48
 */
@ConfigurationProperties(prefix = SchedulerProperties.PREFIX)
@Component
@Data
public class SchedulerProperties {
    /**
     * 参数前缀
     */
    public static final String PREFIX = "scheduler";

    /**
     * 是否开启
     * 默认为空
     */
    private boolean enabled = false;
}

Quartz工具类主要是封装了定时任务详情和定时任务触发器的初始化方法。

/**
 * Quartz工具类
 * @author sword
 * @date 2022/11/17 20:27
 */
public class QuartzUtil {
    /**
     * 初始化一个定时任务详情
     * @param targetObject 目标类对象
     * @param targetMethod 目标方法
     * @return org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean 定时详情
     * @author wukongjian
     * @date 2019/10/16 9:58
     */
    public static MethodInvokingJobDetailFactoryBean createJobDetail(Object targetObject, String targetMethod) {
        MethodInvokingJobDetailFactoryBean jobDetailFactoryBean = new MethodInvokingJobDetailFactoryBean();
        // 设置目标类对象
        jobDetailFactoryBean.setTargetObject(targetObject);
        // 设置目标方法
        jobDetailFactoryBean.setTargetMethod(targetMethod);
        // 防止并发执行
        jobDetailFactoryBean.setConcurrent(false);
        return jobDetailFactoryBean;
    }

    /**
     * 初始化一个定时任务触发器
     * @param jobDetail 定时任务详情
     * @param cronExpression 触发器执行时间表达式
     * @return 定时任务触发器
     * @author wukongjian
     * @date 2019/10/16 10:04
     */
    public static CronTriggerFactoryBean createCronTrigger(JobDetail jobDetail, String cronExpression) {
        CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
        // 设置定时任务详情
        cronTriggerFactoryBean.setJobDetail(jobDetail);
        // 设置执行时间表达式
        cronTriggerFactoryBean.setCronExpression(cronExpression);
        return cronTriggerFactoryBean;
    }
}

应用启动定时触发执行指定方法:

16:53:14.553 [Thread-1] DEBUG org.springframework.boot.devtools.restart.classloader.RestartClassLoader - Created RestartClassLoader org.springframework.boot.devtools.restart.classloader.RestartClassLoader@29952818

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.5)

2022-12-06 16:53:15.138  INFO 57352 --- [  restartedMain] com.sword.demo.Application               : Starting Application using Java 1.8.0_202 on wkj with PID 57352 (E:\workspace\Git\demo\quartz-springboot\target\classes started by wukongjian in E:\workspace\Git\demo\quartz-springboot)
2022-12-06 16:53:15.141  INFO 57352 --- [  restartedMain] com.sword.demo.Application               : No active profile set, falling back to 1 default profile: "default"
2022-12-06 16:53:15.191  INFO 57352 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2022-12-06 16:53:15.191  INFO 57352 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2022-12-06 16:53:16.266 ERROR 57352 --- [  restartedMain] o.a.catalina.core.AprLifecycleListener   : An incompatible version [1.1.27] of the Apache Tomcat Native library is installed, while Tomcat requires version [1.2.14]
2022-12-06 16:53:16.563  INFO 57352 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-12-06 16:53:16.571  INFO 57352 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-12-06 16:53:16.571  INFO 57352 --- [  restartedMain] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.68]
2022-12-06 16:53:16.693  INFO 57352 --- [  restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-12-06 16:53:16.693  INFO 57352 --- [  restartedMain] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1501 ms
2022-12-06 16:53:16.791  INFO 57352 --- [  restartedMain] c.s.demo.quartz.config.SchedulerConfig   : 定时任务已启动
2022-12-06 16:53:16.812  INFO 57352 --- [  restartedMain] org.quartz.impl.StdSchedulerFactory      : Using default implementation for ThreadExecutor
2022-12-06 16:53:16.833  INFO 57352 --- [  restartedMain] org.quartz.core.SchedulerSignalerImpl    : Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
2022-12-06 16:53:16.833  INFO 57352 --- [  restartedMain] org.quartz.core.QuartzScheduler          : Quartz Scheduler v.2.3.2 created.
2022-12-06 16:53:16.834  INFO 57352 --- [  restartedMain] org.quartz.simpl.RAMJobStore             : RAMJobStore initialized.
2022-12-06 16:53:16.834  INFO 57352 --- [  restartedMain] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'scheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

2022-12-06 16:53:16.834  INFO 57352 --- [  restartedMain] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'scheduler' initialized from an externally provided properties instance.
2022-12-06 16:53:16.834  INFO 57352 --- [  restartedMain] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2022-12-06 16:53:16.837  INFO 57352 --- [  restartedMain] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.AdaptableJobFactory@11ad3263
2022-12-06 16:53:17.976  INFO 57352 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2022-12-06 16:53:18.026  INFO 57352 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-12-06 16:53:18.028  INFO 57352 --- [  restartedMain] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now
2022-12-06 16:53:18.028  INFO 57352 --- [  restartedMain] org.quartz.core.QuartzScheduler          : Scheduler scheduler_$_NON_CLUSTERED started.
2022-12-06 16:53:18.041  INFO 57352 --- [  restartedMain] com.sword.demo.Application               : Started Application in 3.475 seconds (JVM running for 4.916)
2022-12-06 16:53:18.047  INFO 57352 --- [eduler_Worker-1] c.s.d.q.service.impl.ClockServiceImpl    : 2022-12-06T16:53:18.047
2022-12-06 16:53:18.049  INFO 57352 --- [eduler_Worker-2] c.s.d.q.service.impl.ClockServiceImpl    : 2022-12-06T16:53:18.049
2022-12-06 16:53:18.050  INFO 57352 --- [eduler_Worker-3] c.s.d.q.service.impl.ClockServiceImpl    : 2022-12-06T16:53:18.050
2022-12-06 16:53:19.001  INFO 57352 --- [eduler_Worker-4] c.s.d.q.service.impl.ClockServiceImpl    : 2022-12-06T16:53:19.001
2022-12-06 16:53:20.005  INFO 57352 --- [eduler_Worker-5] c.s.d.q.service.impl.ClockServiceImpl    : 2022-12-06T16:53:20.005

定时任务手动操作接口

定时任务服务:

/**
 * 定时任务-服务
 * @author sword
 * @date 2022/11/28 17:24
 */
public interface SchedulerService {

    /**
     * 手动执行指定定时任务详情
     *
     * @param jobDetailId 任务详情id
     * @throws InvocationTargetException 执行目标方法异常
     * @throws IllegalAccessException 非法访问异常
     * @author sword
     * @date 2022/11/28 17:25
     */
    void invokeJobDetail(String jobDetailId) throws InvocationTargetException, IllegalAccessException;

    /**
     * 启动定时任务
     *
     * @throws SchedulerException 定时问题异常
     * @author sword
     * @date 2022/11/28 17:25
     */
    void start() throws SchedulerException;

    /**
     * 暂停定时任务,可以重新启动
     *
     * @throws SchedulerException 定时问题异常
     * @author sword
     * @date 2022/11/28 17:25
     */
    void standby() throws SchedulerException;

    /**
     * 关闭定时任务,无法重新启动
     *
     * @throws SchedulerException 定时问题异常
     * @author sword
     * @date 2022/11/28 17:25
     */
    void shutdown() throws SchedulerException;
}

定时任务服务实现类:

/**
 * 定时任务-服务实现类
 * @author sword
 * @date 2022/11/28 17:29
 */
@Service
@RequiredArgsConstructor
public class SchedulerServiceImpl implements SchedulerService {
    /**
     * 定时任务总控制器
     */
    private final Scheduler scheduler;

    /**
     * Spring上下文
     */
    private final ApplicationContext context;

    @Override
    public void invokeJobDetail(String jobDetailId) throws InvocationTargetException, IllegalAccessException {
        /*
         * 根据指定定时任务详情bean的id获取该bean
         * 因为定时任务详情配置的都是FactoryBean,所以需要添加前缀&来直接获取该bean的MethodInvoker接口,
         * 而不是使用FactoryBean的getObject方法获取到的JobDetail
         * 获取到定时任务详情后执行该任务
         */
        ((MethodInvoker) context.getBean("&" + jobDetailId)).invoke();
    }

    @Override
    public void start() throws SchedulerException {
        scheduler.start();
    }

    @Override
    public void standby() throws SchedulerException {
        scheduler.standby();
    }

    @Override
    public void shutdown() throws SchedulerException {
        scheduler.shutdown();
    }
}

定时任务接口:

/**
 * 定时任务-接口
 * @author sword
 * @date 2022/11/29 15:33
 */
@RestController
@RequestMapping("/scheduler-job")
@Tag(name = "SchedulerApi", description = "定时任务-接口")
@RequiredArgsConstructor
public class SchedulerApi {

    /**
     * 定时任务-服务
     */
    private final SchedulerService schedulerService;

    /**
     * 手动执行指定定时任务详情
     * @param jobDetailId 定时任务详情bean的id
     * @author sword
     * @date 2022/11/29 15:33
     */
    @PostMapping("/invokeJobDetail")
    @Operation(summary = "手动执行指定定时任务详情")
    @Parameter(name = "jobDetailId", description = "定时任务详情bean的id", in = ParameterIn.QUERY)
    public void invokeJobDetail(String jobDetailId) throws InvocationTargetException, IllegalAccessException {
        schedulerService.invokeJobDetail(jobDetailId);
    }

    /**
     * 启动定时任务
     * @author sword
     * @date 2022/11/29 15:33
     */
    @PostMapping("/start")
    @Operation(summary = "启动定时任务")
    public void start() throws SchedulerException {
        schedulerService.start();
    }

    /**
     * 暂停定时任务,可以重新启动
     * @author sword
     * @date 2022/11/29 15:33
     */
    @PostMapping("/standby")
    @Operation(summary = "暂停定时任务,可以重新启动")
    public void standby() throws SchedulerException {
        schedulerService.standby();
    }

    /**
     * 关闭定时任务,无法重新启动
     * @author sword
     * @date 2022/11/29 15:33
     */
    @PostMapping("/shutdown")
    @Operation(summary = "关闭定时任务,无法重新启动")
    public void shutdown() throws SchedulerException {
        schedulerService.shutdown();
    }
}

swagger文档

Cron表达式

上面讲的定时任务触发器使用的是CronTrigger,使用Cron表达式来指定触发的时间。

以下内容来自于 org.quartz.CronTriggerorg.quartz.CronExpression 的javadoc。

Cron表达式包含6个必填字段和一个可选字段,7个字段从左往右按顺序以空格相隔。

从左往右的字段描述如下:

字段名称允许的值允许的特殊字符
0-59, - * /
0-59, - * /
0-23, - * /
1-31, - * ? / L W
0-11 或者 JAN-DEC, - * /
星期几1-7 或者 SUN-SAT, - * ? / L #
年(可选)空 或者 1970-2199, - * /

字段允许的值和特殊字符不区分大小写。

“*”字符用于指定所有值。例如,“分”字段中的“*”表示“每分钟”。

“?”字符可以在“日”和“星期几”字段中使用。它用于指定“无特定值”。当需要在“日”和“星期几”两个字段中的一个字段中指定内容且另一个字段不使用时,如“日”字段值设置为“*”,即每天,此时“星期几”字段的值可以设置为“?”,即无特定。

“-”字符用于指定范围。例如,“时”字段中的“10-12”表示“10小时、11小时和12小时”。支持溢出范围,即左侧的数字大于右侧的数字,如“22-2”即晚上10点到凌晨2点之间。

“,”字符用于指定列表。例如,“星期几”字段中的“MON,WED,FRI”表示“星期一,星期三和星期五”。

“/”字符用于在所有字段的允许起始值的基础上再次指定需要的增量值,例如,“秒”字段中的“0/15”表示“0秒、15秒、30秒和45秒”。秒字段中的“5/15”表示“5秒、20秒、35秒和50秒”。在“/”之前指定“*”等同于指定0作为起始值。要注意的是,增量值如果超过了字段的允许值的上限则重新从起始值开始,即“月”字段中的“7/6”仅在7月时触发,没有有效的增量值。

“L”字符是“last”的缩写,可以在“日”和“星期几”字段中使用,在两个字段中有各自不同的含义。例如,“日”字段中的值“L”表示“一个月的最后一天”,如1月31日或者非闰年的2月28日,“L-n”表示“一个月的最后一天的前第n天”,如“L-3”为“一个月的最后一天的前第3天”。“L”在“星期几”字段中单独使用,它只表示“7”或“SAT”,如果“L”作为后缀和“1-7”中的数字一起使用,则表示“当月的最后xxx天”,例如“6L”表示“当月中的最后一个星期五”。使用“L”选项时,不要指定列表或范围如“1,7L”或者“1-7L”,否则会出现无法预计的结果。

“W”字符可以在“日”字段中使用,用于指定最接近给定日期的工作日(星期一至星期五)。例如,如果“日”字段的值为“15W”,则其含义为:“最接近当月15日的工作日”。因此,如果15日是星期六,则将在14日星期五触发;如果15日是星期天,则将在16日星期一触发,如果15日是星期二,则将在15日星期二触发。但是,如果“日”字段的值为“1W”且第1天是星期六,则将在当月3日星期一触发,而不会在上个月的工作日触发,因为它不会“跳过”一个月的日期边界。“W”字符只能和一个月的固定某一天一起使用,不能和日期范围或列表一起使用。

“L”和“W”字符也可以组合为“LW”用于“日”字段,即“当月的最后一个工作日”。

“#”字符可以在“星期几”字段中使用,用于指定每月的第n个星期几。例如,“星期几”字段中的值“6#3”表示当月的第三个星期五(“6”=星期五,“#3”=当月的第3个星期五)。其他示例:“2#1”=当月的第一个星期一,“4#5”=当月第五个星期三。如果指定“#5”且当月没有第五个星期几,则不会触发。如果使用“#”字符,则“星期几”字段中只能有一个表达式(“3#1,6#3”无效,因为有两个表达式)。

案例:

表达式备注
0 0 12 * * ?每天中午12点
0 15 10 ? * *每天上午10:15
0 15 10 * * ?每天上午10:15
0 15 10 * * ? *每天上午10:15
0 15 10 * * ? 20052005年每天上午10:15
0 * 14 * * ?每天14:00到14:59,每分钟一次
0 0/5 14 * * ?每天14:00到14:55,每5分钟一次
0 0/5 14,18 * * ?每天14:00到14:55和18:00到18:55,每5分钟一次
0 0-5 14 * * ?每天14:00到14:05,每分钟一次
0 10,44 14 ? 3 WED三月每周三14:10和14: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-20052002年、2003年、2004年和2005年每个月的最后一个星期五10:15
0 15 10 ? * 6#3每月第三个星期五10:15

相关源码详见gitee