【Spring】Spring Task详解
概念
Spring Task 是 Spring 框架中用于实现任务调度的一个模块,它基于标准的 Java 定时任务机制(如 ScheduledExecutorService),提供了更加简洁和强大的功能。与 Quartz 等复杂的调度框架相比,Spring Task 更加轻量级,适合于中小型项目或简单的定时任务需求。
@Scheduled基本使用
@Scheduled 是 Spring Task 中的核心注解,用于实现定时逻辑。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
// cron 表达式
String cron() default "";
// 时区
String zone() default "";
// 固定延迟执行
long fixedDelay() default -1;
String fixedDelayString() default "";
// 固定频率执行
long fixedRate() default -1;
/**
* Execute the annotated method with a fixed period between invocations.
* <p>The time unit is milliseconds by default but can be overridden via
* {@link #timeUnit}.
* @return the period as a String value — for example, a placeholder
* or a {@link java.time.Duration#parse java.time.Duration} compliant value
* @since 3.2.2
*/
String fixedRateString() default "";
// 延迟启动
long initialDelay() default -1;
String initialDelayString() default "";
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
1、固定延迟执行
import org.springframework.scheduled.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MyTask {
@Scheduled(fixedDelay = 5000) // 每次任务完成后等待 5 秒再执行下一次
public void taskWithFixedDelay() {
System.out.println("任务执行:fixedDelay");
}
}
2、固定频率执行
@Scheduled(fixedRate = 5000) // 每隔 5 秒执行一次
public void taskWithFixedRate() {
System.out.println("任务执行:fixedRate");
}
3、延迟启动
@Scheduled(initialDelay = 10000, fixedRate = 5000) // 启动后延迟 10 秒,然后每隔 5 秒执行一次
public void taskWithInitialDelay() {
System.out.println("任务执行:initialDelay");
}
4、cron 表达式
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
public void taskWithCron() {
System.out.println("任务执行:cron");
}
需要指出的是,使用 @Scheduled 注解的方法,没有形参和返回值:
定时任务的设计初衷是执行一些独立的、无状态的操作,例如:
- 定时清理缓存
- 定时生成报表
- 定时发送邮件
这些操作通常依赖于内部的状态或全局资源(如数据库、文件系统),而不是通过方法参数传递数据。
定时任务的执行结果通常有以下几种处理方式:
- 直接输出日志 :将结果打印到控制台或写入日志文件。
- 更新状态 :将结果保存到数据库或其他持久化存储中。
- 触发其他操作 :通过事件机制或消息队列通知其他模块。
由于这些处理方式都不需要返回值,因此 @Scheduled 方法通常被设计为 void 类型。
cron表达式
cron 表达式是一个字符串,分为 6 或 7 个域。每个域代表一个含义,以空格隔开,有如下两种语法格式:
秒 分 时 每月第几天 月份 周几 年份
秒 分 时 每月第几天 月份 周几
1、秒、分、时
该域可以出现,- * /
和0-59
的整数
*
:表示匹配该域的任意值,在秒域中使用 * 表示每秒钟都会出发,
:表示列出枚举值,在秒域中使用5,20
表示在 5 秒和 20 秒各触发一次-
:表示范围,在秒域使用5-20
,表示从 5 秒到 20 秒每秒触发一次/
:表示起始时间开始触发,然后每隔固定时间触发一次,在秒域中使用0/5
,表示第 0 秒触发一次,5 秒、10 秒……各触发一次。
2、日期:DayOfMonth
该域中可以出现,- * / ? L W C
8 个字符,以及1-31
的整数。
- c:表示和当前日期相关联,如果在该域使用
5c
,则在执行当天的 5 日后执行,且每月的那天都会执行。比如执行日是 10 号,则每月的 15 号都会触发。 - L:表示最后,在该域使用 L,表示每月的最后一天触发。
- W:表示工作日,在该域用 15W,表示在最接近本月第 15 天的工作日出发,如果 15 号是周六则 14 号触发;如果 15 号是周日则 16 号出发;如果 15 号是周二,则 15 号触发。 注:该用法只会在当前月计算,不会到下月触发,比如在该域用 31W,31 号是周日,那么在 29 号触发,而不是下月 1 号。在该域用 LW,表示这个月的最后一个工作日触发。
?
:在无法确定是哪一天时使用。
3、月份
该域中可出现,- * /
四个字符,以及1-12
的整数或JAN-DEC
的单词缩写。
4、星期:DayOfWeek
可出现,- * / ? L # C
8 个字符,以及1-7
的整数或SUN-SAT
单词的缩写,1 代表星期天,7 代表周六。
#
:# 前面代表星期几,#后面代表一个月的第几周。如5#3
表示一个月第三周的星期四。
5、年份
,- * /
4 个字符,以及1970~2099
的整数,该域可以省略,表示每年都会触发。
源码分析
Spring Task 使用 ScheduledThreadPoolExecutor 定义工作线程,默认是单线程的,如果在项目中有多个定时任务,可以配置为多个核心线程。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class TaskConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置线程池大小为 10
scheduler.setThreadNamePrefix("TaskScheduler-");
return scheduler;
}
}
Spring Task 的任务调度机制主要包括以下几个步骤:
- 扫描任务 :通过 ScheduledAnnotationBeanPostProcessor 扫描带有 @Scheduled 注解的方法。
- 解析任务 :解析注解中的属性(如 cron、fixedRate、fixedDelay),生成对应的任务对象。
- 注册任务 :将任务对象注册到 TaskScheduler 中。
- 执行任务 :调度器根据任务规则安排任务的执行。
一、任务扫描
通过 ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization 实现。
ScheduledAnnotationBeanPostProcessor 实现了 BeanPostProcessor 接口,在 Spring 容器完成初始化 Bean 后会扫描所有带有 @Scheduled 注解的方法。
在 ScheduledAnnotationBeanPostProcessor 类中,postProcessAfterInitialization 方法负责扫描 Bean 中的 @Scheduled 注解:
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> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : 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, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isTraceEnabled()) {
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}
说明:
- ①:如果 Bean 是 AopInfrastructureBean、TaskScheduler 或 ScheduledExecutorService 类型,则直接返回,不做进一步处理。这是因为这些类型的 Bean 通常是 Spring 内部使用的基础设施组件,不需要额外处理。
- ②:使用 AnnotationUtils.isCandidateClass 判断目标类是否可能包含 @Scheduled 或 @Schedules 注解。
- 使用 MethodIntrospector.selectMethods 遍历目标类的所有方法。
- 对于每个方法,使用 AnnotatedElementUtils.getMergedRepeatableAnnotations 提取 @Scheduled 或 @Schedules 注解。
- 如果方法上存在注解,则将其存入 annotatedMethods 映射表中。
- ③:如果没有找到任何带有 @Scheduled 注解的方法,则将目标类加入 nonAnnotatedClasses 集合,避免下次重复扫描。
- ④:如果找到了符合条件的方法,则对每个方法及其注解调用 processScheduled 方法进行解析。
二、任务解析
通过 ScheduledAnnotationBeanPostProcessor#processScheduled 实现。
解析有两个工作:
- 负责解析 @Scheduled 注解中定义的任务规则(如 cron 表达式、fixedRate、fixedDelay 等),并封装为 Trigger,表示下一次要执行的时间。
- 将扫描到的定时方法通过反射得到 Method 类对象,并封装到 Runnable 的实现类中,重写 run 方法,在任务调度器执行任务时,调用定时方法。
将 Trigger 和 Runnable 封装为任务,注册到任务调度器中。
解析 @Scheduled 注解的 4 种属性:延迟启动 initialDelay、cron 表达式、固定延迟 fixedDelay、固定频率 fixedRate。
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
Runnable runnable = createRunnable(bean, method);
boolean processedSchedule = false;
String errorMessage =
"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
// ①:解析延迟启动 initialDelay
long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {
Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
if (this.embeddedValueResolver != null) {
initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
}
if (StringUtils.hasLength(initialDelayString)) {
try {
initialDelay = convertToMillis(initialDelayString, scheduled.timeUnit());
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
}
}
}
// ② 解析 cron 表达式
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
if (this.embeddedValueResolver != null) {
cron = this.embeddedValueResolver.resolveStringValue(cron);
zone = this.embeddedValueResolver.resolveStringValue(zone);
}
if (StringUtils.hasLength(cron)) {
Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone;
if (StringUtils.hasText(zone)) {
timeZone = StringUtils.parseTimeZoneString(zone);
}
else {
timeZone = TimeZone.getDefault();
}
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
}
}
}
// At this point we don't need to differentiate between initial delay set or not anymore
if (initialDelay < 0) {
initialDelay = 0;
}
// ③:解析固定延迟 fixedDelay
long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
if (fixedDelay >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
if (this.embeddedValueResolver != null) {
fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
}
if (StringUtils.hasLength(fixedDelayString)) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
try {
fixedDelay = convertToMillis(fixedDelayString, scheduled.timeUnit());
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
}
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
}
// ④:解析固定频率 fixedRate
long fixedRate = convertToMillis(scheduled.fixedRate(), scheduled.timeUnit());
if (fixedRate >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
if (this.embeddedValueResolver != null) {
fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
}
if (StringUtils.hasLength(fixedRateString)) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
try {
fixedRate = convertToMillis(fixedRateString, scheduled.timeUnit());
}
catch (RuntimeException ex) {
throw new IllegalArgumentException(
"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
}
tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
}
// Check whether we had any attribute set
Assert.isTrue(processedSchedule, errorMessage);
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks);
}
}
catch (IllegalArgumentException ex) {
throw new IllegalStateException(
"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
}
}
以解析 cron 表达式为例,解析出 cron 表达式内容、带有 @Scheduled 注解的方法后,将反射得到的注解方法 Method 类的对象和当前的 Bean 封装到 Runnable 接口实现类 ScheduledMethodRunnable 中:
Runnable runnable = createRunnable(bean, method);
protected Runnable createRunnable(Object target, Method method) {
Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
return new ScheduledMethodRunnable(target, invocableMethod);
}
public ScheduledMethodRunnable(Object target, Method method) {
this.target = target;
this.method = method;
}
重写 run 方法:通过反射设置 @Scheduled 注解方法的可访问性,然后调用方法。当定时任务调度器(如 TaskScheduler)执行这个 Runnable 时,实际上会调用 ScheduledMethodRunnable.run() 方法,从而触发目标方法的执行。
@Override
public void run() {
try {
ReflectionUtils.makeAccessible(this.method);
this.method.invoke(this.target);
}
catch (InvocationTargetException ex) {
ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
}
catch (IllegalAccessException ex) {
throw new UndeclaredThrowableException(ex);
}
}
三、任务注册
通过 ScheduledTaskRegistrar 实现。
ScheduledTaskRegistrar 负责管理所有的调度任务,并将其注册到 TaskScheduler 中。
protected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}
说明:
- 如果没有显式配置任务调度器 TaskScheduler,会默认使用单线程的 ScheduledThreadPoolExecutor。如果项目中有多个定时任务,最好配置为多线程的。
其中,使用 LinkedHashSet 存储任务,以保证任务的顺序执行。
private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);
LinkedHashSet 底层基于 LinkedHashMap 实现,LinkedHashMap 通过以下两种数据结构结合实现了高效的存储和有序性维护:
- 哈希表 :用于快速查找、插入和删除操作。
- 双向链表 :用于维护元素的插入顺序。
具体来说:
- 哈希表 :与 HashMap 类似,LinkedHashMap 使用数组 + 链表/红黑树的结构存储键值对。
- 双向链表 :每个键值对节点除了存储在哈希表中外,还会被链接到一个双向链表中,用于记录插入顺序。
四、任务执行
通过 TaskScheduler 实现。
TaskScheduler 负责实际的任务调度和执行,Spring 默认使用 ThreadPoolTaskScheduler 实现。
在 ThreadPoolTaskScheduler 类中,schedule 方法负责根据任务规则安排任务的执行:
@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
ScheduledExecutorService executor = getScheduledExecutor();
return new ReschedulingRunnable(task, trigger, executor).schedule();
}
- 触发器机制:Trigger 接口定义了任务的触发规则(如 Cron 表达式或固定间隔)。
- 任务执行:调度器会根据触发器的规则安排任务的执行时间,并在指定时间调用 Runnable 的 run 方法。
具体来说,ReschedulingRunnable 会使用 Trigger 接口的 nextExecutionTime 方法计算任务的下一次执行时间,并记录时间差。然后,由 ScheduledThreadPoolExecutor 中的线程在指定时间后执行任务。
总结
- 任务扫描:先扫描带有 @Scheduled 注解的方法
- 任务解析:扫描到的方法被分为两部分,方法通过反射封装到 Runnable 中,作为任务在任务调度器执行任务时被调用;注解信息如 cron 表达式被封装为 Trigger,在调度器执行任务时会通过 Trigger 计算任务下次执行的时间。两者组装为一个 Task。
- 任务注册:ScheduledTaskRegistrar 会将任务 Task 保存到一个 LinkedHashSet,以保证任务执行顺序。
- 任务执行:任务调度器 TaskScheduler,通过 Trigger 获取任务下次执行的时间,然后通过 ScheduledThreadPoolExecutor 线程池中的线程执行任务,会调用 Runnable 中重写 run 方法,也就是执行定时方法。注意,任务调度器默认使用单线程的 ScheduledThreadPoolExecutor,如果项目中有多个定时任务,可以配置为多线程的。
优点&适用场景
优点:
- 简单易用 Spring Task 提供了注解驱动的开发方式,无需复杂的配置即可快速实现定时任务。
- 轻量级 不依赖外部库,直接集成在 Spring 框架中,适合中小型项目。
- 灵活的调度规则 支持多种调度方式,包括固定延迟、固定频率和 Cron 表达式。
缺点
- 功能有限 相较于 Quartz 等专业调度框架,Spring Task 的功能较为基础,不支持分布式任务调度。
- 单机限制 Spring Task 默认运行在单个 JVM 中,无法直接支持分布式环境下的任务调度。
适用场景
- 小型项目 对于不需要复杂调度逻辑的小型项目,Spring Task 是一个很好的选择。
- 简单的定时任务 如定时清理缓存、定期生成报表、定时发送邮件等。