@Scheduled/@Async 的原理是什么?自调用失效陷阱如何避免?
结论先行:@Scheduled 由 ScheduledAnnotationBeanPostProcessor 在 Bean 初始化后把方法注册给 TaskScheduler 周期性触发;@Async 则由 AOP 代理把方法调用提交到 TaskExecutor 异步执行。二者都隐含“容器接管”,而 @Async/@Transactional/@Cacheable 都依赖代理——同类内 this.method() 自调用不经过代理,注解即失效,这是最高频的坑。
一、@Scheduled 原理与要点
@EnableScheduling // 开启定时任务
@Component
public class Job {
@Scheduled(cron = "0 */5 * * * ?") // 每 5 分钟
public void sync() { ... }
@Scheduled(fixedDelay = 30000) // 上次结束后 30s 再执行
public void clean() { ... }
}
- 默认使用单线程调度器:上一个任务没跑完,下一个不会并行(fixedDelay 更稳妥);
- 任务在线程池排队/执行,长任务会阻塞后续触发 → 生产配置 ThreadPoolTaskScheduler(spring.task.scheduling.pool.size);
- 异常处理:任务方法内异常默认被吞掉(打印日志),需自己捕获监控;
- 集群部署注意:多实例会重复执行,配合分布式锁/单实例调度保证幂等。
二、@Async 原理与配置
@EnableAsync
@Async("businessExecutor") // 指定线程池
public CompletableFuture<String> asyncWork() { ... }
- 底层:AsyncAnnotationBeanPostProcessor 生成代理,调用被包装成任务提交 TaskExecutor;
- Boot 默认提供 applicationTaskExecutor(ThreadPoolTaskExecutor),可调 spring.task.execution.pool.*;
- 返回类型:void 或 Future/CompletableFuture,同步返回值的语义会被改变;
- 异常:void 方法异常进 AsyncUncaughtExceptionHandler,需自定义处理器统一记录。
三、自调用失效陷阱
@Service
public class BizService {
@Async
public void a() { } // 走代理,生效
public void b() { a(); } // this.a():同类直调,代理不介入 → 异步失效
}
规避手段:
- 拆分:把需增强的方法放到另一个 Bean,跨 Bean 调用;
- 自注入:注入自身代理(@Lazy 自引用或 ObjectProvider);
- AopContext:@EnableAspectJAutoProxy(exposeProxy = true) 后 AopContext.currentProxy() 调用;
- 经验法则:需要代理增强(事务/异步/缓存)的方法,永远通过外部引用调用。
常见追问 / 记忆点
- 追问:@Async 和 @Scheduled 能叠加吗?答:能,定时任务触发异步执行;注意两个线程池别共用导致饥饿。
- 追问:为什么自调用会失效而 Spring 不自动处理?答:注解增强发生在代理层,this 指向原始对象,框架无法拦截内部直调。
- 记忆点:Scheduled 交给调度器、Async 交给线程池;代理增强皆需外部调用,自调用=白写注解。