SpringBoot内置了Sping Schedule定时框架,通过@schedule注解驱动方式添加所注解方法到定时任务,再根据配置的定时信息定时执行。@schedule注解内部默认采用一个线程的线程池来串行执行任务的。一般情况下没有什么问题,但是如果有多个定时任务,每个定时任务执行时间可能不短的情况下,那么有的定时任务可能一直没有机会执行。
Java 定时任务的几种实现方式:
- 基于 java.util.Timer 定时器,实现类似闹钟的定时任务。
- 使用 Quartz、elastic-job、xxl-job 等开源第三方定时任务框架,适合分布式项目应用。
- 使用 Spring 提供的一个注解: @Schedule,开发简单,使用比较方便,也是本文介绍的一种方式。
1、@schedule入门示例
1.1 创建定时任务
首先,需要在项目启动类上添加 @EnableScheduling 注解,以开启对定时任务的支持。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
其中 @EnableScheduling 注解的作用是发现注解 @Scheduled 的任务并后台执行。
1.2 编写定时任务类和方法
编写定时任务类和方法,定时任务类通过 Spring IOC 加载,使用 @Component 注解,定时方法使用 @Scheduled 注解。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ScheduledTask {
@Scheduled(fixedRate = 3000)
public void scheduledTask() {
System.out.println("任务执行时间:" + LocalDateTime.now());
}
}
fixedRate 是 long 类型,表示任务执行的间隔毫秒数,以上代码中的定时任务每 3 秒执行一次。运行该springboot项目,项目的启动和运行日志如下,可以看到每 3 秒打印一次日志执行记录。
2022-08-22 14:46:27.416 INFO 8856 --- [ main] com.kz.example.DemoApplication : No active profile set, falling back to default profiles: default
2022-08-22 14:46:27.864 INFO 8856 --- [ main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService 'taskScheduler'
2022-08-22 14:46:27.880 INFO 8856 --- [ main] com.kz.example.DemoApplication : Started DemoApplication in 0.7 seconds (JVM running for 1.562)
任务执行时间:2022-08-22T14:46:27.880
任务执行时间:2022-08-22T14:46:30.893
任务执行时间:2022-08-22T14:46:33.895
任务执行时间:2022-08-22T14:46:36.899
任务执行时间:2022-08-22T14:46:39.889
2、@Scheduled详解
2.1 @Scheduled 使用方式
在@schedule入门示例中,使用了@Scheduled(fixedRate = 3000) 注解来定义每 3 秒执行的任务,对于 @Scheduled 的使用可以总结如下几种方式:
@Scheduled(fixedRate = 3000)
:上一次开始执行时间点之后 3 秒再执行(fixedRate 属性:定时任务开始后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)@Scheduled(fixedDelay = 3000)
:上一次执行完毕时间点之后 3 秒再执行(fixedDelay 属性:定时任务执行完成后再次执行定时任务的延时(需等待上次定时任务完成),单位毫秒)@Scheduled(initialDelay = 1000, fixedRate = 3000)
:第一次延迟1秒后执行,之后按fixedRate的规则每 3 秒执行一次(initialDelay 属性:第一次执行定时任务的延迟时间,需配合fixedDelay或者fixedRate来使用)@Scheduled(cron="0 0 2 1 * ? *")
:通过cron表达式定义规则
默认情况下 @Schedule 使用线程池的大小为 1。一般情况下没有什么问题,但是如果有多个定时任务,每个定时任务执行时间可能不短的情况下,那么有的定时任务可能一直没有机会执行。看下面的例子:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
@Component
public class ScheduledTask {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Scheduled(cron = "*/5 * * * * ?")
private void cron() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedDelay = 5000)
private void fixedDelay() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedRate = 5000)
private void fixedRate() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
}
运行springboot项目后,看到的日志情况如下:
scheduling-1-fixedRate:2022-08-22 14:49:17
scheduling-1-fixedDelay:2022-08-22 14:49:23
scheduling-1-cron:2022-08-22 14:49:29
scheduling-1-fixedRate:2022-08-22 14:49:35
scheduling-1-fixedRate:2022-08-22 14:49:41
scheduling-1-fixedRate:2022-08-22 14:49:47
scheduling-1-fixedDelay:2022-08-22 14:49:53
scheduling-1-fixedRate:2022-08-22 14:49:59
scheduling-1-cron:2022-08-22 14:50:05
scheduling-1-fixedRate:2022-08-22 14:50:11
scheduling-1-fixedRate:2022-08-22 14:50:17
scheduling-1-fixedRate:2022-08-22 14:50:23
scheduling-1-fixedRate:2022-08-22 14:50:29
scheduling-1-fixedRate:2022-08-22 14:50:35
scheduling-1-fixedDelay:2022-08-22 14:50:41
scheduling-1-fixedRate:2022-08-22 14:50:47
scheduling-1-fixedRate:2022-08-22 14:50:53
scheduling-1-cron:2022-08-22 14:50:59
scheduling-1-fixedRate:2022-08-22 14:51:05
scheduling-1-fixedRate:2022-08-22 14:51:11
scheduling-1-fixedRate:2022-08-22 14:51:17
scheduling-1-fixedRate:2022-08-22 14:51:23
scheduling-1-fixedRate:2022-08-22 14:51:29
scheduling-1-fixedRate:2022-08-22 14:51:35
scheduling-1-fixedRate:2022-08-22 14:51:41
scheduling-1-fixedRate:2022-08-22 14:51:47
scheduling-1-fixedDelay:2022-08-22 14:51:53
scheduling-1-fixedRate:2022-08-22 14:51:59
scheduling-1-fixedRate:2022-08-22 14:52:05
scheduling-1-fixedRate:2022-08-22 14:52:11
scheduling-1-cron:2022-08-22 14:52:17
scheduling-1-fixedRate:2022-08-22 14:52:23
scheduling-1-fixedRate:2022-08-22 14:52:29
scheduling-1-fixedRate:2022-08-22 14:52:35
scheduling-1-fixedRate:2022-08-22 14:52:41
scheduling-1-fixedRate:2022-08-22 14:52:47
scheduling-1-fixedRate:2022-08-22 14:52:53
scheduling-1-fixedRate:2022-08-22 14:52:59
scheduling-1-fixedRate:2022-08-22 14:53:05
scheduling-1-fixedRate:2022-08-22 14:53:11
scheduling-1-fixedRate:2022-08-22 14:53:17
scheduling-1-fixedRate:2022-08-22 14:53:23
scheduling-1-fixedDelay:2022-08-22 14:53:29
scheduling-1-fixedRate:2022-08-22 14:53:35
scheduling-1-fixedRate:2022-08-22 14:53:41
scheduling-1-fixedRate:2022-08-22 14:53:47
scheduling-1-fixedRate:2022-08-22 14:53:53
scheduling-1-cron:2022-08-22 14:53:59
scheduling-1-fixedRate:2022-08-22 14:54:05
scheduling-1-fixedRate:2022-08-22 14:54:11
scheduling-1-fixedRate:2022-08-22 14:54:17
scheduling-1-fixedRate:2022-08-22 14:54:23
scheduling-1-fixedRate:2022-08-22 14:54:29
scheduling-1-fixedRate:2022-08-22 14:54:35
scheduling-1-fixedRate:2022-08-22 14:54:41
scheduling-1-fixedRate:2022-08-22 14:54:47
scheduling-1-fixedRate:2022-08-22 14:54:53
scheduling-1-fixedRate:2022-08-22 14:54:59
scheduling-1-fixedRate:2022-08-22 14:55:05
scheduling-1-fixedRate:2022-08-22 14:55:11
scheduling-1-fixedRate:2022-08-22 14:55:17
scheduling-1-fixedRate:2022-08-22 14:55:23
scheduling-1-fixedRate:2022-08-22 14:55:29
scheduling-1-fixedDelay:2022-08-22 14:55:35
scheduling-1-fixedRate:2022-08-22 14:55:41
仔细观察可以发现,上面的任务中,fixedDelay 与 cron 很久都不会被执行。
@Schedule 的三种方式 cron、fixedDelay、fixedRate 不管线程够不够都会阻塞到上一次执行完成,才会执行下一次。如果任务方法执行时间非常短,上面三种方式其实基本没有太多的区别。如果任务方法执行时间比较长,大于了设置的执行周期,那么就有很大的区别。例如,假设执行任务的线程足够,执行周期是 5s,任务方法会执行 6s。
- cron 的执行方式是,任务方法执行完,遇到下一次匹配的时间再次执行,基本就会 10s 执行一次,因为执行任务方法的时间区间会错过一次匹配。
- fixedDelay 的执行方式是,方法执行了 6s,然后会再等 5s 再执行下一次,在上面的条件下,基本就是每 11s 执行一次。
- fixedRate 的执行方式就变成了每隔 6s 执行一次,因为按固定区间执行它没 5s 就应该执行一次,但是任务方法执行了 6s,没办法,只好 6s 执行一次。
2.2 @Scheduled 多线程
要解决上面的问题,可以把执行任务的线程池设置大一点。
2.2.1 实现 SchedulingConfigurer 接口
新建一个MyScheduleConfig类,实现 SchedulingConfigurer 接口,在 configureTasks 方法中配置线程数。
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
再次运行项目,日志如下:
pool-1-thread-1-fixedRate:2022-08-22 15:06:07
pool-1-thread-3-fixedDelay:2022-08-22 15:06:07
pool-1-thread-2-cron:2022-08-22 15:06:10
pool-1-thread-1-fixedRate:2022-08-22 15:06:13
pool-1-thread-3-fixedDelay:2022-08-22 15:06:18
pool-1-thread-5-fixedRate:2022-08-22 15:06:19
pool-1-thread-1-cron:2022-08-22 15:06:20
pool-1-thread-2-fixedRate:2022-08-22 15:06:25
pool-1-thread-6-fixedDelay:2022-08-22 15:06:29
pool-1-thread-5-cron:2022-08-22 15:06:30
pool-1-thread-2-fixedRate:2022-08-22 15:06:31
pool-1-thread-2-fixedRate:2022-08-22 15:06:37
pool-1-thread-8-cron:2022-08-22 15:06:40
pool-1-thread-6-fixedDelay:2022-08-22 15:06:40
pool-1-thread-2-fixedRate:2022-08-22 15:06:43
pool-1-thread-2-fixedRate:2022-08-22 15:06:49
pool-1-thread-8-cron:2022-08-22 15:06:50
pool-1-thread-4-fixedDelay:2022-08-22 15:06:51
pool-1-thread-2-fixedRate:2022-08-22 15:06:55
pool-1-thread-8-cron:2022-08-22 15:07:00
pool-1-thread-2-fixedRate:2022-08-22 15:07:01
pool-1-thread-1-fixedDelay:2022-08-22 15:07:02
pool-1-thread-2-fixedRate:2022-08-22 15:07:07
pool-1-thread-3-cron:2022-08-22 15:07:10
2.2.2 实现 SchedulingConfigurer 接口
也可以创建一个配置类(@Configuration),注入一个TaskScheduler bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class MyConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(5);
return taskScheduler;
}
}
当然也可以使用 ScheduledExecutorService,和上面的方式是一样的效果。
package com.kz.example.task;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@Configuration
public class MyConfig {
@Bean
public ScheduledExecutorService scheduledExecutorService() {
return Executors.newScheduledThreadPool(10);
}
}
2.2.3 基于注解设定多线程定时任务
基于 @EnableAsync 注解设定多线程定时任务,在定时任务所属的类上添加 @EnableAsync 注解开启多线程(@EnableAsync也可以添加在springboot的启动类上)。在定时任务方法上添加 @Async 注解开启该方法的多线程,且开启多线程的方法需为public。
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
@Component
@EnableAsync //开启多线程
public class ScheduledTask {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Scheduled(cron = "*/5 * * * * ?")
@Async
public void cron() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedDelay = 5000)
@Async
public void fixedDelay() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
@Scheduled(fixedRate = 5000)
@Async
public void fixedRate() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
TimeUnit.SECONDS.sleep(6);
}
}
2.3 cron表达式
cronExpression定义时间规则,Cron表达式由6或7个空格分隔的时间字段组成:秒 分钟 小时 日期 月份 星期 年(可选)
- 表达式生成网站: http://cron.qqe2.com/
字段 | 允许值 | 允许的特殊字符 |
---|---|---|
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 | , - * / |
星期 | 1-7 | , - * ? / L C # |
年 | 1970-2099 | , - * / |
,
:表示列出枚举值,例如分钟使用5,20,则表示5和20分钟各执行一次
-
:表示范围,例如分钟使用5-20,表示5-20分钟每分钟触发一次
*
:表示匹配该域任意值,例如分钟使用 * ,表示每分钟都会执行一次
/
:表示起始时间开始触发,以后每隔多长时间触发一次,例如秒使用0/3,表示从0开始触发,后每三分钟触发一次
?
:只能在日和星期使用,表示匹配任意值,但实际不会;因为日和星期可能会存在冲突,如果想表示每月20号0点执行,则需要写为 0 0 0 20 * ?,星期位必须写为?,虽然概念上 * 也表示通配
L
:表示最后,只出现在日和星期;例如在星期的5L,表示最后一个星期五触发
W
:表示有效工作日(周一-周五),只出现在日,如果指定的当天在某月刚好为周末,则就近取周五或周一执行
LW
:连用表示每个月最后一个星期五,只在日使用
#
:用于确定第几个星期的星期几,只在星期使用;例如2#3,表示在每月的第三个星期一
常用的cron表达式有:
0/3 * * * * ? :表示每三秒钟执行一次
0 0 2 1 * ? * :表示在每月 1 日的凌晨 2 点执行
0 15 10 ? * MON-FRI :表示周一到周五每天上午 10:15 执行
0 15 10 ? * 6#3 :每月的第三个星期五上午10:15触发
0 15 10 ? 6L 2019-2020 :表示 2019-2020 年的每个月的最后一个星期五上午 10:15 执行
0 0 10,14,16 * * ? :每天上午 10 点,下午 2 点,4 点执行
0 0/30 9-17 * * ? :朝九晚五工作时间内每半小时执行
0 0 12 ? * WED :表示每个星期三中午 12 点执行
0 0 12 * * ? :每天中午 12点执行
0 15 10 ? * * :每天上午 10:15 执行
0 15 10 * * ? :每天上午 10:15 执行
0 15 10 * * ? * :每天上午 10:15 执行
0 15 10 * * ? 2019 :2019 年的每天上午 10:15 执行
3、@Schedule 原理
在 SpringBoot 中,我们使用 @EnableScheduling 来启用 @Schedule。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}
EnableScheduling 注解需要注意 import 了 SchedulingConfiguration。SchedulingConfiguration 一看名字就知道是一个配置类,是为了添加相应的依赖类。
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}
}
可以看到在 SchedulingConfiguration 创建了一个 ScheduledAnnotationBeanPostProcessor。看样子 SpringBoot 定时任务的核心就是 ScheduledAnnotationBeanPostProcessor 类了,下面我们来看一看 ScheduledAnnotationBeanPostProcessor 类。
ScheduledAnnotationBeanPostProcessor
public class ScheduledAnnotationBeanPostProcessor
implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
}
可以看到 ScheduledAnnotationBeanPostProcessor 实现了很多接口,这里重点关注 2 个,ApplicationListener 和 DestructionAwareBeanPostProcessor。
DestructionAwareBeanPostProcessor 封装任务
DestructionAwareBeanPostProcessor 继承了 BeanPostProcessor。BeanPostProcessor的作用就是在 Bean 创建时执行 setter 之后,在自定义的 afterPropertiesSet 和 init-method 前后提供拦截点,大致执行的先后顺序是:
Bean 实例化 -> setter -> BeanPostProcessor#postProcessBeforeInitialization ->
-> InitializingBean#afterPropertiesSet -> init-method -> BeanPostProcessor#postProcessAfterInitialization
ScheduledAnnotationBeanPostProcessor 的 postProcessAfterInitialization 方法如下:
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
bean instanceof ScheduledExecutorService) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
});
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass);
if (logger.isTraceEnabled()) {
logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
}
}
else {
// Non-empty set of methods
annotatedMethods.forEach((method, scheduledMethods) ->
scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isTraceEnabled()) {
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}
流程简单描述:找到所有的 Schedule 方法,把它封装为 ScheduledMethodRunnable 类 (ScheduledMethodRunnable 类实现了 Runnable 接口),并把其做为一个任务注册到 ScheduledTaskRegistrar 中。
ApplicationListener 执行任务
通过 BeanPostProcessor 解析出了所有的任务,接下来要做的事情就是提交任务了。
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
// Running in an ApplicationContext -> register tasks this late...
// giving other ContextRefreshedEvent listeners a chance to perform
// their work at the same time (e.g. Spring Batch's job registration).
finishRegistration();
}
}
ScheduledAnnotationBeanPostProcessor 监听的事件是 ContextRefreshedEvent,就是在容器初始化,或者刷新的时候被调用。监听到 ContextRefreshedEvent 事件之后,值调用了 finishRegistration 方法,这个方法的基本流程如下:
- 找到容器中的 SchedulingConfigurer,并调用它的 configureTasks,SchedulingConfigurer 的作用主要就是配置 ScheduledTaskRegistrar 类,例如线程池等参数,例如:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}
- 调用 ScheduledTaskRegistrar 的 afterPropertiesSet 方法执行任务,如果对具体的逻辑感兴趣,可以阅读 ScheduledTaskRegistrar 的 scheduleTasks 方法。
评论区