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();
}
}
Cron表达式
上面讲的定时任务触发器使用的是CronTrigger,使用Cron表达式来指定触发的时间。
以下内容来自于 org.quartz.CronTrigger 和 org.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 * * ? 2005 | 2005年每天上午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-2005 | 2002年、2003年、2004年和2005年每个月的最后一个星期五10:15 |
| 0 15 10 ? * 6#3 | 每月第三个星期五10:15 |