在程序应用中常常会涉及到一些定时任务,如在凌晨等服务低峰期定时执行一些耗时任务,让资源利用实现最大化。在 Spring Boot
中同样也提供定时任务的管理能力,通过注解方式即可实现便捷的定时任务配置。
一、定时任务
1. 注解介绍
通过 @Scheduled
注解即可实现定时任务开启,其作用于方法上,将会根据配置的周期定时执行目标方法,注意需要在项目启动类上添加 @EnableScheduling
注解才能生效。
如下示例中开始了一个定时任务每间隔 10
秒打印一次当前系统时间。
@Component
public class ScheduleTask {
private final Logger logger = LoggerFactory.getLogger(ScheduleTask.class);
/**
* 定时任务,每 10 秒打印一次时间
*/
@Scheduled(fixedRate = 10000)
public void reportCurrentTime() {
logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}
}
2. 注解参数
在上述的示例中在注解中通过参数 fixedRate
设置了任务间隔周期,下面介绍一下其它常用参数。
(1) fixedRate
fixedRate
用于指定定时任务的间隔时间,如之前的示例中每间隔 10
秒打印一次时间。
(2) fixedDelay
fixedDelay
的类似于 fixedRate
,但其计时方式是从上次任务结束后开始。
举个简单的例子,我们创建了一个任务执行完毕需要耗时五分钟,分别使用 fixedRate
和 fixedDelay
创建了两个定时设置每两分钟执行一次。在第一次定时任务触发之后,当过去两分钟后使用 fixedRate
创建的任务将会再次触发,而使用 fixedDelay
创建的任务则需要等待上一次任务执行即三分钟后,然后再等待两分钟后才会触发下一轮。
(3) timeUnit
fixedRate
与 fixedDelay
默认的时间单位都是毫秒,可通过 timeUnit
指定单位从而提高代码的可读性。
如下面的示例中通过 fixedRate
搭配 timeUnit
设置每隔 10
秒打印一次时间。
@Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS)
public void reportTime1() {
logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}
二、Cron解析
1. 基本格式
@Scheduled
注解中除了上述的参数外,还支持通过 cron
表达式来配置周期,通过配置 cron
表达式可实现灵活的周期配置,其通过七个 *
用于控制执行频率。
不同位置
*
代表含义如下:
- 第一个
*
表示秒:取值范围0-59
。- 第二个
*
表示分钟:取值范围0-59
。- 第三个
*
表示小时:取值范围0-23
。- 第四个
*
表示天数:取值范围1-31
。- 第五个
*
表示月份:取值范围1-12
。- 第六个
*
表示每周:取值范围0-6
。- 第七个
*
表示每年,非必填通常省略。
如下示例中通过 cron
表达式设置了每隔 10
秒打印一次时间。
// 每隔 10 秒打印时间
@Scheduled(cron = "0/10 * * * * *")
public void reportCurrentTime() {
logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}
cron
表达式同时其支持使用 ${}
方式从 YML
文件读取配置,示例如下:
// 从配置文件读取配置
@Scheduled(cron = "${scheduled.time}")
public void reportCurrentTime() {
logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}
2. 解析方式
当通过 @Schdule
注解配置后任务将有 Spring
执行统一调度,在 Spring
中提供了 CronTrigger
用于解析 cron
表达式,其通过 nextExecutionTime()
获取下一次执行的时间。
其中 SimpleTriggerContext
用于定义任务的上下文,其包含三个参数 lastScheduledExecutionTime
, lastActualExecutionTime
与 lastCompletionTime
,分别标识上次执行时间、实际执行时间与完成时间。若需要解析计算 cron
表达式两个时间点之间的间隔,可通过连续触发 nextExecutionTime()
计算二者的差值即可。
@Test
public void test1() {
String express = "0 0/5 * * * *";
CronTrigger trigger = new CronTrigger(express);
Date now = new Date();
System.out.println("Current time" + now);
SimpleTriggerContext context = new SimpleTriggerContext(now, now, now);
// 计算下次触发时间
Date nextDate = trigger.nextExecutionTime(context);
System.out.println("Next execution time: " + nextDate);
}
三、源码解读
1. 实现方式
为了更详细的了解 @Scheduled
注解的底层实现,下面通过一个简单的示例来分析其实现原理。这边通过 @Scheduled
注解配置一个每间隔 3000
毫秒打印一次当前时间的定时任务,并在 log.info()
输出该任务执行断点并启动项目。
@Slf4j
@Component
public class TestSchedule {
@Scheduled(fixedRate = 3_000L)
public void demo1() {
log.info("1");
}
}
完成后启动项目在 IDEA
控制台查看堆栈日志,可以看到项目启动后通过 ScheduledAnnotationBeanPostProcessor
的 finishRegistration()
注册并执行的配置了的定时任务。
为了更清楚的了解的代码执行栈,这里先画了一个简易的流程图。当应用启动时,通过 finishRegistration()
方法调用 resolveSchedulerBean()
获取并注册线程池 taskScheduler
实例,然后随后在 afterPropertiesSet()
中调用 scheduleTasks()
方法执行定时任务。
在 finishRegistration()
中执行线程池实例的获取注册,并调用 afterPropertiesSet()
触发定时任务执行。
resolveSchedulerBean()
核心逻辑即获取定时任务线程池实例,通过传入的 TaskScheduler.class
类型从 IOC
容器中获取 taskScheduler
实例,并将该 bean
实例封装为 NamedBeanHolder
对象。
众所周知,IOC
实现核心即为缓存 Map
记录不同 bean
实例,而 resolveNamedBean()
通过 getBeanNamesForType()
获取 bean
实例名称再有 resolveNamedBean()
实例化对象,需要注意 DefaultListableBeanFactory
中对 resolveNamedBean()
实现了重载要进行区别。
2. 线程配置
在上述提到的实例化线程池实例时,其通过 AbstractBeanFactory
的 doGetBean()
获取已经缓存的实例化对。通过断点可以看到返回实例其核心线程数为 1
,也就意味着其为单线程执行任务。
下面通过示例进行验证,创建两个定时任务,在任务一通过 while(true)
循环执行模拟任务耗时,任务二则每间隔一秒执行简单的日志输出。
@Slf4j
@Component
public class TestSchedule {
private final AtomicInteger i = new AtomicInteger(0);
@Scheduled(fixedRate = 3_000L)
public void demo1() {
while (true) {
try {
int data = i.get();
if (data > 100) {
break;
}
log.info(String.valueOf(data));
Thread.sleep(1000L);
} catch (InterruptedException e) {
}
}
i.incrementAndGet();
}
@Scheduled(fixedRate = 1000L)
public void demo2() {
log.info("2");
}
}
启动程序后查看日志可以看到,demo2
在执行一次之后再无任务输出,表明此时的线程资源已被 demo1
所占用,也验证了上述的单线程的结果。
在之前解析源码的时候提到了定时任务的线程池所用的实例名为 taskScheduler
,因此,当程序中涉及到定时任务时,通常手动注入 taskScheduler
线程池实例对象防止线程资源占用的情况。
@Configuration
public class ExecutorConfig {
@Bean("taskScheduler")
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("test-");
// 核心线程数
scheduler.setPoolSize(8);
// 空闲线程等待时间
scheduler.setAwaitTerminationSeconds(60);
// 取消任务时是否将其从队列中删除
scheduler.setRemoveOnCancelPolicy(true);
// 关闭线程池时等待任务执行完毕后
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
}
同样以刚才的代码为例,在添加 taskScheduler
实例后重启程序,可以看到此时的日志输出已经正常打印两个定时任务的信息。
四、任务启停
除了上述提到的定时注解,在 Spring Boot 中同时提供了其它一系列类实现一次性的任务执行。
在 Spring
中提供了 ApplicationRunner
与 CommandLineRunner
两个类可实现系统启动时触发任务,在项目启动时将会触发二者的 run
方法,通常用于系统的热点数据预加载与数据缓存载入。同理也提供了 ApplicationListener
类可项目停止时触发,常用于资源或缓存的释放与清除。
1. ApplicationRunner
新建类并实现 ApplicationRunner
接口,当 Spring
工程启动时将会执行 run()
方法体的内容,方法体内容仅会触发一次。
通常在项目中利用其实现数据预热功能,或者加载缓存数据至内存之中。
@Component
public class RunnerTask1 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
try {
System.out.println(">>> Start from ApplicationRunner, Current time: " + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2. CommandLineRunner
CommandLineRunner
的使用方式与作用效果与 ApplicationRunner
类似,都是在项目启动时执行。
二者区别在于 run()
方法参数不同,ApplicationRunner
的对应参数是对象,其信息更为详尽,而 CommandLineRunner
的参数则为字符数组。
@Component
public class RunnerTask2 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
try {
System.out.println(">>> Start from CommandLineRunner, Current time: " + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. DisposableBean
一个类若实现了 DisposableBean
接口,则当其 bean
对象销毁之前则会触发 destroy()
方法。
@Component
public class TestClass implements DisposableBean {
@Override
public void destroy() throws Exception {
try {
System.out.println(">>> Destroy from DisposableBean, Current time: " + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4. ApplicationListener
ApplicationListener
作用与 ApplicationRunner
相反,其在程序关闭之前将会触发 onApplicationEvent()
方法体中的内容,同样也仅执行一次,通常在其中实现资源的释放与对象的销毁工作。
@Component
public class ShutdownTask implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
try {
System.out.println(">>>Shutdown from ApplicationListener, Current time: " + System.currentTimeMillis());
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
}