@Scheduled定时任务源码:ScheduledAnnotationBeanPostProcessor的完整实现
@Scheduled定时任务源码:ScheduledAnnotationBeanPostProcessor的完整实现
适读人群:使用Spring定时任务的Java开发者,希望深入理解调度机制的工程师 | 阅读时长:约16分钟
开篇故事
有一次我们系统有个数据同步任务,设置的是每5分钟跑一次。结果某天发现任务没有按时执行,日志里完全没有任何输出,就像任务消失了一样。
排查了好久,最终发现是因为上一次任务执行时间超过了5分钟(数据量变大了),而默认情况下@Scheduled(fixedDelay)的下一次执行是在上一次执行完毕之后再等5分钟。这没什么问题。但我们用的是fixedRate,理论上应该每5分钟启动一次,不管上一次是否完成。
问题在于:默认的定时任务线程池只有一个线程。上一次任务还在跑,下一次到时间了但线程被占用,任务就排队等着。等上一次跑完,下一次立即执行,但时间已经偏了。
理解了@Scheduled的执行原理,这类问题就能快速定位和解决。
一、@Scheduled的核心机制
Spring定时任务涉及的核心类:
ScheduledAnnotationBeanPostProcessor:扫描@Scheduled注解,注册定时任务TaskScheduler(默认实现ThreadPoolTaskScheduler):执行定时任务的线程池ScheduledTaskRegistrar:持有所有注册的定时任务CronTask/FixedDelayTask/FixedRateTask:不同调度策略的任务封装
二、源码核心路径解析
2.1 定时任务注册流程
2.2 ScheduledAnnotationBeanPostProcessor.processScheduled
// ScheduledAnnotationBeanPostProcessor.java 第390行(简化)
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);
// 1. 处理initialDelay
long initialDelay = convertToMillis(scheduled.initialDelay(),
scheduled.initialDelayString(), scheduled.timeUnit());
// 2. 处理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 (!Scheduled.CRON_DISABLED.equals(cron)) {
TimeZone timeZone = StringUtils.hasText(zone) ?
TimeZone.getTimeZone(zone) : TimeZone.getDefault();
tasks.add(this.registrar.scheduleCronTask(
new CronTask(runnable, new CronTrigger(cron, timeZone))));
processedSchedule = true;
}
}
// 3. 处理fixedDelay
long fixedDelay = convertToMillis(scheduled.fixedDelay(),
scheduled.fixedDelayString(), scheduled.timeUnit());
if (fixedDelay >= 0) {
// 必须没有其他调度方式
tasks.add(this.registrar.scheduleFixedDelayTask(
new FixedDelayTask(runnable, fixedDelay, initialDelay)));
processedSchedule = true;
}
// 4. 处理fixedRate
long fixedRate = convertToMillis(scheduled.fixedRate(),
scheduled.fixedRateString(), scheduled.timeUnit());
if (fixedRate >= 0) {
tasks.add(this.registrar.scheduleFixedRateTask(
new FixedRateTask(runnable, fixedRate, initialDelay)));
processedSchedule = true;
}
// 至少需要配置一种调度方式
if (!processedSchedule) {
throw new IllegalArgumentException(errorMessage);
}
// 记录到scheduledTasks集合,用于后续取消
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());
}
}2.3 fixedDelay vs fixedRate vs cron的区别
2.4 ThreadPoolTaskScheduler默认配置
// 默认只有1个线程!
// TaskSchedulingAutoConfiguration.java(Spring Boot自动配置)
@Bean
@ConditionalOnMissingBean
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
// TaskSchedulingProperties默认值:
// spring.task.scheduling.pool.size = 1这是很多定时任务问题的根源:单线程意味着所有定时任务串行执行,一个任务耗时过长会影响其他任务。
三、完整代码示例
3.1 生产级定时任务配置
// 配置多线程调度器
@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 配置多线程线程池
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 根据任务数量配置
scheduler.setThreadNamePrefix("scheduled-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
// 或者用yaml配置(Spring Boot 2.1+)
// spring:
// task:
// scheduling:
// pool:
// size: 10
// thread-name-prefix: "sched-"
// shutdown:
// await-termination: true
// await-termination-period: 1m3.2 带动态配置的定时任务
@Component
public class DynamicScheduledTask implements SchedulingConfigurer {
@Autowired
private TaskConfigRepository taskConfigRepository;
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
// 从数据库读取cron表达式,实现动态调度
registrar.addTriggerTask(
this::executeTask, // 任务逻辑
triggerContext -> { // 动态触发器
// 每次触发时都重新从数据库读取cron,实现动态修改
String cron = taskConfigRepository.findCron("data-sync-task");
if (cron == null || cron.isBlank()) {
cron = "0 */5 * * * ?"; // 默认5分钟
}
CronTrigger trigger = new CronTrigger(cron);
return trigger.nextExecutionTime(triggerContext);
}
);
}
private void executeTask() {
log.info("Executing dynamic scheduled task at {}", LocalDateTime.now());
// 实际任务逻辑
}
}3.3 基于@Scheduled的标准写法 + 防止重复执行
@Component
public class DataSyncTask {
private static final Logger log = LoggerFactory.getLogger(DataSyncTask.class);
@Autowired
private DataSyncService dataSyncService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 防止多实例重复执行:使用分布式锁
@Scheduled(cron = "${task.data-sync.cron:0 0 2 * * ?}") // 支持配置文件覆盖
public void dataSyncTask() {
String lockKey = "task:data-sync:lock";
String lockValue = UUID.randomUUID().toString();
// 获取分布式锁(10分钟过期,防止宕机锁不释放)
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 10, TimeUnit.MINUTES);
if (!Boolean.TRUE.equals(locked)) {
log.info("Task is running on another instance, skipping");
return;
}
try {
log.info("Starting data sync task");
long start = System.currentTimeMillis();
dataSyncService.syncAll();
log.info("Data sync completed in {}ms",
System.currentTimeMillis() - start);
} catch (Exception e) {
log.error("Data sync task failed", e);
// 可以发告警
} finally {
// 只有自己加的锁才能释放(Lua脚本原子操作)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
RedisScript.of(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
}四、踩坑实录
坑1:fixedRate任务积压导致OOM
现象:fixedRate任务,任务执行时间偶尔超过间隔时间,积压的任务越来越多,最终OOM。
根因:当使用多线程调度器时,fixedRate任务会在到达触发时间时创建新的执行,不等上一次完成。如果任务耗时持续超过间隔,任务会不断积压。
解决方案:
// 方案1:改用fixedDelay(上次结束后才开始计时)
@Scheduled(fixedDelay = 5000)
public void task() { ... }
// 方案2:使用tryLock防止并发
private final AtomicBoolean running = new AtomicBoolean(false);
@Scheduled(fixedRate = 5000)
public void task() {
if (!running.compareAndSet(false, true)) {
log.warn("Previous task is still running, skip this execution");
return;
}
try {
doTask();
} finally {
running.set(false);
}
}坑2:cron表达式写错却不报错
现象:@Scheduled(cron = "0 0 25 * * ?"),定时任务永远不执行(25点不存在)。
根因:Spring的CronTrigger不会在启动时验证cron表达式的逻辑有效性(只验证格式),非法的时间(如25小时)会导致任务永远找不到下次执行时间。
解决:在单元测试中验证cron表达式:
@Test
void testCronExpression() {
CronTrigger trigger = new CronTrigger("0 0 2 * * ?");
// 验证下次执行时间是否合理
Date nextExecutionTime = trigger.nextExecutionTime(
new SimpleTriggerContext());
assertNotNull(nextExecutionTime);
System.out.println("Next execution: " + nextExecutionTime);
}坑3:定时任务在集群环境重复执行
每个实例都运行自己的定时任务,N个实例会执行N次,数据重复处理。
解决方案按严格程度排序:
- 分布式锁(如上面示例,适合大多数场景)
- 使用ShedLock等专门的分布式调度锁库
- 改用XXL-JOB、Quartz集群等分布式调度框架
坑4:应用关闭时定时任务没有优雅停止
现象:关闭应用时,正在执行的定时任务被强制中断,可能造成数据不一致。
解决:
@Configuration
@EnableScheduling
public class SchedulingConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
// 关键:等待任务完成再关闭
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(120); // 最多等2分钟
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}五、总结与延伸
@Scheduled的核心链路:
ScheduledAnnotationBeanPostProcessor在Bean初始化后扫描@Scheduled方法- 应用启动完成(
ContextRefreshedEvent)后,批量注册到TaskScheduler ThreadPoolTaskScheduler使用JDK的ScheduledExecutorService触发执行
生产建议:
- 永远不要使用默认的单线程调度器,至少配置
spring.task.scheduling.pool.size=5 - 集群环境必须加分布式锁,推荐使用ShedLock(
@SchedulerLock注解) - cron表达式要在测试环境验证,不要只是猜
下一篇聊AbstractRoutingDataSource多数据源动态切换原理。
