Scheduled 定时任务 - @EnableScheduling、TaskScheduler 与 ScheduledAnnotationBeanPostProcessor 源码
概述
Spring Framework 从 3.0 开始提供了基于注解的定时任务支持,通过 @Scheduled 和 @EnableScheduling 两个核心注解,开发者可以快速声明式地定义定时任务。底层依赖 TaskScheduler 接口体系进行任务调度,由 ScheduledAnnotationBeanPostProcessor 完成注解解析和任务注册。
本文基于 Spring Framework 5.3.x 源码,深入分析 Spring Scheduled 定时任务的完整实现机制,涵盖注解体系、调度器接口、Cron 表达式解析、动态任务注册、分布式防重复执行以及实战案例。
1. @EnableScheduling 注解
@EnableScheduling 是 Spring 定时任务的入口注解,标注在配置类上即可启用定时任务支持。
1.1 注解定义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
}核心动作是 @Import(SchedulingConfiguration.class),将 SchedulingConfiguration 导入 Spring 容器。
1.2 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 Bean。该 Bean 的 postProcessAfterInitialization 方法是整个定时任务机制的核心入口。
2. @Scheduled 注解的三种模式
@Scheduled 注解支持三种任务触发模式:Cron 表达式、固定延迟(fixedDelay) 和 固定频率(fixedRate)。
2.1 注解定义
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}2.2 三种模式详解
| 模式 | 属性 | 说明 |
|---|---|---|
| Cron | cron | 基于 Cron 表达式触发,支持 - 表示禁用 |
| 固定延迟 | fixedDelay / fixedDelayString | 上次执行结束后间隔指定毫秒再次执行 |
| 固定频率 | fixedRate / fixedRateString | 上次执行开始后间隔指定毫秒再次执行 |
示例代码:
@Component
public class ScheduledTaskDemo {
// 模式一:Cron 表达式,每天凌晨 2 点执行
@Scheduled(cron = "0 0 2 * * ?")
public void runByCron() {
System.out.println("Cron 任务执行: " + LocalDateTime.now());
}
// 模式二:固定延迟,上次执行完后 5 秒再执行
@Scheduled(fixedDelay = 5000)
public void runByFixedDelay() {
System.out.println("FixedDelay 任务执行: " + LocalDateTime.now());
}
// 模式三:固定频率,每 5 秒执行一次(无视上次是否完成)
@Scheduled(fixedRate = 5000)
public void runByFixedRate() {
System.out.println("FixedRate 任务执行: " + LocalDateTime.now());
}
// 组合 initialDelay:启动后延迟 10 秒,之后每 5 秒执行
@Scheduled(fixedRate = 5000, initialDelay = 10000)
public void runWithInitialDelay() {
System.out.println("带初始延迟的任务执行: " + LocalDateTime.now());
}
}3. TaskScheduler 接口体系
Spring 提供了完整的任务调度接口体系,核心接口是 TaskScheduler。
3.1 接口层次结构
TaskScheduler (顶级接口)
↑
ConcurrentTaskScheduler (基于 java.util.concurrent.ScheduledExecutorService 的简单实现)
↑
ThreadPoolTaskScheduler (Spring 推荐的完整实现,继承 ExecutorConfigurationSupport)3.2 TaskScheduler 接口
public interface TaskScheduler {
// 基于 Date 触发一次
ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
// 基于 Trigger 触发器(如 CronTrigger)
ScheduledFuture<?> schedule(Runnable task, Date startTime);
// 固定延迟
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay);
// 固定频率
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period);
}3.3 ConcurrentTaskScheduler
基于 ScheduledExecutorService 的简单实现,通常由 Spring Boot 自动配置在未检测到其他 TaskScheduler Bean 时使用。
3.4 ThreadPoolTaskScheduler
Spring 推荐的完整实现,内部维护一个 ScheduledThreadPoolExecutor,支持线程池配置和优雅关闭。
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 线程池大小
scheduler.setThreadNamePrefix("scheduled-"); // 线程名前缀
scheduler.setWaitForTasksToCompleteOnShutdown(true); // 优雅关闭
scheduler.setAwaitTerminationSeconds(60); // 等待任务完成的最大秒数
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
scheduler.initialize();
return scheduler;
}
}3.5 ScheduledTaskRegistrar
ScheduledTaskRegistrar 是任务注册的中心协调者,它收集所有待注册的定时任务,并在 afterPropertiesSet() 方法中将任务委托给 TaskScheduler 执行。
public class ScheduledTaskRegistrar implements ScheduledTaskHolder, InitializingBean {
private TaskScheduler taskScheduler;
private ScheduledExecutorService localExecutor;
private List<TriggerTask> triggerTasks; // Cron 类型任务
private List<CronTask> cronTasks;
private List<IntervalTask> fixedRateTasks; // FixedRate 类型任务
private List<IntervalTask> fixedDelayTasks; // FixedDelay 类型任务
private List<ScheduledTask> scheduledTasks; // 已注册的任务
@Override
public void afterPropertiesSet() {
scheduleTasks();
}
protected void scheduleTasks() {
// 获取 TaskScheduler,没有则创建默认的
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
// 注册 Cron 任务
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
// 注册 FixedRate 任务
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
// 注册 FixedDelay 任务
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}4. CronExpression 解析
4.1 Cron 表达式的 6 位与 7 位格式
Spring 的 CronExpression 类(Spring 5.3 引入,替代旧的 CronSequenceGenerator)支持标准 Cron 表达式格式。
| 位置 | 字段 | 6 位格式 | 7 位格式 |
|---|---|---|---|
| 1 | 秒 (Second) | 0-59 | 0-59 |
| 2 | 分 (Minute) | 0-59 | 0-59 |
| 3 | 时 (Hour) | 0-23 | 0-23 |
| 4 | 日 (Day-of-Month) | 1-31 | 1-31 |
| 5 | 月 (Month) | 1-12 or JAN-DEC | 1-12 or JAN-DEC |
| 6 | 周 (Day-of-Week) | 1-7 or SUN-SAT | 1-7 or SUN-SAT |
| 7 | 年 (Year) | — | 1970-2099 |
示例:
// 6 位格式:每天凌晨 2 点(秒 分 时 日 月 周)
0 0 2 * * ?
// 7 位格式:加上年份
0 0 2 * * ? 2025
// 禁用任务(Spring 特有支持)
@Scheduled(cron = "-")
public void disabledTask() {
// 此任务不会被注册
}4.2 特殊字符支持
| 字符 | 含义 | 示例 |
|---|---|---|
* | 所有值 | * 表示每一秒/每一分 |
? | 不指定值(仅日和周用) | 0 0 2 ? * * 不指定具体日 |
- | 范围 | 10-15 表示 10 到 15 |
, | 枚举 | MON,WED,FRI 表示周一、三、五 |
/ | 步进 | 0/5 * * * * ? 每隔 5 秒 |
L | 最后(仅日和周) | L 表示本月最后一天;6L 表示最后一周的周五 |
W | 最近工作日(仅日) | 15W 表示 15 号最近的工作日 |
# | 第 N 个星期几(仅周) | 3#2 表示本月第二个周二 |
- | 禁用任务(Spring 扩展) | cron = "-" 表示该任务不注册 |
4.3 CronExpression 源码解析
public final class CronExpression implements Serializable {
private final String expression;
private final TimeZone timeZone;
private final List<CronField> fields;
// 解析入口
public static CronExpression parse(String expression) {
// 内部使用 CronParser 进行解析
return new CronExpression(expression);
}
// 计算下一次执行时间
public ZonedDateTime next(ZonedDateTime dateTime) {
// 从当前时间开始,逐字段进位推算下一次匹配时间
ZonedDateTime next = dateTime;
for (CronField field : this.fields) {
next = field.nextOrSame(next);
if (next == null) {
return null;
}
}
return next;
}
}Cron 表达式的解析流程:
parse()方法将表达式字符串拆分为各个字段- 每个字段解析为对应的
CronField对象(如SecondsField、MinutesField等) - 调用
next(ZonedDateTime)时,从秒字段开始逐层向后进位推算,得到下一个匹配的时间点
4.4 特殊字符 L / W / # 的解析逻辑
CronField 的子类在匹配时实现特殊字符逻辑:
- L(Last):
DayOfWeekField中,L匹配该月的最后一个指定星期几;DayOfMonthField中匹配该月最后一天 - W(Weekday):
DayOfMonthField中,如果指定日期是周末,则向前/向后取最近的工作日 - #(Nth):
DayOfWeekField中,3#2表示"本月的第二个星期二"
5. 初始延迟、fixedDelay 与 fixedRate 的区别与执行时序差异
5.1 概念对比
| 特性 | fixedDelay | fixedRate |
|---|---|---|
| 计时起点 | 上次执行结束后 | 上次执行开始后 |
| 是否等待执行完成 | 是(串行) | 否(可能并发) |
| 任务堆积 | 不会堆积 | 若执行时间 > 周期,会堆积 |
| 适用场景 | 需要确保上次完成再执行 | 需要严格维持执行频率 |
5.2 执行时序图解
fixedDelay(5000):
|---[任务A 耗时3s]---|----延迟5s----|---[任务B 耗时3s]---|----延迟5s----|
↑ 0s ↑ 3s ↑ 8s ↑ 11s ↑ 16s
fixedRate(5000):
|---[任务A 耗时3s]---|----间隔2s----|---[任务B 耗时3s]---|-间隔2s-|
↑ 0s ↑ 3s ↑ 5s ↑ 8s ↑ 10s
fixedRate(5000) - 任务超时场景:
|---[任务A 耗时7s]---|----|---[任务B 立刻执行]---|----
↑ 0s ↑ 7s ↑ 10s ↑ 17s
(本该 5s 时触发 B,但 A 未完成,B 推迟到 10s 执行)5.3 源码层面的执行差异
在 ThreadPoolTaskScheduler 底层,fixedRate 和 fixedDelay 被映射到 ScheduledThreadPoolExecutor 的不同方法:
// fixedRate -> scheduleAtFixedRate
ScheduledFuture<?> future = executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.MILLISECONDS);
// fixedDelay -> scheduleWithFixedDelay
ScheduledFuture<?> future = executor.scheduleWithFixedDelay(task, initialDelay, delay, TimeUnit.MILLISECONDS);ScheduledThreadPoolExecutor 的内部实现:
- scheduleAtFixedRate:下次执行时间 = 上次任务开始时间 + period,如果上次未完成则等待
- scheduleWithFixedDelay:下次执行时间 = 上次任务结束时间 + delay
5.4 initialDelay 的作用
initialDelay 可应用于任意模式,表示容器启动后延迟多长时间首次执行:
@Scheduled(fixedDelay = 5000, initialDelay = 10000)
public void delayedTask() {
// 容器启动 10 秒后第一次执行,之后每次结束后 5 秒再次执行
}6. 动态定时任务实现
在某些场景下,定时任务的执行周期需要在运行时动态调整(例如从数据库读取配置),此时无法使用固定的 @Scheduled 注解,需要通过以下两种方式实现。
6.1 实现 SchedulingConfigurer 接口
@Configuration
@EnableScheduling
public class DynamicSchedulingConfig implements SchedulingConfigurer {
@Autowired
private TaskConfigRepository taskConfigRepository;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 设置自定义的 TaskScheduler
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("dynamic-scheduled-");
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
// 从数据库读取配置后动态注册任务
List<TaskConfig> configs = taskConfigRepository.findAll();
for (TaskConfig config : configs) {
taskRegistrar.addCronTask(
() -> executeDynamicTask(config),
config.getCronExpression()
);
}
}
private void executeDynamicTask(TaskConfig config) {
System.out.println("执行动态任务: " + config.getTaskName()
+ ",时间: " + LocalDateTime.now());
}
}6.2 编程式注册 ScheduledTaskRegistrar
更灵活的方式是直接注入 ScheduledTaskRegistrar 并在运行时动态添加/取消任务:
@Component
public class DynamicTaskManager {
@Autowired
private ScheduledTaskRegistrar taskRegistrar;
private final Map<String, ScheduledTask> taskMap = new ConcurrentHashMap<>();
/**
* 动态添加一个 Cron 任务
*/
public void addCronTask(String taskId, Runnable task, String cronExpression) {
CronTask cronTask = new CronTask(task, cronExpression);
ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);
if (scheduledTask != null) {
taskMap.put(taskId, scheduledTask);
}
}
/**
* 动态取消任务
*/
public void cancelTask(String taskId) {
ScheduledTask scheduledTask = taskMap.get(taskId);
if (scheduledTask != null) {
scheduledTask.cancel();
taskMap.remove(taskId);
}
}
/**
* 动态添加 FixedRate 任务
*/
public void addFixedRateTask(String taskId, Runnable task, long interval) {
IntervalTask intervalTask = new IntervalTask(task, interval, 0);
ScheduledTask scheduledTask = taskRegistrar.scheduleFixedRateTask(intervalTask);
if (scheduledTask != null) {
taskMap.put(taskId, scheduledTask);
}
}
}6.3 基于数据库配置的动态调度示例
@Component
public class DatabaseDrivenScheduler {
@Autowired
private DynamicTaskManager taskManager;
@Autowired
private TaskConfigRepository repository;
private static final String TASK_PREFIX = "dynamic-task-";
/**
* 每分钟检查数据库配置是否有变更
*/
@Scheduled(fixedRate = 60000)
public void refreshTasks() {
List<TaskConfig> configs = repository.findAll();
for (TaskConfig config : configs) {
String taskId = TASK_PREFIX + config.getId();
if (config.isEnabled()) {
// 添加或更新任务
taskManager.addCronTask(taskId,
() -> executeTask(config),
config.getCronExpression());
} else {
// 禁用任务
taskManager.cancelTask(taskId);
}
}
}
private void executeTask(TaskConfig config) {
System.out.println("执行数据库配置的任务: " + config.getTaskName());
}
}7. 分布式锁防重复执行
在微服务或集群部署场景下,同一任务会在多个节点上同时执行,导致重复处理。需要通过分布式锁确保同一时刻只有一个节点执行任务。
7.1 基于 Redisson 的分布式锁实现
@Component
public class DistributedScheduledTask {
@Autowired
private RedissonClient redissonClient;
private static final String LOCK_KEY_PREFIX = "scheduler:lock:";
/**
* 使用分布式锁包装定时任务
*/
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReconciliation() {
String lockKey = LOCK_KEY_PREFIX + "dailyReconciliation";
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,等待 0 秒,锁持有时间 30 秒
boolean locked = lock.tryLock(0, 30, TimeUnit.SECONDS);
if (!locked) {
// 其他节点已获取锁,当前节点跳过执行
System.out.println("其他节点正在执行对账任务,当前节点跳过");
return;
}
// 执行业务逻辑
System.out.println("开始执行对账任务,节点: "
+ InetAddress.getLocalHost().getHostName()
+ ",时间: " + LocalDateTime.now());
doReconciliation();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private void doReconciliation() {
// 对账业务逻辑
System.out.println("执行对账...");
}
}7.2 基于 Redis 原生 SETNX 的分布式锁(无 Redisson)
@Component
public class RedisDistributedScheduledTask {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY_PREFIX = "scheduler:lock:";
private static final long LOCK_TTL_SECONDS = 30;
@Scheduled(cron = "0 0 2 * * ?")
public void dailyTask() {
String lockKey = LOCK_KEY_PREFIX + "dailyTask";
// 使用 SET NX EX 原子命令获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, getNodeId(), LOCK_TTL_SECONDS, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
System.out.println("获取锁成功,执行任务,节点: " + getNodeId());
doBusinessLogic();
} finally {
// 使用 Lua 脚本确保原子性删除
releaseLock(lockKey);
}
} else {
System.out.println("获取锁失败,其他节点正在执行");
}
}
private String getNodeId() {
return "node-" + UUID.randomUUID().toString().substring(0, 8);
}
private void releaseLock(String lockKey) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
getNodeId()
);
}
private void doBusinessLogic() {
System.out.println("执行业务逻辑...");
}
}7.3 封装为注解 + AOP
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedScheduledLock {
String key(); // 锁的 key
long waitTime() default 0; // 等待时间
long leaseTime() default 30; // 锁持有时间
TimeUnit timeUnit() default TimeUnit.SECONDS;
}@Aspect
@Component
public class DistributedScheduledLockAspect {
@Autowired
private RedissonClient redissonClient;
@Around("@annotation(distributedScheduledLock)")
public void aroundScheduledTask(ProceedingJoinPoint pjp,
DistributedScheduledLock distributedScheduledLock) throws Throwable {
String lockKey = "scheduler:lock:" + distributedScheduledLock.key();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(
distributedScheduledLock.waitTime(),
distributedScheduledLock.leaseTime(),
distributedScheduledLock.timeUnit()
);
if (!locked) {
System.out.println("获取分布式锁失败,跳过执行: " + lockKey);
return;
}
try {
pjp.proceed();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}使用方式:
@Component
public class OrderReconciliationTask {
@Scheduled(cron = "0 0 2 * * ?")
@DistributedScheduledLock(key = "orderReconciliation", leaseTime = 60)
public void reconcile() {
System.out.println("执行订单对账...");
}
}8. 源码分析:ScheduledAnnotationBeanPostProcessor
ScheduledAnnotationBeanPostProcessor 是整个定时任务机制的枢纽。以下基于 Spring Framework 5.3.x 源码分析其完整处理流程。
8.1 类层次与职责
public class ScheduledAnnotationBeanPostProcessor
implements BeanPostProcessor, // Bean 后置处理器
MergedBeanDefinitionPostProcessor, // Bean 定义合并处理器
DestructionAwareBeanPostProcessor, // 销毁回调
Ordered, // 排序
EmbeddedValueResolverAware, // 占位符解析
BeanNameAware, // Bean 名称感知
BeanFactoryAware, // BeanFactory 感知
ApplicationContextAware, // ApplicationContext 感知
SmartInitializingSingleton { // 单例 Bean 初始化后回调
public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = "taskScheduler";
private Object scheduler;
private ScheduledTaskRegistrar registrar;
private final Map<Object, Set<ScheduledTask>> scheduledTasks = new ConcurrentHashMap<>(16);
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
}8.2 完整处理流程
阶段一:postProcessAfterInitialization —— 解析 @Scheduled 注解
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 1. 跳过自身的内部 Bean
if (bean instanceof AopInfrastructureBean) {
return bean;
}
// 2. 获取目标 Bean 的 Class(处理 AOP 代理情况)
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (targetClass == null) {
return bean;
}
// 3. 处理 @Scheduled 注解的方法
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() ? null : scheduledAnnotations;
});
if (annotatedMethods.isEmpty()) {
return bean;
}
// 4. 遍历每个注解方法,解析并注册任务
for (Map.Entry<Method, Set<Scheduled>> entry : annotatedMethods.entrySet()) {
Method method = entry.getKey();
for (Scheduled scheduled : entry.getValue()) {
// 核心处理:解析注解 -> 创建 task -> 注册到 registrar
processScheduled(scheduled, method, bean);
}
}
return bean;
}阶段二:processScheduled —— 解析注解属性并创建任务
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
try {
// 1. 创建 Runnable 任务包装
Runnable runnable = createRunnable(bean, method);
// 2. 记录是否需要设置调度器
boolean processedSchedule = false;
String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required";
// 3. 收集所有任务
Set<ScheduledTask> tasks = new LinkedHashSet<>();
// 4. 解析 initialDelay
long initialDelay = scheduled.initialDelay();
String initialDelayString = scheduled.initialDelayString();
if (StringUtils.hasText(initialDelayString)) {
initialDelay = parseDelay(initialDelayString);
}
// 5. 处理 Cron 模式
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
String zone = scheduled.zone();
// 关键:cron 表达式为 "-" 时禁用任务
if (ScheduledTaskRegistrar.CRON_DISABLED.equals(cron)) {
logger.debug("Cron 表达式被设置为 '-', 任务不会注册");
continue;
}
// 解析 cron 表达式
CronExpression expression = CronExpression.parse(cron);
TimeZone timeZone = StringUtils.hasText(zone) ? TimeZone.getTimeZone(zone) : null;
// 创建 CronTask 并注册
tasks.add(this.registrar.scheduleCronTask(
new CronTask(runnable, new CronTrigger(expression, timeZone, cron))));
processedSchedule = true;
}
// 6. 处理 FixedDelay 模式
long fixedDelay = scheduled.fixedDelay();
if (fixedDelay >= 0) {
tasks.add(this.registrar.scheduleFixedDelayTask(
new IntervalTask(runnable, fixedDelay, initialDelay)));
processedSchedule = true;
}
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
tasks.add(this.registrar.scheduleFixedDelayTask(
new IntervalTask(runnable, parseDelay(fixedDelayString), initialDelay)));
processedSchedule = true;
}
// 7. 处理 FixedRate 模式
long fixedRate = scheduled.fixedRate();
if (fixedRate >= 0) {
tasks.add(this.registrar.scheduleFixedRateTask(
new IntervalTask(runnable, fixedRate, initialDelay)));
processedSchedule = true;
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
tasks.add(this.registrar.scheduleFixedRateTask(
new IntervalTask(runnable, parseDelay(fixedRateString), initialDelay)));
processedSchedule = true;
}
// 8. 校验:必须指定一种模式
if (!processedSchedule) {
throw new IllegalArgumentException(errorMessage);
}
// 9. 将任务与 Bean 关联
this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>()).addAll(tasks);
} catch (IllegalArgumentException ex) {
throw new IllegalStateException("处理 @Scheduled 注解异常: " + method, ex);
}
}阶段三:Runnable 创建 —— 支持异常通知
protected Runnable createRunnable(Object target, Method method) {
// 将目标方法包装为 Runnable,自动处理异常
Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
return new ScheduledMethodRunnable(target, invocableMethod) {
@Override
public void run() {
try {
super.run();
} catch (InvocationTargetException ex) {
// 发布异常事件
errorHandler(ex.getTargetException());
} catch (Throwable ex) {
errorHandler(ex);
}
}
private void errorHandler(Throwable t) {
// 发布 ScheduledTaskFailedEvent
publishEvent(new ScheduledTaskFailedEvent(this, t));
}
};
}阶段四:finishRegistration —— 初始化调度器
@Override
public void afterSingletonsInstantiated() {
// SmartInitializingSingleton 回调,在所有单例 Bean 初始化完成后调用
// 1. 找到所有 SchedulingConfigurer 并执行配置
List<SchedulingConfigurer> configurers =
new ArrayList<>(this.beanFactory.getBeansOfType(SchedulingConfigurer.class).values());
AnnotationAwareOrderComparator.sort(configurers);
// 2. 设置 TaskScheduler
if (this.registrar.hasTaskScheduler()) {
// 已有自定义 TaskScheduler
} else if (this.scheduler != null) {
// 使用 @Scheduled 上指定的 scheduler
this.registrar.setTaskScheduler(resolveSchedulerBean(this.scheduler));
} else {
// 查找名为 "taskScheduler" 的 Bean
try {
Object schedulerBean = this.beanFactory.getBean(DEFAULT_TASK_SCHEDULER_BEAN_NAME);
this.registrar.setTaskScheduler(resolveSchedulerBean(schedulerBean));
} catch (NoUniqueBeanDefinitionException ex) {
// 多个 TaskScheduler,取 "taskScheduler"
} catch (NoSuchBeanDefinitionException ex) {
// 没有自定义 TaskScheduler,使用默认的
if (this.registrar.getTaskScheduler() == null) {
// 内部创建 SingleThreadScheduledExecutor
}
}
}
// 3. 调用 SchedulingConfigurer.configureTasks()
for (SchedulingConfigurer configurer : configurers) {
configurer.configureTasks(this.registrar);
}
// 4. 执行注册(将任务提交给 TaskScheduler)
if (this.registrar.getTaskScheduler() == null) {
this.registrar.afterPropertiesSet();
}
}8.3 整体流程图解
Bean 实例化
│
▼
postProcessAfterInitialization
│
├── 获取目标 Class(处理 AOP 代理)
├── 扫描 @Scheduled 注解方法
│
▼
processScheduled (每个 @Scheduled 方法)
│
├── 创建 Runnable(ScheduledMethodRunnable)
├── 解析 cron → CronTask → registrar.scheduleCronTask()
├── 解析 fixedDelay → IntervalTask → registrar.scheduleFixedDelayTask()
└── 解析 fixedRate → IntervalTask → registrar.scheduleFixedRateTask()
│
▼
afterSingletonsInstantiated (SmartInitializingSingleton)
│
├── 执行 SchedulingConfigurer.configureTasks()
└── registrar.afterPropertiesSet() → 将任务提交到 TaskScheduler
│
▼
ThreadPoolTaskScheduler (底层 ScheduledThreadPoolExecutor)
│
├── scheduleAtFixedRate() ← fixedRate 任务
├── scheduleWithFixedDelay() ← fixedDelay 任务
└── schedule() ← cron 任务
│
▼
任务按计划执行9. 实战案例:电商凌晨 2 点对账 + Redisson 分布式锁防重复
9.1 业务背景
电商平台每天凌晨 2 点需要执行对账任务,核对当日订单、支付、退款等数据。系统以集群方式部署在多台服务器上,需要确保同一时刻只有一台服务器执行对账,避免重复处理。
9.2 完整实现
@Component
@Slf4j
public class OrderReconciliationTask {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private ReconciliationService reconciliationService;
private static final String LOCK_KEY = "scheduler:lock:order-reconciliation";
private static final long LOCK_WAIT_TIME = 0; // 不等待,立即获取结果
private static final long LOCK_LEASE_TIME = 300; // 锁持有 300 秒(5 分钟)
/**
* 每天凌晨 2:00 执行订单对账
* 配合分布式锁确保集群中只执行一次
*/
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReconciliation() {
RLock lock = redissonClient.getLock(LOCK_KEY);
boolean acquired = false;
try {
// 尝试获取分布式锁
acquired = lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS);
if (!acquired) {
log.info("对账任务已被其他节点获取锁,当前节点跳过执行");
return;
}
// 获取锁成功,记录执行节点信息
String nodeId = getNodeId();
log.info("节点 [{}] 获取到锁,开始执行对账任务", nodeId);
// 执行对账核心逻辑
executeReconciliation();
log.info("节点 [{}] 对账任务执行完成", nodeId);
} catch (InterruptedException e) {
log.error("对账任务被中断", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
log.error("对账任务执行异常", e);
// 实际生产中可以发送告警通知
} finally {
// 确保只释放当前线程持有的锁
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
log.debug("分布式锁已释放");
}
}
}
/**
* 对账核心逻辑
*/
private void executeReconciliation() {
// 1. 获取对账日期(默认今天,实际场景可配置 T-1)
LocalDate reconciliationDate = LocalDate.now().minusDays(1);
log.info("对账日期: {}", reconciliationDate);
// 2. 查询订单数据
List<Order> orders = orderService.queryOrdersByDate(reconciliationDate);
log.info("查询到订单数量: {}", orders.size());
// 3. 查询支付数据
List<Payment> payments = paymentService.queryPaymentsByDate(reconciliationDate);
log.info("查询到支付记录数量: {}", payments.size());
// 4. 执行对账匹配
ReconciliationResult result = reconciliationService.reconcile(orders, payments);
// 5. 处理对账结果
if (result.hasMismatch()) {
// 记录差异数据
reconciliationService.saveMismatchRecords(result.getMismatchRecords());
log.warn("对账发现 {} 条差异记录,已保存", result.getMismatchRecords().size());
// 发送告警通知(邮件/钉钉/企微)
notificationService.sendReconciliationAlert(result);
}
// 6. 生成对账报表
reconciliationService.generateReport(result, reconciliationDate);
log.info("对账报表已生成,日期: {}", reconciliationDate);
}
private String getNodeId() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
return UUID.randomUUID().toString().substring(0, 8);
}
}
}9.3 配置类
@Configuration
@EnableScheduling
public class ReconciliationSchedulerConfig {
/**
* 配置线程池,避免对账任务阻塞其他定时任务
*/
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("reconciliation-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return scheduler;
}
/**
* Redisson 客户端(实际配置通常在 application.yml 中)
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5);
return Redisson.create(config);
}
}9.4 application.yml
spring:
task:
scheduling:
pool:
size: 5
thread-name-prefix: scheduled-task-
redis:
host: 127.0.0.1
port: 6379
# Redisson 配置
redisson:
single-server-config:
address: "redis://127.0.0.1:6379"
connection-pool-size: 10
connection-minimum-idle-size: 510. 常见问题与最佳实践
10.1 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 定时任务不执行 | 未加 @EnableScheduling | 检查配置类上是否标注了 @EnableScheduling |
| Cron 任务执行多次 | 容器中存在多个 ScheduledAnnotationBeanPostProcessor | 确保只在一个 @Configuration 类上标注 @EnableScheduling |
| 任务执行时间异常 | 默认 SingleThreadScheduledExecutor 只用一个线程 | 配置 ThreadPoolTaskScheduler 并设置合适的 poolSize |
fixedRate 任务阻塞 | 上一个任务未完成导致后续任务等待 | 使用异步执行或确保任务执行时间不超过周期 |
| 集群重复执行 | 每个节点独立调度 | 引入分布式锁机制 |
10.2 最佳实践
始终配置线程池:默认使用
SingleThreadScheduledExecutor,所有任务共享一个线程,容易阻塞。务必自定义ThreadPoolTaskScheduler。区分 fixedRate 和 fixedDelay:
- 任务执行时间固定且较短 →
fixedRate - 任务执行时间不固定且需等待上一轮完成 →
fixedDelay
- 任务执行时间固定且较短 →
Cron 表达式禁用能力:使用
cron = "-"可以在不修改代码的情况下禁用任务,结合配置中心可实现动态启停。异常处理:默认未捕获的异常会导致后续任务不再执行(对于
fixedRate/fixedDelay),务必在任务内部 try-catch。分布式场景必加锁:集群部署时,任何定时任务都应考虑分布式锁,推荐 Redisson
tryLock实现。监控与告警:配合
ScheduledTaskFailedEvent监听器,实现任务执行失败告警。
@Component
public class ScheduledTaskFailedEventListener {
@EventListener
public void handleTaskFailed(ScheduledTaskFailedEvent event) {
String taskName = event.getTask().toString();
Throwable cause = event.getException();
System.err.println("定时任务 [" + taskName + "] 执行失败: " + cause.getMessage());
// 发送告警通知
}
}- 优雅关闭:配置
ThreadPoolTaskScheduler的waitForTasksToCompleteOnShutdown和awaitTerminationSeconds,确保应用关闭时正在执行的任务能够安全结束。
总结
Spring Framework 的定时任务体系以 @EnableScheduling 为入口,通过 ScheduledAnnotationBeanPostProcessor 完成注解解析,将 @Scheduled 标注的方法转化为 CronTask 或 IntervalTask,最终由 TaskScheduler(底层为 ScheduledThreadPoolExecutor)调度执行。
对于集群部署场景,需要结合 Redisson/Redis 分布式锁确保任务不重复执行。通过 SchedulingConfigurer 或编程式 ScheduledTaskRegistrar 可以实现动态任务注册,满足运行时变更调度周期的需求。
掌握 ScheduledAnnotationBeanPostProcessor 的完整处理流程(postProcessAfterInitialization → processScheduled → afterSingletonsInstantiated → registrar.afterPropertiesSet),有助于理解 Spring 定时任务的工作原理,并在实际项目中做出正确的架构决策。