@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():同类直调,代理不介入 → 异步失效
}

规避手段:

  1. 拆分:把需增强的方法放到另一个 Bean,跨 Bean 调用;
  2. 自注入:注入自身代理(@Lazy 自引用或 ObjectProvider);
  3. AopContext:@EnableAspectJAutoProxy(exposeProxy = true) 后 AopContext.currentProxy() 调用;
  4. 经验法则:需要代理增强(事务/异步/缓存)的方法,永远通过外部引用调用。

常见追问 / 记忆点

  • 追问:@Async 和 @Scheduled 能叠加吗?答:能,定时任务触发异步执行;注意两个线程池别共用导致饥饿。
  • 追问:为什么自调用会失效而 Spring 不自动处理?答:注解增强发生在代理层,this 指向原始对象,框架无法拦截内部直调。
  • 记忆点:Scheduled 交给调度器、Async 交给线程池;代理增强皆需外部调用,自调用=白写注解。
笔记加载中…