Spring Framework 7.0.9 深度剖析-16-异步与调度
16.1 本章导读
任何一个稍微上规模的 Spring 应用,背后都跑着两类"看不见的线程活动":一类是业务方法希望"提交了就立刻返回、别挡着主流程"的异步执行 (@Async),另一类是"每天凌晨三点跑一次对账""每隔 5 秒拉一次状态"的定时调度 (@Scheduled)。这两件事在 Spring Framework 里共用同一套线程抽象底座,却走两条完全独立的装配与拦截链路。本章要做的,就是把这两条链路从注解一路拆到 java.util.concurrent 的线程池上,回答下面这些"资深开发者真正会被绊住"的问题:
- 为什么标了
@Async却还是同步执行了?为什么同一个类里自己调用自己的@Async方法就不生效? @Async方法的返回类型为什么只能是void、Future、CompletableFuture?如果方法抛异常,调用方为什么什么都收不到?- Spring 到底用哪个线程池跑
@Async?"默认线程池"是怎么一步步找出来的?@Async("orderExecutor")里那个字符串是怎么解析成一个Executorbean 的? ThreadPoolTaskExecutor的corePoolSize、maxPoolSize、queueCapacity、keepAliveSeconds、threadNamePrefix这些属性,最终是怎么被塞进java.util.concurrent.ThreadPoolExecutor的?queueCapacity=0为什么就变成了SynchronousQueue?@Scheduled(cron="0 * * * * ?")从注解上的那串字符,到某个具体的未来时刻被触发,中间经过了哪些对象?CronExpression.next()是怎么"算"出下一次触发时间的?为什么是 6 段而不是 5 段?- 为什么不配置任何线程池,所有
@Scheduled任务会挤在同一个线程里串行跑?一个慢任务为什么会"饿死"其他任务? - Spring 容器关闭时,正在跑的任务是被直接打断还是等它跑完?
waitForTasksToCompleteOnShutdown和awaitTerminationSeconds到底控制什么?
本章涉及的模块与核心类(路径均以仓库 spring-framework-7.0.9 为准):
- spring-core :
org.springframework.core.task包下的TaskExecutor、AsyncTaskExecutor、TaskDecorator、SimpleAsyncTaskExecutor、VirtualThreadTaskExecutor、TaskRejectedException。 - spring-context :
org.springframework.scheduling包下的TaskScheduler、Trigger、TriggerContext;org.springframework.scheduling.concurrent包下的ExecutorConfigurationSupport、ThreadPoolTaskExecutor、ThreadPoolTaskScheduler、ReschedulingRunnable;org.springframework.scheduling.annotation包下的@EnableAsync、@EnableScheduling、@Async、@Scheduled、AsyncConfigurationSelector、ProxyAsyncConfiguration、AsyncAnnotationBeanPostProcessor、AsyncAnnotationAdvisor、AnnotationAsyncExecutionInterceptor、SchedulingConfiguration、ScheduledAnnotationBeanPostProcessor;org.springframework.scheduling.config包下的ScheduledTaskRegistrar、CronTask、FixedRateTask、FixedDelayTask、ScheduledTask、TaskSchedulerRouter;org.springframework.scheduling.support包下的CronExpression、CronTrigger、CronField、BitsCronField、TaskUtils。 - spring-aop :
org.springframework.aop.interceptor包下的AsyncExecutionAspectSupport、AsyncExecutionInterceptor、AsyncUncaughtExceptionHandler、SimpleAsyncUncaughtExceptionHandler。
需要的前置知识:J.U.C 的 ThreadPoolExecutor、ScheduledThreadPoolExecutor、BlockingQueue、RejectedExecutionHandler 的基本语义;Spring AOP 的 Advisor/MethodInterceptor/BeanPostProcessor 代理机制;@Configuration + @Import 的装配方式。读完本章,你应当能在脑子里画出"一次 @Async 调用"和"一次 @Scheduled(cron) 触发"的完整调用栈,并能独立排查线上"异步不异步、调度不调度、异常被吞、任务串行"这四类经典事故。
一个先把版本事实钉死的提醒:本章全部结论以 7.0.9 仓库实际代码 为准。7.0.9 已经内置了对 JDK 21 **虚拟线程(Virtual Thread)**的一等支持------VirtualThreadTaskExecutor 真实存在于 spring-core,ExecutorConfigurationSupport.setVirtualThreads(boolean)、ThreadPoolTaskScheduler 也都对接了虚拟线程;调度器解析链在 6.1 之后被重构成了 TaskSchedulerRouter。这些与网上很多旧资料("Spring 不支持虚拟线程""默认调度器就是 Executors.newSingleThreadScheduledExecutor 直接写死在 Registrar 里")的描述并不完全一致,下文会逐一指出差异。
16.2 核心概念
在钻进源码之前,先把本章反复出现的抽象钉清楚。每个概念都给出它所在的接口/类与一句话职责,避免后面"名词对不上代码"。
16.2.1 TaskExecutor:Spring 的任务执行抽象
TaskExecutor(spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java)是 Spring 对"提交一个 Runnable 去执行"这件事的最顶层抽象。它本身就是一个函数式接口,直接继承 JDK 的 java.util.concurrent.Executor:
java
@FunctionalInterface
public interface TaskExecutor extends Executor {
@Override
void execute(Runnable task);
}
它的 javadoc 点明了设计动机:它等价于 JDK 的 Executor,客户端可以声明依赖一个 Executor 而拿到任意 TaskExecutor 实现;之所以保留这个独立接口,主要是为了兼容早期依赖 TaskExecutor 的老 API。注意它抛出的不是 JDK 的 RejectedExecutionException,而是 Spring 自己的 TaskRejectedException(它继承自 RejectedExecutionException,见 spring-core/.../task/TaskRejectedException.java)------这是 Spring 把"拒绝执行"这一异常语义统一收口的关键,后面 ThreadPoolTaskExecutor.execute 里会看到它如何把 JDK 异常包一层再抛。
16.2.2 AsyncTaskExecutor:带 Future 句柄的异步执行器
AsyncTaskExecutor(spring-core/.../task/AsyncTaskExecutor.java)在 TaskExecutor 之上扩展了三个能力:提交 Runnable/Callable 拿 Future、提交拿 CompletableFuture、以及一个已废弃的"启动超时"参数。它声明"实现本接口意味着 execute(Runnable) 一定是异步的------绝不在调用者线程里跑"。7.0.9 里它的 submit(Runnable)、submit(Callable) 都给了默认实现(默认把任务包成 FutureTask 再交给 execute),submitCompletable 则是 6.0 新增的 CompletableFuture 提交入口。
16.2.3 TaskDecorator:任务前后的装饰器
TaskDecorator(spring-core/.../task/TaskDecorator.java)是一个函数式接口,只有一个方法 Runnable decorate(Runnable runnable)。它的用途是在任务真正被执行前,包一层"前后处理"------典型场景是把调用线程的上下文(如 ThreadLocal 里的 MDC 日志上下文、安全上下文)拷贝到异步工作线程里。它是解决"@Async 方法里 ThreadLocal 丢了"这一类问题的官方钩子。
16.2.4 TaskScheduler 与 Trigger:调度的两个抽象
TaskScheduler(spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java)是 Spring 对"按时间调度任务"的抽象,与 TaskExecutor("立刻异步执行")是平行的另一族。它定义了一组 schedule/scheduleAtFixedRate/scheduleWithFixedDelay 方法,返回 ScheduledFuture<?>。Trigger(同包 Trigger.java)则是"下一次该什么时候跑"的策略抽象,核心方法是 Instant nextExecution(TriggerContext triggerContext)------它根据上一次执行的时间上下文,算出下一次触发的时刻;CronTrigger 就是它最常见的实现。
16.2.5 @Async 与 @Scheduled:两个标记型注解
@Async(spring-context/.../scheduling/annotation/Async.java):标在方法(或类)上,表示该方法应被异步执行。只有一个属性value(),用来指定用哪个 Executor(qualifier/bean 名)。它不是通过某个Runner直接生效的,而是靠 AOP 代理生效------这是理解后面所有"自调用失效"问题的钥匙。@Scheduled(同包Scheduled.java):标在方法上(方法必须无参),表示该方法要被定时调度。属性有cron、fixedDelay、fixedRate、initialDelay、zone、timeUnit、scheduler等;cron/fixedDelay/fixedRate三者必须恰好指定一个 。它靠ScheduledAnnotationBeanPostProcessor扫描注册,不走 AOP 代理。
16.2.6 默认执行器/调度器的解析约定
Spring 在"用户没显式指定用哪个线程池"时,有一套固定的查找约定。这套约定在 7.0.9 里对异步和调度分别是:
- 异步默认 Executor :先按类型找唯一的
TaskExecutorbean;找不到/找到多个,再按名字找名为taskExecutor的Executorbean;都找不到,就现场 new 一个SimpleAsyncTaskExecutor。 - 调度默认 TaskScheduler :先按类型找唯一的
TaskSchedulerbean;再按名字找taskScheduler;再退而求其次找ScheduledExecutorService包成ConcurrentTaskScheduler;都没有才现场建一个单线程ScheduledExecutorService。
这两条链分别由 AsyncExecutionAspectSupport.getDefaultExecutor 和 TaskSchedulerRouter.determineDefaultScheduler 实现,源码后面逐段拆。记住"单线程兜底"这个事实 ,它是本章最大的坑------不配置线程池,@Scheduled 就只有一条线程在跑所有定时任务。
16.3 源码剖析(一):TaskExecutor 接口族与执行器适配
16.3.1 TaskExecutor:一个 @FunctionalInterface 的分量
先回到最顶层的接口。TaskExecutor 的完整源码(spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java 第 38-51 行)其实只有一行有效方法:
java
@FunctionalInterface
public interface TaskExecutor extends Executor {
/**
* Execute the given {@code task}.
* <p>The call might return immediately if the implementation uses
* an asynchronous execution strategy, or might block in the case
* of synchronous execution.
* @param task the {@code Runnable} to execute (never {@code null})
* @throws TaskRejectedException if the given task was not accepted
*/
@Override
void execute(Runnable task);
}
逐句读这个接口的 javadoc 设计:
- "Execute the given task" ------唯一的契约就是"把这个 Runnable 执行掉",至于在哪条线程、同步还是异步、用不用池,统统由实现自己定。这就是它和 JDK
Executor完全对齐的原因:@FunctionalInterface+ 继承Executor,意味着任何一个 JDKExecutor都可以在需要TaskExecutor的地方被适配过来。 - "might return immediately ... or might block" ------这句非常关键。Spring 刻意没有在接口层承诺"一定异步"。
SyncTaskExecutor(同包SyncTaskExecutor.java)就是一个在调用者线程里同步执行 的实现;而SimpleAsyncTaskExecutor、ThreadPoolTaskExecutor才是异步的。"到底是不是异步"这个承诺被下放到了子接口AsyncTaskExecutor身上。 - 抛出
TaskRejectedException而不是 JDK 的RejectedExecutionException------这是 Spring 对异常体系的统一收口。看TaskRejectedException(spring-core/.../task/TaskRejectedException.java)的声明:它extends RejectedExecutionException。也就是说,对 JDK 用户它仍然是个RejectedExecutionException,对 Spring 用户它带了 Spring 的上下文(executor、task字段)。后面ThreadPoolTaskExecutor.execute里会看到它如何把RejectedExecutionException包成TaskRejectedException。
为什么要单独造一个 TaskExecutor 而不直接用 JDK Executor?javadoc 里写得坦白:"This interface remains separate from the standard Executor interface primarily for backwards compatibility "。它诞生于 Spring 2.0,那时 JDK 5 的 Executor 刚出来,Spring 需要一个自己的抽象以在容器里统一管理生命周期;到今天它的价值更多在于"统一的 SPI 契约 + 统一的拒绝异常"。
16.3.2 AsyncTaskExecutor:三态提交与默认实现
AsyncTaskExecutor(spring-core/src/main/java/org/springframework/core/task/AsyncTaskExecutor.java)扩展了 TaskExecutor,它的类级 javadoc 第一句就把语义钉死:"Implementing this interface also indicates that the execute(Runnable) method will not execute the given Runnable in the caller's thread but rather asynchronously in some other thread."------实现了它,就承诺 execute 一定是跨线程异步的。它的核心方法族(第 90-134 行):
java
default Future<?> submit(Runnable task) {
FutureTask<Object> future = new FutureTask<>(task, null);
execute(future);
return future;
}
default <T extends @Nullable Object> Future<T> submit(Callable<T> task) {
FutureTask<T> future = new FutureTask<>(task);
execute(future, TIMEOUT_INDEFINITE);
return future;
}
default CompletableFuture<Void> submitCompletable(Runnable task) {
return CompletableFuture.runAsync(task, this);
}
default <T extends @Nullable Object> CompletableFuture<T> submitCompletable(Callable<T> task) {
return FutureUtils.callAsync(task, this);
}
逐段解读:
submit(Runnable)的默认实现 :把用户任务包成一个FutureTask(task, null)(返回值固定为 null 的FutureTask),然后调execute(future)把这个FutureTask自己作为 Runnable 提交出去。这里有个精妙点------FutureTask既是Future(给调用方拿结果)又是Runnable(给执行器跑),所以"提交任务拿 Future"和"实际跑任务"用的是同一个对象。执行器跑的是future.run(),它内部会执行用户的task并把结果存进 future。submit(Callable):同理,包成FutureTask<T>,注意它调的是execute(future, TIMEOUT_INDEFINITE)------那个已废弃的带超时的execute(Runnable, long)。这个execute(Runnable, long)在 5.3.16 已废弃,默认实现就是直接execute(task)忽略超时。submitCompletable(6.0 新增) :两个重载都用CompletableFuture.runAsync(task, this)/FutureUtils.callAsync(task, this)。注意this传进去了------因为AsyncTaskExecutor本身就是 JDKExecutor,所以CompletableFuture可以直接把它当异步执行器用。这就是为什么@Async方法返回CompletableFuture时,框架能拿到一个真正异步的CompletableFuture。- 两个
TIMEOUT_*常量已废弃 :TIMEOUT_IMMEDIATE=0、TIMEOUT_INDEFINITE=Long.MAX_VALUE都标了@Deprecated(since="5.3.16"),原因写在 javadoc:"the common executors do not support start timeouts"------通用线程池根本不支持"任务必须在多少毫秒内启动"这个语义,所以这条 API 线被整体废弃了。7.0.9 里保留只是为了二进制兼容。
理解这族默认方法的意义在于:一个 AsyncTaskExecutor 实现理论上只需要真正实现 execute(Runnable) 这一个方法,submit/submitCompletable 这套高层方法靠默认方法就全都有了 。这也是为什么 SimpleAsyncTaskExecutor 只重写了 execute 却能当 AsyncTaskExecutor 用。
16.3.3 TaskDecorator:上下文传递的官方钩子
TaskDecorator(spring-core/src/main/java/org/springframework/core/task/TaskDecorator.java)全文就一个抽象方法:
java
@FunctionalInterface
public interface TaskDecorator {
Runnable decorate(Runnable runnable);
}
它的 javadoc 把用途说得很清楚:"The primary use case is to set some execution context around the task's invocation, or to provide some monitoring/statistics for task execution."------围绕任务调用设置执行上下文,或做监控统计。最典型的应用是:业务线程里 MDC(日志追踪 ID)放在 ThreadLocal 里,一旦丢到线程池的工作线程执行,ThreadLocal 就没了。用一个 TaskDecorator 在 decorate 里把当前 MDC 快照抓下来,包进返回的 Runnable,在工作线程 run() 开头设置、结尾清理,就能把日志上下文带过去。
但 javadoc 也埋了一个坑,必须点出来:"Exception handling in TaskDecorator implementations may be limited. Specifically in case of a Future-based operation, the exposed Runnable will be a wrapper which does not propagate any exceptions from its run method."------当你 submit(Callable) 时,传给 decorator 的 Runnable 其实是 FutureTask,FutureTask.run() 吞掉异常(异常存在 future 里等 get()),所以 decorator 里 try/catch 是抓不到业务异常的。这一点在 ThreadPoolTaskExecutor.setTaskDecorator、SimpleAsyncTaskExecutor.setTaskDecorator 的 javadoc 里反复强调。
16.3.4 SchedulingTaskExecutor:执行器与调度器的语义桥梁
顺带一提 SchedulingTaskExecutor(spring-context/.../scheduling/SchedulingTaskExecutor.java),它同时继承 AsyncTaskExecutor 和 Ordered,是个标记性接口------表示"这个执行器适合做短任务、调度场景"。ThreadPoolTaskExecutor、ThreadPoolTaskScheduler、ConcurrentTaskExecutor 都实现了它。它的存在主要是给容器做类型区分:在自动装配默认执行器时,Spring 能识别出"这个 bean 既是 TaskExecutor 又是 SchedulingTaskExecutor"。
这一小节我们看到的是 Spring 异步体系的"接口地基":TaskExecutor 定义契约、AsyncTaskExecutor 承诺异步并提供 Future/CompletableFuture 提交、TaskDecorator 提供上下文钩子。下一节我们看这些属性是如何被一个具体的 ThreadPoolTaskExecutor "翻译"成 JDK ThreadPoolExecutor 的------这是本章的硬核部分。
16.4 源码剖析(二):ExecutorConfigurationSupport------线程池的生命周期总管
ThreadPoolTaskExecutor 和 ThreadPoolTaskScheduler 都不是从零造线程池,而是继承同一个基类 ExecutorConfigurationSupport(spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java)。这个基类负责三件大事:线程名/线程工厂等公共配置、初始化 ExecutorService、容器关闭时的优雅停机。理解了它,两个具体执行器就只剩"怎么 new 底层 ExecutorService"这一个差异点。
16.4.1 类声明与关键字段
先看类声明(第 60-104 行):
java
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean,
SmartLifecycle, ApplicationListener<ContextClosedEvent> {
public static final int DEFAULT_PHASE = Integer.MAX_VALUE / 2;
protected final Log logger = LogFactory.getLog(getClass());
private boolean virtualThreads = false;
private ThreadFactory threadFactory = this;
private boolean threadNamePrefixSet = false;
private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
private boolean acceptTasksAfterContextClose = false;
private boolean waitForTasksToCompleteOnShutdown = false;
private long awaitTerminationMillis = 0;
private int phase = DEFAULT_PHASE;
private @Nullable String beanName;
private @Nullable ApplicationContext applicationContext;
private @Nullable ExecutorService executor;
private @Nullable ExecutorLifecycleDelegate lifecycleDelegate;
private volatile boolean lateShutdown;
}
几个关键设计:
- 继承
CustomizableThreadFactory------它自己就是一个ThreadFactory(threadFactory = this的默认值就来自这里)。也就是说,如果你不单独配ThreadFactory,这个执行器 bean 本身就能当线程工厂用,线程名前缀、优先级这些都由它自己产生。 - 同时实现
InitializingBean+DisposableBean+SmartLifecycle+ApplicationListener<ContextClosedEvent>------这意味着它完整接入了 Spring 容器生命周期:初始化时afterPropertiesSet()、销毁时destroy()、还能响应ContextClosedEvent做"提前软停机"。 - 默认拒绝策略是
AbortPolicy:rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy()。这是 JDK 的默认策略------队列满了直接抛RejectedExecutionException。很多人以为 Spring 默认是"调用者运行"(CallerRunsPolicy),错了,默认是 Abort。 DEFAULT_PHASE = Integer.MAX_VALUE / 2:6.2 新增。这个生命周期相位值很讲究------它比普通SmartLifecycle(相位 0)晚停、比Integer.MAX_VALUE的默认相位早停,目的是让执行器"在大多数组件停掉之后再停,但又留出余地"。javadoc 解释:"putting the typically auto-started executor/scheduler beans into an earlier startup phase and a later shutdown phase while still leaving room for regular Lifecycle components"。
16.4.2 initialize():装配底层 ExecutorService 的模板方法
生命周期的核心是 initialize()(第 298-320 行):
java
public void initialize() {
if (logger.isDebugEnabled()) {
logger.debug("Initializing ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
}
if (!this.threadNamePrefixSet && this.beanName != null) {
setThreadNamePrefix(this.beanName + "-");
}
ThreadFactory factory = (this.virtualThreads ?
new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory() : this.threadFactory);
this.executor = initializeExecutor(factory, this.rejectedExecutionHandler);
this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor);
}
protected abstract ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler);
逐段解读:
- 线程名前缀的自动兜底 :
if (!this.threadNamePrefixSet && this.beanName != null)------如果你没手动设线程名前缀,就用 bean 名加一个-做前缀。这就是为什么你定义一个@Bean("myExecutor") ThreadPoolTaskExecutor,它产生的线程默认叫myExecutor-1、myExecutor-2。这对线上排查线程栈至关重要:给执行器起个有意义的 bean 名,就等于免费获得了可读的线程名。 - 虚拟线程分支 :
this.virtualThreads为 true 时,线程工厂不再是自己,而是new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory()------即把虚拟线程工厂从VirtualThreadTaskExecutor里拿出来用。这就是 6.2 之后setVirtualThreads(true)的底层入口。 - 模板方法
initializeExecutor:真正 newThreadPoolExecutor/ScheduledThreadPoolExecutor的动作下放到子类。基类拿到 factory 和拒绝策略后,统一构造ExecutorLifecycleDelegate(负责 start/stop/isRunning 的暂停恢复语义)。 - 谁调
initialize()?是afterPropertiesSet()(第 291-293 行):
java
@Override
public void afterPropertiesSet() {
initialize();
}
也就是说,只要这个执行器是个 Spring bean(走 InitializingBean),属性全部设好后容器自动调 initialize()。如果你像 @EnableAsync javadoc 里那样手动 new 一个 ThreadPoolTaskExecutor 又忘了调 initialize(),它就不会有底层线程池------javadoc 特意提醒"Add the @Bean annotation ... it is no longer necessary to manually call initialize()"。
16.4.3 shutdown() 与优雅停机的两种模式
容器关闭是这个类最能体现"设计功力"的地方。shutdown()(第 364-379 行):
java
public void shutdown() {
if (logger.isDebugEnabled()) {
logger.debug("Shutting down ExecutorService" + (this.beanName != null ? " '" + this.beanName + "'" : ""));
}
if (this.executor != null) {
if (this.waitForTasksToCompleteOnShutdown) {
this.executor.shutdown();
}
else {
for (Runnable remainingTask : this.executor.shutdownNow()) {
cancelRemainingTask(remainingTask);
}
}
awaitTerminationIfNecessary(this.executor);
}
}
两种模式泾渭分明:
waitForTasksToCompleteOnShutdown=false(默认) :调shutdownNow()------JDK 语义是"拒绝新任务 + 中断正在跑的任务 + 返回队列里还没跑的任务列表"。Spring 拿到这些没跑的任务,逐个cancelRemainingTask(如果是 Future 就cancel(true))。也就是说默认关闭是比较"粗暴"的:正在跑的会被中断,排队的直接丢弃。waitForTasksToCompleteOnShutdown=true:调shutdown()(注意没有Now)------JDK 语义是"拒绝新任务,但已提交的(包括队列里的)全部跑完"。
然后 awaitTerminationIfNecessary(第 399-417 行)才是"优雅等待"的真正开关:
java
private void awaitTerminationIfNecessary(ExecutorService executor) {
if (this.awaitTerminationMillis > 0) {
try {
if (!executor.awaitTermination(this.awaitTerminationMillis, TimeUnit.MILLISECONDS)) {
if (logger.isWarnEnabled()) {
logger.warn("Timed out while waiting for executor" + ...);
}
}
}
catch (InterruptedException ex) {
...
Thread.currentThread().interrupt();
}
}
}
这里要纠正一个常见误解:waitForTasksToCompleteOnShutdown=true 和 awaitTerminationSeconds 是两件事 。前者决定"用 shutdown 还是 shutdownNow"(跑完还是中断),后者决定"容器关闭流程要不要阻塞在这里等多久"。javadoc 里反复强调:如果你设了 waitForTasksToCompleteOnShutdown=true,一定要同时设一个显著大的 awaitTerminationSeconds ,因为队列里所有任务都会被执行,容器关闭流程会一直阻塞到 awaitTerminationSeconds 超时。
16.4.4 ContextClosedEvent:比 DisposableBean 更早的"提前软停机"
7.0.9 还有一条更早的停机路径------onApplicationEvent(ContextClosedEvent)(第 504-517 行):
java
@Override
public void onApplicationEvent(ContextClosedEvent event) {
if (event.getApplicationContext() == this.applicationContext) {
if (this.acceptTasksAfterContextClose || this.waitForTasksToCompleteOnShutdown) {
// Late shutdown without early stop lifecycle.
this.lateShutdown = true;
}
else {
if (this.lifecycleDelegate != null) {
this.lifecycleDelegate.markShutdown();
}
initiateEarlyShutdown();
}
}
}
容器 close() 时会先广播 ContextClosedEvent,此时还没走到各个 bean 的 destroy()。执行器收到这个事件后,如果没有特别声明"关闭后还要接任务",就立刻 initiateShutdown()(底层 executor.shutdown(),停止接新任务但让在跑的跑完)------这叫"early soft shutdown"。这样做的好处是:在容器后续的 coordinated lifecycle stop 阶段(SmartLifecycle.stop),执行器已经不再接新任务了,正在跑的任务有机会在 stop 阶段被等到完成。
而 initiateEarlyShutdown() 在 ThreadPoolTaskExecutor 里被重写了(后面会看到):只有 strictEarlyShutdown=true 才真的提前 shutdown,否则默认是"宽容"的------允许关闭后仍有迟到的任务进来。这是 6.1.4 之后 ThreadPoolTaskExecutor 与 ThreadPoolTaskScheduler 的一个行为差异,后面讲坑时还要 revisit。
16.5 源码剖析(三):ThreadPoolTaskExecutor------Spring 属性到 JDK 线程池的适配
ThreadPoolTaskExecutor(spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java)是 @Async 默认场景下最常用的执行器。它的全部价值就在于一件事:把一组 JavaBean 风格的属性(corePoolSize、maxPoolSize、queueCapacity......)翻译成一个 JDK ThreadPoolExecutor。我们逐字段、逐方法看这个翻译是怎么做的。
16.5.1 属性默认值:一个"伪装成单线程池"的默认配置
先看字段声明(第 86-108 行):
java
private final Object poolSizeMonitor = new Object();
private int corePoolSize = 1;
private int maxPoolSize = Integer.MAX_VALUE;
private int keepAliveSeconds = 60;
private int queueCapacity = Integer.MAX_VALUE;
private boolean allowCoreThreadTimeOut = false;
private boolean prestartAllCoreThreads = false;
private boolean strictEarlyShutdown = false;
private @Nullable TaskDecorator taskDecorator;
private @Nullable ThreadPoolExecutor threadPoolExecutor;
// Runnable decorator to user-level FutureTask, if different
private final Map<Runnable, Object> decoratedTaskMap =
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
把这几个默认值连起来读,会得到一个反直觉的结论:ThreadPoolTaskExecutor 的默认配置 ≈ Executors.newSingleThreadExecutor() ------核心线程 1、最大线程无限、队列容量无限。javadoc 第 50-58 行直接挑明:"The default configuration is a core pool size of 1, with unlimited max pool size and unlimited queue capacity. This is roughly equivalent to Executors.newSingleThreadExecutor()"。
为什么这个默认值很重要?因为 JDK ThreadPoolExecutor 的扩缩容规则是:核心线程没满 → 建新核心线程;核心满了 → 进队列;队列满了 → 建非核心线程到 max;都满了 → 拒绝 。而这里 queueCapacity = Integer.MAX_VALUE,意味着队列(LinkedBlockingQueue)永远不会满------于是 maxPoolSize 那一层永远走不到。所以默认配置下,无论你丢多少任务,都只有 1 个线程在跑,其余全在队列里排队。这就是为什么很多人"以为配了线程池其实是单线程串行"。
decoratedTaskMap 这个字段也值得注意:它用**弱引用(WEAK)**的 ConcurrentReferenceHashMap,把"装饰后的 Runnable"映射回"用户原始任务"。目的在后面 cancelRemainingTask 里会用到------关闭时要通过装饰后的 Runnable 找到用户的 Future 句柄去 cancel。用弱引用是为了不强行持有用户任务,避免内存泄漏。
16.5.2 initializeExecutor:真正 new ThreadPoolExecutor 的那一行
最核心的方法(第 274-313 行):
java
@Override
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
queue, threadFactory, rejectedExecutionHandler) {
@Override
public void execute(Runnable command) {
Runnable decorated = command;
if (taskDecorator != null) {
decorated = taskDecorator.decorate(command);
if (decorated != command) {
decoratedTaskMap.put(decorated, command);
}
}
super.execute(decorated);
}
@Override
protected void beforeExecute(Thread thread, Runnable task) {
ThreadPoolTaskExecutor.this.beforeExecute(thread, task);
}
@Override
protected void afterExecute(Runnable task, Throwable ex) {
ThreadPoolTaskExecutor.this.afterExecute(task, ex);
}
};
if (this.allowCoreThreadTimeOut) {
executor.allowCoreThreadTimeOut(true);
}
if (this.prestartAllCoreThreads) {
executor.prestartAllCoreThreads();
}
this.threadPoolExecutor = executor;
return executor;
}
逐段拆:
createQueue(this.queueCapacity)决定队列类型------这是 queueCapacity 语义的关键,下一小节专门讲。- 七个参数原样塞进 JDK
ThreadPoolExecutor:corePoolSize、maxPoolSize、keepAliveSeconds(注意单位是TimeUnit.SECONDS)、queue、threadFactory、rejectedExecutionHandler。没有任何魔法------Spring 的适配就是把属性值对号入座。这意味着 JDK 线程池的全部语义(扩容顺序、回收逻辑、拒绝策略)在这里完整保留。 - 匿名子类重写
execute:在这里插入TaskDecorator。如果配了 decorator,先把 command 装饰一遍,再把"装饰后→原始"的映射记进decoratedTaskMap,然后super.execute(decorated)。注意 decorator 是在提交时 (调用线程里)执行的,不是在工作线程里------decorate(command)这一步拿得到调用方的 ThreadLocal,这正是它能做上下文传递的原因。 beforeExecute/afterExecute回调桥接回基类 :基类ExecutorConfigurationSupport的beforeExecute/afterExecute负责lifecycleDelegate的 start/stop 计数(实现isRunning()、stop(Runnable)等待任务完成的语义)。这里匿名子类把它桥接出去。- 两个可选开关 :
allowCoreThreadTimeOut(true)允许核心线程空闲超时回收(配合有界队列时实现动态伸缩);prestartAllCoreThreads()启动时就把所有核心线程预先建好,避免第一个任务才冷启动。
16.5.3 createQueue:queueCapacity 语义的真相
队列创建逻辑(第 324-331 行)只有短短几行,却解释了 javadoc 里那句"queueCapacity=0 mimics newCachedThreadPool()":
java
protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
if (queueCapacity > 0) {
return new LinkedBlockingQueue<>(queueCapacity);
}
else {
return new SynchronousQueue<>();
}
}
- queueCapacity > 0 :建一个有界
LinkedBlockingQueue,容量就是 queueCapacity。配合默认 core=1、max=无限,意味着:核心线程 1 个在跑,新来的任务先排进这个有界队列;队列排满了,才会扩到 maxPoolSize;队列还没满就永远只有 1 个线程。 - queueCapacity = 0 :建一个
SynchronousQueue。SynchronousQueue不存任务------每个插入必须等另一个线程立即取走。这就迫使线程池"来一个任务就立刻新建线程接"(因为队列永远"满"着,按 JDK 规则立刻扩到 max),行为等价于newCachedThreadPool。javadoc 提醒:"Setting queueCapacity to 0 mimicsExecutors.newCachedThreadPool(), with immediate scaling of threads in the pool to a potentially very high number. Consider also setting a maxPoolSize at that point"------否则 max 是无限,任务一来线程数可能爆炸。
这就是"队列容量"在 Spring 里的完整语义:正数=有界队列,0=SynchronousQueue(无缓冲直传) 。注意默认是 Integer.MAX_VALUE------一个几乎无界的有界队列,所以默认永远不触发扩容。
16.5.4 execute/submit:把 JDK 异常包成 TaskRejectedException
看对外提交方法(第 381-412 行),三个方法结构完全一致:
java
@Override
public void execute(Runnable task) {
Executor executor = getThreadPoolExecutor();
try {
executor.execute(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
@Override
public Future<?> submit(Runnable task) {
ExecutorService executor = getThreadPoolExecutor();
try {
return executor.submit(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
@Override
public <T> Future<T> submit(Callable<T> task) {
ExecutorService executor = getThreadPoolExecutor();
try {
return executor.submit(task);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
每一个都是同一套模板:拿到底层 threadPoolExecutor,try 调 JDK 方法,catch RejectedExecutionException 后包成 Spring 的 TaskRejectedException(带上 executor 和 task 上下文)再抛。这就是 16.3.1 说的"异常统一收口"在落地------用户永远收到带上下文的 TaskRejectedException,而不是光秃秃的 JDK 异常。
getThreadPoolExecutor()(第 338-341 行)有个 Assert.state(threadPoolExecutor != null, "not initialized")------如果你忘了 initialize 就提交任务,会立刻报"ThreadPoolTaskExecutor not initialized"。
16.5.5 运行时可调:JMX 友好的核心/最大/存活时间
注意 setCorePoolSize/setMaxPoolSize/setKeepAliveSeconds 这几个 setter(第 116-169 行)都长这样:
java
public void setCorePoolSize(int corePoolSize) {
synchronized (this.poolSizeMonitor) {
if (this.threadPoolExecutor != null) {
this.threadPoolExecutor.setCorePoolSize(corePoolSize);
}
this.corePoolSize = corePoolSize;
}
}
它不仅更新自己的字段,如果底层线程池已经存在,还会实时把新值推给 threadPoolExecutor.setCorePoolSize(...)。这就是 javadoc 说的"This setting can be modified at runtime, for example through JMX"------运行时(比如通过 JMX)调整池大小,对正在跑的线程池立即生效。这是 Spring 执行器比裸 JDK 线程池多出来的"可管理性"。
最后看 initiateEarlyShutdown 的重写(第 424-429 行):
java
@Override
protected void initiateEarlyShutdown() {
if (this.strictEarlyShutdown) {
super.initiateEarlyShutdown();
}
}
前面 16.4.4 埋的伏笔在这里收:默认 strictEarlyShutdown=false,所以 ThreadPoolTaskExecutor 在 ContextClosedEvent 时不 提前 shutdown,而是宽容地让迟到任务进入 coordinated lifecycle stop 阶段;只有显式 setStrictEarlyShutdown(true) 才会像 ThreadPoolTaskScheduler 那样在上下文关闭时立刻拒绝新任务。这个差异在"容器优雅停机"那一节还要重点讲。
16.6 源码剖析(四):SimpleAsyncTaskExecutor 与 VirtualThreadTaskExecutor
讲完了"池化"的 ThreadPoolTaskExecutor,现在看它的反面------不池化 的 SimpleAsyncTaskExecutor,以及 7.0.9 里它与虚拟线程的结合。
16.6.1 SimpleAsyncTaskExecutor:每个任务一条新线程
SimpleAsyncTaskExecutor(spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java)的类 javadoc 第一句就警告:"This implementation does not reuse threads! "------它不复用线程 ,每个任务都 new Thread。核心执行方法(第 361-381 行):
java
protected void doExecute(Runnable task) {
newThread(task).start();
}
protected Thread newThread(Runnable task) {
if (this.virtualThreadDelegate != null) {
return this.virtualThreadDelegate.newVirtualThread(nextThreadName(), task);
}
else {
return (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
}
}
doExecute 就是"造一条线程、start"。newThread 里有三个分支:开了虚拟线程就建虚拟线程;配了外部 ThreadFactory 就用它;否则用 CustomizableThreadCreator.createThread(按自己的线程名前缀/优先级建平台线程)。
它的 execute(Runnable, long)(第 311-336 行)是真正的入口,我们逐段看:
java
@Override
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
if (!isActive()) {
throw new TaskRejectedException(getClass().getSimpleName() + " is not active");
}
Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
Future<?> future = (task instanceof Future<?> f ? f : null);
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
this.concurrencyThrottle.beforeAccess();
try {
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
catch (Throwable ex) {
// Release concurrency permit if thread creation fails
this.concurrencyThrottle.afterAccess();
throw new TaskRejectedException("Failed to start execution thread for task: " + task, ex);
}
}
else if (this.activeThreads != null) {
doExecute(new TaskTrackingRunnable(taskToUse, future));
}
else {
doExecute(taskToUse);
}
}
isActive()检查 :关闭后(closed这个AtomicBoolean置 true)直接拒绝,抛TaskRejectedException("... is not active")。- TaskDecorator 先包一层 :和
ThreadPoolTaskExecutor一样,decorate 在提交线程执行。 - 并发限流分支 :
isThrottleActive()(即设了concurrencyLimit)且不是"立即任务"时,先concurrencyThrottle.beforeAccess()拿许可,再doExecute;如果建线程失败,afterAccess()释放许可再抛异常。 - 任务追踪分支 :如果配了
taskTerminationTimeout或cancelRemainingTasksOnClose,activeThreads这个集合非空,任务要包成TaskTrackingRunnable(用来追踪哪些线程在跑、close 时好等/打断它们)。
16.6.2 concurrencyLimit:信号量式并发上限
setConcurrencyLimit(第 246-248 行)委托给内部的 ConcurrencyThrottleAdapter:
java
public void setConcurrencyLimit(int concurrencyLimit) {
this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit);
}
ConcurrencyThrottleAdapter(第 437-461 行)继承 ConcurrencyThrottleSupport,重写了 onLimitReached:
java
@Override
protected void onLimitReached() {
if (rejectTasksWhenLimitReached) {
onAccessRejected("Concurrency limit reached: " + getConcurrencyLimit());
}
super.onLimitReached();
}
这里有个 6.2.6 新增的开关 rejectTasksWhenLimitReached:默认 false------达到并发上限时调用者阻塞等待 (super.onLimitReached() 来自 ConcurrencyThrottleSupport,是个等待许可的逻辑);设为 true 则立刻抛 TaskRejectedException。javadoc 把这个语义讲得很清楚:"the default is false, blocking the caller until the submission can be accepted. Switch this to true for immediate rejection instead."
它和线程池的 maxPoolSize 有本质区别 :SimpleAsyncTaskExecutor 没有任务队列,达到上限时是"阻塞提交者"而不是"任务排队"。javadoc 明确建议:如果你想要"队列缓冲、不阻塞提交者",就用 ThreadPoolTaskExecutor 而不是这里。
16.6.3 优雅关闭:close() 与 TaskTrackingRunnable
close()(第 390-420 行)是 6.1 新增的 AutoCloseable 实现:
java
@Override
public void close() {
if (this.closed.compareAndSet(false, true)) {
Set<Thread> threads = this.activeThreads;
if (threads != null) {
if (this.cancelRemainingTasksOnClose) {
synchronized (threads) {
this.cancelled = true;
}
threads.forEach(Thread::interrupt);
}
if (this.taskTerminationTimeout > 0) {
synchronized (threads) {
try {
if (!threads.isEmpty()) {
threads.wait(this.taskTerminationTimeout);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
this.cancelled = true;
}
if (!this.cancelRemainingTasksOnClose) {
threads.forEach(Thread::interrupt);
}
}
}
}
}
逐段:先用 compareAndSet(false, true) 保证只关一次。cancelRemainingTasksOnClose=true 时先标记 cancelled 并 interrupt 所有活跃线程。taskTerminationTimeout>0 时,在 threads 这个集合上 wait(timeout) 等待------谁来 notify?是 TaskTrackingRunnable.run() 的 finally:每跑完一个任务就把线程从集合移除,若此时 closed 且集合空了就 notify()。这样 close 线程能一直等到所有任务跑完或超时。超时后若没设立即取消,再 interrupt 一次"补刀"。
TaskTrackingRunnable.run()(第 481-507 行)是配套:开头把自己 add 进 activeThreads,finally 里 remove 并 concurrencyThrottle.afterAccess() 释放许可。这就是为什么 javadoc 说"a concrete >0 timeout ... will lead to the wrapping of every submitted task into a task-tracking runnable which involves considerable overhead"------一旦开了终止超时,每个任务都要包一层、每个线程都要登记进集合,开销不小。
16.6.4 VirtualThreadTaskExecutor:7.0.9 的虚拟线程一等公民
现在看任务书特别点名要确认的虚拟线程支持。VirtualThreadTaskExecutor(spring-core/src/main/java/org/springframework/core/task/VirtualThreadTaskExecutor.java)真实存在,全文如下:
java
public class VirtualThreadTaskExecutor implements AsyncTaskExecutor {
private final ThreadFactory virtualThreadFactory;
public VirtualThreadTaskExecutor() {
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory();
}
public VirtualThreadTaskExecutor(String threadNamePrefix) {
this.virtualThreadFactory = new VirtualThreadDelegate().virtualThreadFactory(threadNamePrefix);
}
public final ThreadFactory getVirtualThreadFactory() {
return this.virtualThreadFactory;
}
@Override
public void execute(Runnable task) {
this.virtualThreadFactory.newThread(task).start();
}
}
它的实现极其朴素:内部持一个虚拟线程 ThreadFactory(由 VirtualThreadDelegate 创建,这个类在 spring-core/src/main/java21/ 和 spring-core/src/main/java/ 各有一份,是 JDK21 的虚拟线程工厂的多版本兼容封装),execute 就是"用工厂 new 一条虚拟线程、start"。它只接受线程名前缀这一个配置项 ,javadoc 明说:"The only configuration option is a thread name prefix. For additional features such as concurrency limiting or task decoration, consider using SimpleAsyncTaskExecutor.setVirtualThreads instead."
也就是说 7.0.9 提供了两条虚拟线程路径:
- 直接
new VirtualThreadTaskExecutor("vt-")------最简单,每任务一条虚拟线程,无池、无限流。 SimpleAsyncTaskExecutor.setVirtualThreads(true)------保留了SimpleAsyncTaskExecutor的并发限流、TaskDecorator、终止超时等能力,只是把"new 平台线程"换成"new 虚拟线程"(见newThread()里的virtualThreadDelegate分支)。- 前面
ExecutorConfigurationSupport.initialize()里看到的setVirtualThreads(true)------让ThreadPoolTaskExecutor/ThreadPoolTaskScheduler的底层线程池改用虚拟线程工厂,同时完整保留 Spring 的生命周期管理。
这三条路径分别对应"轻量无管理""要限流装饰但不要池""要完整生命周期管理"三种诉求。虚拟线程时代,"每任务一线程"从缺点变成了优点(虚拟线程极轻量),所以 SimpleAsyncTaskExecutor 的 javadoc 也顺势改口:"Alternatively, on JDK 21, consider setting setVirtualThreads to true."
16.7 源码剖析(五):@EnableAsync 的装配链
执行器讲完了,现在看"标了 @Async 的方法是怎么被代理的"。入口是 @EnableAsync,它本身不做任何执行,只做一件事:@Import(AsyncConfigurationSelector.class)。
16.7.1 @EnableAsync:一个 Import 注解
EnableAsync(spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java)的注解定义(第 164-216 行):
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
Class<? extends Annotation> annotation() default Annotation.class;
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
四个属性:
annotation():自定义哪个注解算"异步标记"。默认是Annotation.class(即不限制,用内置的@Async和 EJB 的@Asynchronous)。可以换成你自己的注解类型。proxyTargetClass():是否强制 CGLIB 代理。默认 false(优先 JDK 动态代理)。javadoc 提醒通常建议依赖全局代理配置,而不是在这里单独设。mode():advice 怎么织入。默认PROXY;可选ASPECTJ(走编译期/加载期织入,连自调用都能拦截)。order():这个 BeanPostProcessor 的顺序,默认LOWEST_PRECEDENCE------刻意排在所有其他后处理器之后 。javadoc 解释:"so that it can add an advisor to existing proxies rather than double-proxy"------为了让它给已经建好的代理(比如@Transactional的代理)再加一层 advisor,而不是叠出双层代理。
16.7.2 AsyncConfigurationSelector:按 mode 二选一
AsyncConfigurationSelector(同包 AsyncConfigurationSelector.java)继承 AdviceModeImportSelector<EnableAsync>,selectImports(第 44-50 行):
java
@Override
public String[] selectImports(AdviceMode adviceMode) {
return switch (adviceMode) {
case PROXY -> new String[] {ProxyAsyncConfiguration.class.getName()};
case ASPECTJ -> new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
};
}
PROXY(默认)→ 导入ProxyAsyncConfiguration(纯类名引用)。ASPECTJ→ 导入"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"(字符串常量,在spring-aspects模块里)。用字符串而不是类引用,是为了让spring-context不硬依赖spring-aspects------只有真用 ASPECTJ 模式时才需要那个 jar 在 classpath。
ASPECTJ 分支意味着:你需要 spring-aspects jar + 编译期/加载期织入,异步逻辑直接织进字节码,没有代理对象、自调用也能拦截。代价是要配 AspectJ 织入环境。绝大多数生产用 PROXY 模式。
16.7.3 AbstractAsyncConfiguration:收集 AsyncConfigurer
ProxyAsyncConfiguration 继承 AbstractAsyncConfiguration,后者负责收集用户实现的 AsyncConfigurer。AbstractAsyncConfiguration(同包 AbstractAsyncConfiguration.java)的关键方法 setConfigurers(第 69-84 行):
java
@Autowired
@SuppressWarnings("NullAway")
void setConfigurers(ObjectProvider<AsyncConfigurer> configurers) {
SingletonSupplier<AsyncConfigurer> configurer = SingletonSupplier.ofNullable(() -> {
List<AsyncConfigurer> candidates = configurers.stream().toList();
if (CollectionUtils.isEmpty(candidates)) {
return null;
}
if (candidates.size() > 1) {
throw new IllegalStateException("Only one AsyncConfigurer may exist");
}
return candidates.get(0);
});
this.executor = adapt(configurer, AsyncConfigurer::getAsyncExecutor);
this.exceptionHandler = adapt(configurer, AsyncConfigurer::getAsyncUncaughtExceptionHandler);
}
逐段:
- 注入所有
AsyncConfigurer类型的 bean。如果一个都没有,executor 和 exceptionHandler 就是"空供应商"。 - 有多个直接报错 :
"Only one AsyncConfigurer may exist"------全局只能有一个异步配置定制。 adapt把"拿到 configurer → 调它的getAsyncExecutor()"包成一个Supplier(延迟求值,见adapt方法第 86-91 行:configurer != null ? provider.apply(configurer) : null)。
为什么用 Supplier/SingletonSupplier 而不是立刻调 getAsyncExecutor()?因为 AsyncConfigurer 里可能 @Bean 了一堆依赖,过早调用会打乱 bean 创建顺序。包成供应商,等到真正需要执行器时才求值。@EnableAsync javadoc 也提醒:"AsyncConfigurer configuration classes get initialized early ... make sure to declare dependencies 'lazy'"。
setImportMetadata(第 56-64 行)则把 @EnableAsync 注解本身的属性读进 AnnotationAttributes enableAsync,供子类用。
16.7.4 ProxyAsyncConfiguration:注册 AsyncAnnotationBeanPostProcessor
最后是 ProxyAsyncConfiguration(同包 ProxyAsyncConfiguration.java)的 asyncAdvisor() bean 方法(第 44-59 行):
java
@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
Assert.state(this.enableAsync != null, "@EnableAsync annotation metadata was not injected");
AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
bpp.configure(this.executor, this.exceptionHandler);
Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
bpp.setAsyncAnnotationType(customAsyncAnnotation);
}
if (this.enableAsync.getBoolean("proxyTargetClass")) {
bpp.setProxyTargetClass(true);
}
bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
return bpp;
}
这就是 @EnableAsync 真正注册到容器的基础设施 bean:一个 AsyncAnnotationBeanPostProcessor,bean 名是 TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME(即 "org.springframework.context.annotation.internalAsyncAnnotationProcessor")。逐行:
bpp.configure(this.executor, this.exceptionHandler)------把AsyncConfigurer提供的执行器/异常处理器供应商塞进去。- 读
@EnableAsync(annotation=...):如果用户自定义了异步注解类型,setAsyncAnnotationType。 - 读
proxyTargetClass、order属性透传给后处理器。
至此装配链收口:@EnableAsync → @Import(AsyncConfigurationSelector) → selectImports 返回 ProxyAsyncConfiguration → 它注册一个 AsyncAnnotationBeanPostProcessor bean 。后面所有 @Async 的拦截,都靠这个 BeanPostProcessor 在 bean 初始化时给目标 bean 织入 advisor。
注意一个细节:这个 @Bean 方法名叫 asyncAdvisor,但返回的是 AsyncAnnotationBeanPostProcessor(一个 BeanPostProcessor),不是 Advisor------名字里的 "Advisor" 只是历史遗留,真正的 advisor 在它内部 setBeanFactory 时才创建(下一节讲)。
16.8 源码剖析(六):AsyncAnnotationBeanPostProcessor 与 AsyncAnnotationAdvisor
上一节装配链最终注册了 AsyncAnnotationBeanPostProcessor。这一节看它和它内部的 AsyncAnnotationAdvisor 是怎么"决定给哪些方法加异步拦截"的。
16.8.1 继承链:AbstractAdvisingBeanPostProcessor
AsyncAnnotationBeanPostProcessor(spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java)的声明:
java
public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
它继承 AbstractBeanFactoryAwareAdvisingPostProcessor,后者又继承 AbstractAdvisingBeanPostProcessor。这个继承链是 Spring AOP 的标准套路:
AbstractAdvisingBeanPostProcessor:实现BeanPostProcessor,在postProcessAfterInitialization里判断 bean 是否匹配 pointcut,匹配就给它包一层代理(ProxyFactory加上持有的advisor)。AbstractBeanFactoryAwareAdvisingPostProcessor:在上面基础上加了"拿到 BeanFactory、跳过 AOP 基础设施 bean 自己"的能力。
也就是说,AsyncAnnotationBeanPostProcessor 的本质是:一个持有 advisor 的 BeanPostProcessor,对所有"带 @Async 的 bean"在初始化后织入代理 。构造函数(第 87-89 行)里 setBeforeExistingAdvisors(true)------把异步 advisor 放在调用链最前面,javadoc 解释:"so that it can add an advisor to existing proxies rather than double-proxy" 类的设计意图是"异步尽可能早地切换线程,避免在原线程上先跑一堆其他 advice"。
16.8.2 setBeanFactory:创建 Advisor 的真正时机
它重写了 setBeanFactory(第 141-151 行):
java
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
if (this.asyncAnnotationType != null) {
advisor.setAsyncAnnotationType(this.asyncAnnotationType);
}
advisor.setBeanFactory(beanFactory);
this.advisor = advisor;
}
注意时序:AsyncAnnotationBeanPostProcessor 自己作为 bean 被容器初始化时,会被回调 setBeanFactory。就在这一刻,它才 new 出真正的 AsyncAnnotationAdvisor ,并把之前 configure 进来的 executor/exceptionHandler 供应商传进去,然后赋给父类的 this.advisor 字段。之后容器每初始化一个业务 bean,父类 postProcessAfterInitialization 就拿这个 advisor 的 pointcut 去匹配,匹配上就代理。
DEFAULT_TASK_EXECUTOR_BEAN_NAME 常量(第 74-75 行)直接转发自 AnnotationAsyncExecutionInterceptor.DEFAULT_TASK_EXECUTOR_BEAN_NAME,即 "taskExecutor"------这是后面默认执行器查找的兜底 bean 名。
16.8.3 AsyncAnnotationAdvisor:advice 与 pointcut 的组装
AsyncAnnotationAdvisor(同包 AsyncAnnotationAdvisor.java)继承 AbstractPointcutAdvisor。它的构造函数(第 94-118 行)同时建好了 advice 和 pointcut:
java
public AsyncAnnotationAdvisor(
@Nullable Supplier<? extends @Nullable Executor> executor, @Nullable Supplier<? extends @Nullable AsyncUncaughtExceptionHandler> exceptionHandler) {
Set<Class<? extends Annotation>> asyncAnnotationTypes = CollectionUtils.newLinkedHashSet(2);
asyncAnnotationTypes.add(Async.class);
ClassLoader classLoader = AsyncAnnotationAdvisor.class.getClassLoader();
try {
asyncAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("jakarta.ejb.Asynchronous", classLoader));
}
catch (ClassNotFoundException ex) {
// If EJB API not present, simply ignore.
}
try {
asyncAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("jakarta.enterprise.concurrent.Asynchronous", classLoader));
}
catch (ClassNotFoundException ex) {
// If Jakarta Concurrent API not present, simply ignore.
}
this.advice = buildAdvice(executor, exceptionHandler);
this.pointcut = buildPointcut(asyncAnnotationTypes);
}
逐段:
- 默认识别三种异步注解 :Spring 自己的
@Async,加上 EJB 3.1 的jakarta.ejb.Asynchronous,再加上 Jakarta Concurrency 的jakarta.enterprise.concurrent.Asynchronous。后两个用ClassUtils.forName+ catchClassNotFoundException------classpath 上没有就静默跳过。这就是为什么你的方法标了 EJB 的@Asynchronous也能被 Spring 异步化。 buildAdvice(第 159-165 行):new 一个AnnotationAsyncExecutionInterceptor(传 null 默认执行器),然后interceptor.configure(executor, exceptionHandler)------把供应商配进去。这个 interceptor 就是真正干活的 advice。buildPointcut(第 172-186 行):为每种注解建 pointcut 再 union。
16.8.4 buildPointcut:类级 + 方法级的组合切点
buildPointcut(第 172-186 行):
java
protected Pointcut buildPointcut(Set<Class<? extends Annotation>> asyncAnnotationTypes) {
ComposablePointcut result = null;
for (Class<? extends Annotation> asyncAnnotationType : asyncAnnotationTypes) {
Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);
if (result == null) {
result = new ComposablePointcut(cpc);
}
else {
result.union(cpc);
}
result = result.union(mpc);
}
return (result != null ? result : Pointcut.TRUE);
}
对每一种异步注解,建两个 AnnotationMatchingPointcut:
cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true)------类级:标了该注解的类。第二个参数true表示"检查目标类(包括接口/类层级)"。mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true)------方法级:标了该注解的方法。
然后 ComposablePointcut.union 把它们合并成"类上有注解 或 方法上有注解"。这正对应 @Async 可以标在类上(该类所有方法异步)或方法上。
这里有个关键设计:pointcut 是方法级匹配 (AnnotationMatchingPointcut(null, annotation, true) 的第二个参数为 null 表示不限制类、只匹配方法)。它匹配的是"方法上直接有 @Async",但因为第三个参数 true(checkInherited),也会处理注解继承/桥接方法的情况。这也是为什么"@Async 标在接口方法上"和"标在实现类方法上"都能被识别。
16.8.5 setBeanFactory:把 BeanFactory 透传给 advice
AsyncAnnotationAdvisor.setBeanFactory(第 140-145 行):
java
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (this.advice instanceof BeanFactoryAware beanFactoryAware) {
beanFactoryAware.setBeanFactory(beanFactory);
}
}
因为 AnnotationAsyncExecutionInterceptor(继承 AsyncExecutionAspectSupport)实现了 BeanFactoryAware,所以 advisor 拿到 BeanFactory 后立刻透传给 advice。这一步至关重要------拦截器需要 BeanFactory 才能在运行时按 qualifier 查找具体的 Executor bean 。AsyncAnnotationBeanPostProcessor.setBeanFactory 里调的 advisor.setBeanFactory(beanFactory) 最终就走到这里。
至此,"代理如何产生"的链条完整了:BeanPostProcessor 在每个 bean 初始化后,用 advisor 的 pointcut(类级/方法级 @Async 匹配)判断要不要代理;要代理就给 bean 加上 AnnotationAsyncExecutionInterceptor 这个 advice。真正的异步执行逻辑全在这个 interceptor 里------下一节是全章的核心。
16.9 源码剖析(七):AsyncExecutionInterceptor------异步执行的心脏
这一节是全章最硬核的部分。一个 @Async 方法被代理拦截后,到底发生了什么?答案在 AsyncExecutionInterceptor.invoke 及其父类 AsyncExecutionAspectSupport。
16.9.1 invoke:拦截后做了什么
AsyncExecutionInterceptor(spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java)实现 AOP Alliance 的 MethodInterceptor。invoke(第 100-128 行)全文:
java
@Override
public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass);
AsyncTaskExecutor executor = determineAsyncExecutor(userMethod);
if (executor == null) {
throw new IllegalStateException(
"No executor specified and no default executor set on AsyncExecutionInterceptor either");
}
Callable<Object> task = () -> {
try {
Object result = invocation.proceed();
if (result instanceof Future<?> future) {
return future.get();
}
}
catch (ExecutionException ex) {
Throwable cause = ex.getCause();
handleError(cause == null ? ex : cause, userMethod, invocation.getArguments());
}
catch (Throwable ex) {
handleError(ex, userMethod, invocation.getArguments());
}
return null;
};
return doSubmit(task, executor, userMethod.getReturnType());
}
逐段精读:
- 解析目标方法 :
AopUtils.getTargetClass拿到被代理的目标类,BridgeMethodResolver.getMostSpecificMethod处理泛型桥接方法(泛型擦除后,桥接方法上可能没有注解,要找到真正的用户方法)。这一步是为了后面按方法查 executor qualifier。 determineAsyncExecutor(userMethod):算出这次调用该用哪个执行器。这是下一小节的重点。算出 null 直接抛异常。- 把整个"真正调用目标方法"包成一个
Callable<Object> task:注意invocation.proceed()在这里------它才是真正执行@Async业务方法的那一步,但它被包进了 Callable,将要被丢到执行器的工作线程里跑 。调用线程走到doSubmit就立刻返回了。 if (result instanceof Future<?> future) return future.get();:这是个很容易看漏的细节。如果业务方法本身返回了一个 Future(比如CompletableFuture),invocation.proceed()返回的就是那个 Future。这里在工作线程里调future.get()阻塞等它完成 ,把结果"摊平"。为什么?因为业务方法为了配合@Async,通常写法是return CompletableFuture.completedFuture(realValue)------一个立刻完成的 Future;真正异步返回给调用方的是框架在doSubmit里新建的 Future。这里future.get()是为了拿到真实返回值。- 异常处理 :工作线程里业务方法抛异常,会从
invocation.proceed()或future.get()里冒出来。ExecutionException拆开取 cause,其他 Throwable 直接拿,都交给handleError。 doSubmit(task, executor, userMethod.getReturnType()):根据声明的返回类型,决定怎么把这个 Callable 提交出去、返回给调用方什么。这是返回类型分支的关键。
16.9.2 determineAsyncExecutor:按方法解析执行器
AsyncExecutionAspectSupport.determineAsyncExecutor(spring-aop/.../interceptor/AsyncExecutionAspectSupport.java 第 168-190 行):
java
protected @Nullable AsyncTaskExecutor determineAsyncExecutor(Method method) {
AsyncTaskExecutor executor = this.executors.get(method);
if (executor == null) {
Executor targetExecutor;
String qualifier = getExecutorQualifier(method);
if (this.embeddedValueResolver != null && StringUtils.hasLength(qualifier)) {
qualifier = this.embeddedValueResolver.resolveStringValue(qualifier);
}
if (StringUtils.hasLength(qualifier)) {
targetExecutor = findQualifiedExecutor(this.beanFactory, qualifier);
}
else {
targetExecutor = this.defaultExecutor.get();
}
if (targetExecutor == null) {
return null;
}
executor = (targetExecutor instanceof AsyncTaskExecutor asyncTaskExecutor ?
asyncTaskExecutor : new TaskExecutorAdapter(targetExecutor));
this.executors.put(method, executor);
}
return executor;
}
逐段:
- 缓存 :
this.executors是个ConcurrentHashMap<Method, AsyncTaskExecutor>------每个方法解析过一次就缓存,下次直接拿。所以 qualifier 解析、bean 查找只发生一次。 getExecutorQualifier(method):抽象方法,由子类AnnotationAsyncExecutionInterceptor实现------就是读@Async("xxx")里的value()。- 占位符解析 :qualifier 可能是
${my.executor}这种占位符,用embeddedValueResolver.resolveStringValue解析。 - 有 qualifier →
findQualifiedExecutor:按 qualifier 名在 BeanFactory 里找 Executor bean。 - 无 qualifier →
this.defaultExecutor.get():用默认执行器(SingletonSupplier,第一次才真正求值)。 - 类型适配 :找到的
targetExecutor如果本身是AsyncTaskExecutor就直接用;否则包一层TaskExecutorAdapter(把裸 JDKExecutor适配成 SpringAsyncTaskExecutor)。
16.9.3 getExecutorQualifier:读 @Async 的 value
子类 AnnotationAsyncExecutionInterceptor.getExecutorQualifier(spring-context/.../annotation/AnnotationAsyncExecutionInterceptor.java 第 83-91 行):
java
@Override
protected @Nullable String getExecutorQualifier(Method method) {
Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class);
if (async == null) {
async = AnnotatedElementUtils.findMergedAnnotation(method.getDeclaringClass(), Async.class);
}
return (async != null ? async.value() : null);
}
先在方法上找 @Async,找不到再在声明类上找。方法级 @Async 的 value 覆盖类级 ------javadoc 强调"Method-level use always overrides any qualifier value configured at the class level"。用 AnnotatedElementUtils.findMergedAnnotation 而不是直接 method.getAnnotation,是为了处理"注解组合"(composed annotation)等元注解场景。
16.9.4 getDefaultExecutor:默认执行器的查找链
这是任务书点名要确认的关键链路。AsyncExecutionAspectSupport.getDefaultExecutor(第 232-268 行):
java
protected @Nullable Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
if (beanFactory != null) {
try {
// Search for TaskExecutor bean... not plain Executor since that would
// match with ScheduledExecutorService as well, which is unusable for
// our purposes here. TaskExecutor is more clearly designed for it.
return beanFactory.getBean(TaskExecutor.class);
}
catch (NoUniqueBeanDefinitionException ex) {
logger.debug("Could not find unique TaskExecutor bean. " +
"Continuing search for an Executor bean named 'taskExecutor'", ex);
try {
return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
}
catch (NoSuchBeanDefinitionException ex2) {
if (logger.isInfoEnabled()) {
logger.info("More than one TaskExecutor bean found within the context, and none is named " +
"'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " +
"as an alias) in order to use it for async processing: " + ex.getBeanNamesFound());
}
}
}
catch (NoSuchBeanDefinitionException ex) {
logger.debug("Could not find default TaskExecutor bean. " +
"Continuing search for an Executor bean named 'taskExecutor'", ex);
try {
return beanFactory.getBean(DEFAULT_TASK_EXECUTOR_BEAN_NAME, Executor.class);
}
catch (NoSuchBeanDefinitionException ex2) {
logger.info("No task executor bean found for async processing: " +
"no bean of type TaskExecutor and no bean named 'taskExecutor' either");
}
// Giving up -> either using local default executor or none at all...
}
}
return null;
}
这条查找链是:
beanFactory.getBean(TaskExecutor.class):按类型找唯一 的TaskExecutorbean。注意注释:为什么找TaskExecutor而不是裸Executor?因为裸Executor会把ScheduledExecutorService也匹配上,而那个不适合跑@Async。- 找到多个
TaskExecutor(NoUniqueBeanDefinitionException)→ 按名字找"taskExecutor"的Executor。 - 一个都没有(
NoSuchBeanDefinitionException)→ 同样按名字找"taskExecutor"。 - 都找不到 → 返回 null。
注意:AsyncExecutionAspectSupport.getDefaultExecutor 返回 null 时,并不会现场建 SimpleAsyncTaskExecutor ------那是子类 AsyncExecutionInterceptor 的事。看子类重写(AsyncExecutionInterceptor.java 第 154-158 行):
java
@Override
protected @Nullable Executor getDefaultExecutor(@Nullable beanFactory) {
Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}
父类链全失败后,子类兜底 new SimpleAsyncTaskExecutor()。这就回答了任务书的问题:7.0.9 没有 "ApplicationTaskExecutor" 这个东西;默认执行器链路是"唯一 TaskExecutor bean → 名为 taskExecutor 的 Executor bean → new SimpleAsyncTaskExecutor()"。很多旧资料说的"默认是 SimpleAsyncTaskExecutor"只在最后这兜底一步成立。
16.9.5 doSubmit:返回类型分支
现在看返回类型怎么决定提交方式。AsyncExecutionAspectSupport.doSubmit(第 278-293 行)全文:
java
protected @Nullable Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
if (CompletableFuture.class.isAssignableFrom(returnType)) {
return executor.submitCompletable(task);
}
else if (Future.class.isAssignableFrom(returnType)) {
return executor.submit(task);
}
else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) {
executor.submit(task);
return null;
}
else {
throw new IllegalArgumentException(
"Invalid return type for async method (only Future and void supported): " + returnType);
}
}
这就是任务书点名的"返回类型分支",逐支:
CompletableFuture:调executor.submitCompletable(task)------返回一个真正异步的CompletableFuture。业务方法里return CompletableFuture.completedFuture(v),框架用这个异步 CompletableFuture 把结果桥接出去。Future(含 ListenableFuture 等) :调executor.submit(task),返回Future<?>。void或 KotlinUnit:executor.submit(task)提交,但不把 Future 返回给调用方(return null)。任务照样在后台跑,但调用方拿不到句柄------这就是"void 方法异常被吞"的根源。- 其他类型 :直接抛
IllegalArgumentException("Invalid return type for async method (only Future and void supported)")。
这就是为什么 @Async 方法的返回类型被严格限制在 void/Future/CompletableFuture/Callable 相关。标了 @Async 的方法返回一个普通对象(比如 String)是非法的------因为调用方拿到的是 null(代理立刻返回),真实返回值在异步线程里,框架没法把它塞回给同步的调用方。
16.9.6 handleError:异常往哪走
handleError(第 307-321 行):
java
protected void handleError(Throwable ex, Method method, @Nullable Object... params) throws Exception {
if (Future.class.isAssignableFrom(method.getReturnType())) {
ReflectionUtils.rethrowException(ex);
}
else {
// Could not transmit the exception to the caller with default executor
try {
this.exceptionHandler.obtain().handleUncaughtException(ex, method, params);
}
catch (Throwable ex2) {
logger.warn("Exception handler for async method '" + method.toGenericString() +
"' threw unexpected exception itself", ex2);
}
}
}
异常分流逻辑:
- 返回类型是 Future/CompletableFuture :
ReflectionUtils.rethrowException(ex)------异常会被塞进 Future,调用方在future.get()时拿到(包成ExecutionException)。 - 返回 void :异常传不回调用方(调用方都已经返回了),只能交给
AsyncUncaughtExceptionHandler。默认实现是SimpleAsyncUncaughtExceptionHandler------只打一条 error 日志:
java
// SimpleAsyncUncaughtExceptionHandler.java 第 38-42 行
@Override
public void handleUncaughtException(Throwable ex, Method method, @Nullable Object... params) {
if (logger.isErrorEnabled()) {
logger.error("Unexpected exception occurred invoking async method: " + method, ex);
}
}
这就是"void 的 @Async 方法抛异常,调用方毫无感知,只在日志里留一行 error"的源码真相。想定制就实现 AsyncUncaughtExceptionHandler 通过 AsyncConfigurer 配进去。
16.10 源码剖析(八):TaskScheduler 接口族与 ThreadPoolTaskScheduler
讲完异步执行,转到调度这一族。它们在结构上和 TaskExecutor 那族高度平行:一个顶层接口(TaskScheduler)+ 一个核心实现(ThreadPoolTaskScheduler,同样继承 ExecutorConfigurationSupport)。
16.10.1 TaskScheduler:调度的顶层契约
TaskScheduler(spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java)定义了六组方法(去掉已废弃的 Date 重载):
java
default Clock getClock() {
return Clock.systemDefaultZone();
}
@Nullable ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
ScheduledFuture<?> schedule(Runnable task, Instant startTime);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay);
三类调度语义泾渭分明:
schedule(task, trigger):按一个Trigger(如CronTrigger)反复触发。trigger 返回 null 表示"不再触发",此时方法返回 null。scheduleAtFixedRate:固定频率------从上一次开始算间隔。scheduleWithFixedDelay:固定延迟------从上一次结束算间隔。
注意 6.0 之后全部迁移到了 Instant/Duration,老的 Date/long 重载都标了 @Deprecated(since="6.0"),只是默认转成新签名。getClock()(5.3 新增)允许实现自己提供时钟------这在测试和时区场景很有用。
16.10.2 Trigger:下一次什么时候跑
Trigger(spring-context/.../scheduling/Trigger.java)核心就一个方法:
java
@Nullable Instant nextExecution(TriggerContext triggerContext);
它根据 TriggerContext(封装了上次计划执行时间、上次实际执行时间、上次完成时间)算出下一次该跑的时刻。返回 null 表示这个 trigger 永远不会再触发了。CronTrigger 是它最主要的实现,下下节专门拆。
16.10.3 ThreadPoolTaskScheduler:ScheduledThreadPoolExecutor 的包装
ThreadPoolTaskScheduler(spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskScheduler.java)继承 ExecutorConfigurationSupport,同时实现 AsyncTaskExecutor、SchedulingTaskExecutor、TaskScheduler。它的核心是持有一个 ScheduledExecutorService。字段(第 81-95 行):
java
private volatile int poolSize = 1;
private volatile boolean removeOnCancelPolicy;
private volatile boolean continueExistingPeriodicTasksAfterShutdownPolicy;
private volatile boolean executeExistingDelayedTasksAfterShutdownPolicy = true;
private @Nullable TaskDecorator taskDecorator;
private volatile @Nullable ErrorHandler errorHandler;
private Clock clock = Clock.systemDefaultZone();
private @Nullable ScheduledExecutorService scheduledExecutor;
默认 poolSize = 1------这就是本章最大的坑的源头:不配的话调度线程只有 1 条。
它的 initializeExecutor(第 194-212 行):
java
@Override
protected ExecutorService initializeExecutor(
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
this.scheduledExecutor = createExecutor(this.poolSize, threadFactory, rejectedExecutionHandler);
if (this.scheduledExecutor instanceof ScheduledThreadPoolExecutor threadPoolExecutor) {
if (this.removeOnCancelPolicy) {
threadPoolExecutor.setRemoveOnCancelPolicy(true);
}
if (this.continueExistingPeriodicTasksAfterShutdownPolicy) {
threadPoolExecutor.setContinueExistingPeriodicTasksAfterShutdownPolicy(true);
}
if (!this.executeExistingDelayedTasksAfterShutdownPolicy) {
threadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
}
}
return this.scheduledExecutor;
}
createExecutor(第 225-246 行)new 一个 ScheduledThreadPoolExecutor,同样用匿名子类桥接 beforeExecute/afterExecute/decorateTask。然后把三个 JDK 策略开关(removeOnCancel、关闭后是否继续周期任务、关闭后是否执行延迟任务)按配置推到底层。注意 executeExistingDelayedTasksAfterShutdownPolicy 默认是 true(关闭后队列里的延迟任务还是会跑)。
16.10.4 schedule(task, trigger):ReschedulingRunnable 的诞生
调度的核心入口 schedule(Runnable, Trigger)(第 352-365 行):
java
@Override
public @Nullable ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
ScheduledExecutorService executor = getScheduledExecutor();
try {
ErrorHandler errorHandler = this.errorHandler;
if (errorHandler == null) {
errorHandler = TaskUtils.getDefaultErrorHandler(true);
}
return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
这里发生了关键的一步:因为 JDK 的 ScheduledExecutorService 只支持"按固定延迟/周期调度",不支持"按任意 trigger 算下一次时间",所以 Spring 用一个 ReschedulingRunnable 适配器把 trigger 的灵活性翻译成一次次的延迟调度 。每次任务跑完,它再问 trigger"下一次几点",然后 executor.schedule(this, delay) 再排一次。这个 ReschedulingRunnable 下下节专门拆。
错误处理:没配自定义 errorHandler 时,TaskUtils.getDefaultErrorHandler(true)------true 表示"这是个周期任务",返回 LOG_AND_SUPPRESS_ERROR_HANDLER(打 error 日志但不抛出,保证下一次还能跑)。这就是为什么 @Scheduled 任务抛异常不会让调度器整个死掉。
16.10.5 fixedRate / fixedDelay 的适配
看 scheduleAtFixedRate(Runnable, Duration)(第 392-402 行):
java
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) {
ScheduledExecutorService executor = getScheduledExecutor();
try {
return executor.scheduleAtFixedRate(errorHandlingTask(task, true),
0, NANO.convert(period), NANO);
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
直接委托给 JDK ScheduledExecutorService.scheduleAtFixedRate,initialDelay=0,period 转成纳秒。errorHandlingTask(task, true) 用 TaskUtils.decorateTaskWithErrorHandler 把任务包一层错误处理(周期任务 suppress 异常)。scheduleWithFixedDelay 同理,只是调 JDK 的 scheduleWithFixedDelay。
注意一个语义细节:fixedRate/fixedDelay 是直接用 JDK 的周期调度 ,不走 ReschedulingRunnable------因为 JDK 原生就支持这两种固定间隔。只有 cron/trigger 这种"下一次时间要动态算"的才需要 ReschedulingRunnable 动态重排。
16.10.6 execute/submit:把调度器当执行器用
ThreadPoolTaskScheduler 也实现了 AsyncTaskExecutor,所以它能直接当 @Async 的执行器。execute(第 316-325 行):
java
@Override
public void execute(Runnable task) {
Executor executor = getScheduledExecutor();
try {
executor.execute(errorHandlingTask(task, false));
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException(executor, task, ex);
}
}
注意 errorHandlingTask(task, false)------这里 isRepeatingTask=false,所以用的是 LOG_AND_PROPAGATE_ERROR_HANDLER(记日志后重新抛出 )。一次性任务异常要传给调用方,周期任务异常要吞掉继续跑------这个 isRepeatingTask 布尔值就是 TaskUtils.getDefaultErrorHandler 的分流开关。
16.11 源码剖析(九):@EnableScheduling 的装配与 ScheduledAnnotationBeanPostProcessor
调度侧的装配比异步侧简单------@EnableScheduling 直接 @Import(SchedulingConfiguration.class),后者只注册一个 ScheduledAnnotationBeanPostProcessor bean。
16.11.1 EnableScheduling → SchedulingConfiguration
EnableScheduling(spring-context/.../annotation/EnableScheduling.java)本身是空注解,唯一动作就是 @Import(SchedulingConfiguration.class)。SchedulingConfiguration(同包 SchedulingConfiguration.java):
java
@Configuration(proxyBeanMethods = false)
@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();
}
}
就注册一个 ScheduledAnnotationBeanPostProcessor(bean 名 "org.springframework.context.annotation.internalScheduledAnnotationProcessor")。没有异步侧那套 AsyncConfigurer 收集------调度侧的定制走 SchedulingConfigurer 接口,在 postProcessor 内部统一收集。
16.11.2 ScheduledAnnotationBeanPostProcessor:一身多职的大管家
这个类(spring-context/.../annotation/ScheduledAnnotationBeanPostProcessor.java)实现了一堆接口:
java
public class ScheduledAnnotationBeanPostProcessor
implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
SmartInitializingSingleton, DisposableBean, ApplicationListener<ApplicationContextEvent> {
它是调度体系的中枢:
DestructionAwareBeanPostProcessor------每个 bean 初始化后扫描它身上的@Scheduled。SmartInitializingSingleton------所有单例就绪后触发finishRegistration()。ApplicationListener<ApplicationContextEvent>------监听ContextRefreshedEvent时注册任务,ContextClosedEvent时取消手动任务。DisposableBean------容器关闭时取消所有任务。
它内部持有一个 ScheduledTaskRegistrar(构造函数 this.registrar = new ScheduledTaskRegistrar())。
16.11.3 postProcessAfterInitialization:扫描 @Scheduled
扫描入口(第 277-317 行):
java
@Override
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, List.of(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);
...
}
else {
annotatedMethods.forEach((method, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
...
}
}
return bean;
}
逐段:
- 跳过基础设施 bean :AOP 基础设施 bean、
TaskScheduler、ScheduledExecutorService自己不扫描(避免把调度器 bean 自己当任务扫)。 AopProxyUtils.ultimateTargetClass(bean):如果 bean 是代理,拿到最终目标类------因为@Scheduled标在目标类方法上,不是代理类上。nonAnnotatedClasses缓存 :扫过一遍发现没有@Scheduled的类加进这个集合,下次直接跳过------性能优化。MethodIntrospector.selectMethods:反射找出所有带@Scheduled/@Schedules(可重复注解)的方法,getMergedRepeatableAnnotations处理@Schedules容器注解。- 对每个方法上的每个
@Scheduled,调processScheduled(scheduled, method, bean)。
16.11.4 processScheduledTask:解析 cron/fixedDelay/fixedRate
核心解析逻辑在 processScheduledTask(第 399-521 行)。我们挑关键分支看。先看 cron 分支(第 425-447 行):
java
// Check cron expression
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.isNegative(), "'initialDelay' not supported for cron triggers");
processedSchedule = true;
if (!Scheduled.CRON_DISABLED.equals(cron)) {
CronTrigger trigger;
if (StringUtils.hasText(zone)) {
trigger = new CronTrigger(cron, StringUtils.parseTimeZoneString(zone));
}
else {
trigger = new CronTrigger(cron);
}
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, trigger)));
}
}
}
- cron 和 zone 都先过
embeddedValueResolver------支持${...}占位符。 - cron 不支持 initialDelay :
Assert.isTrue(initialDelay.isNegative(), "'initialDelay' not supported for cron triggers")------这是个常被踩的坑。 CRON_DISABLED(即"-")跳过 :表达式等于"-"就不注册这个任务------用来配合占位符做"外部配置开关"。- 建
CronTrigger(带 zone 或不带),包成CronTask,调registrar.scheduleCronTask。
再看 fixedDelay(第 452-458 行):
java
Duration fixedDelay = toDuration(scheduled.fixedDelay(), scheduled.timeUnit());
if (!fixedDelay.isNegative()) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, delayToUse)));
}
errorMessage = "Exactly one of the 'cron', 'fixedDelay' or 'fixedRate' attributes is required"------每处理一个就 Assert.isTrue(!processedSchedule, ...),三者只能有一个生效。fixedRate 分支结构相同。
最后兜底(第 504-509 行):
java
if (!processedSchedule) {
if (initialDelay.isNegative()) {
throw new IllegalArgumentException("One-time task only supported with specified initial delay");
}
tasks.add(this.registrar.scheduleOneTimeTask(new OneTimeTask(runnable, delayToUse)));
}
三个都没指定、又给了 initialDelay,就当一次性任务(OneTimeTask),延迟后跑一次。
注意:这里调 registrar.scheduleXxxTask 时,registrar 的 taskScheduler 可能还没设好(finishRegistration 还没跑)。这些方法内部处理了"调度器未就绪"的情况------把任务先存进列表,等 afterPropertiesSet 再统一 schedule(下一节讲 ScheduledTaskRegistrar 时看)。
16.11.5 finishRegistration:什么时候真正开始调度
任务是"扫描时登记"还是"刷新时调度"?看时序控制(第 232-264 行):
java
@Override
public void afterSingletonsInstantiated() {
// Remove resolved singleton classes from cache
this.nonAnnotatedClasses.clear();
if (this.applicationContext == null) {
// Not running in an ApplicationContext -> register tasks early...
finishRegistration();
}
}
private void finishRegistration() {
if (this.scheduler != null) {
this.registrar.setScheduler(this.scheduler);
}
else {
this.localScheduler = new TaskSchedulerRouter();
this.localScheduler.setBeanName(this.beanName);
this.localScheduler.setBeanFactory(this.beanFactory);
this.registrar.setTaskScheduler(this.localScheduler);
}
if (this.beanFactory instanceof ListableBeanFactory lbf) {
Map<String, SchedulingConfigurer> beans = lbf.getBeansOfType(SchedulingConfigurer.class);
List<SchedulingConfigurer> configurers = new ArrayList<>(beans.values());
AnnotationAwareOrderComparator.sort(configurers);
for (SchedulingConfigurer configurer : configurers) {
configurer.configureTasks(this.registrar);
}
}
this.registrar.afterPropertiesSet();
}
- 没 ApplicationContext 时 (独立环境),
afterSingletonsInstantiated里就 finishRegistration。 - 有 ApplicationContext 时 ,finishRegistration 推迟到
ContextRefreshedEvent(见onApplicationEvent第 640-655 行)------等容器完全刷新后再统一调度,这样能给其他ContextRefreshedEvent监听器(比如 Spring Batch 的 job 注册)留出机会。 finishRegistration里:没显式配 scheduler 就建一个TaskSchedulerRouter(路由式调度器,按 qualifier 或默认解析);收集所有SchedulingConfigurer调它们的configureTasks(registrar);最后registrar.afterPropertiesSet()真正启动调度。
这就是为什么"所有 bean 都创建完了"之后定时任务才开始跑------不是立刻标了注解就触发,而是等容器刷新完成。
16.12 源码剖析(十):CronExpression------cron 表达式的解析与下一次触发计算
cron 表达式是调度体系里最"算法味"的部分。Spring 5.3 起用全新的 CronExpression(spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java)重写了老的 CronSequenceGenerator。这一节拆它怎么解析、怎么算下一次。
16.12.1 字段结构:六段,倒序排列
先看它的构造函数(第 66-73 行):
java
private CronExpression(CronField seconds, CronField minutes, CronField hours,
CronField daysOfMonth, CronField months, CronField daysOfWeek, String expression) {
// Reverse order, to make big changes first.
// To make sure we end up at 0 nanos, we add an extra field.
this.fields = new CronField[] {daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
this.expression = expression;
}
注意 fields 数组的顺序:daysOfWeek → months → daysOfMonth → hours → minutes → seconds → zeroNanos 。这是输入顺序的倒序 ,再加一个"零纳秒"字段。注释解释了原因:"Reverse order, to make big changes first"------计算下一次触发时,先调整变化粒度最大的字段(日/月/周),最后才是秒和纳秒,这样每一步调整都能更快收敛到正确时刻。MAX_ATTEMPTS = 366(第 48 行)是收敛失败的兜底上限。
16.12.2 parse:六段校验与宏替换
parse(第 176-200 行):
java
public static CronExpression parse(String expression) {
Assert.hasLength(expression, "Expression must not be empty");
expression = resolveMacros(expression);
String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
if (fields.length != 6) {
throw new IllegalArgumentException(String.format(
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
}
try {
CronField seconds = CronField.parseSeconds(fields[0]);
CronField minutes = CronField.parseMinutes(fields[1]);
CronField hours = CronField.parseHours(fields[2]);
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
CronField months = CronField.parseMonth(fields[4]);
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
}
catch (IllegalArgumentException ex) {
String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
throw new IllegalArgumentException(msg, ex);
}
}
逐段:
resolveMacros(第 222-230 行):把@yearly/@monthly/@daily/@hourly等宏替换成标准六段表达式(宏表见第 50-58 行,比如@daily→0 0 0 * * *)。- 空格切分后必须恰好 6 段 :
fields.length != 6直接报错。这就是 Spring cron 是 6 段(秒 分 时 日 月 周)的铁律------比 Linux crontab(5 段,无秒)多一个秒位。没有第 7 段"年"位(不像 Quartz)。 - 逐字段委托
CronField.parseXxx解析,每个字段知道自己的取值范围。
字段顺序与范围(javadoc 第 82-89 行 ASCII 图):秒(0-59)、分(0-59)、时(0-23)、日(1-31)、月(1-12)、周(0-7,0 和 7 都是周日)。
16.12.3 ? 与 * 的语义
任务书特别问 ? 与 *。在 Spring cron 里,? 和 * 在"日"和"周"字段上是等价的 ------都表示"不指定"。看 BitsCronField.parseDate(spring-context/.../support/BitsCronField.java 第 108-113 行):
java
private static BitsCronField parseDate(String value, BitsCronField.Type type) {
if (value.equals("?")) {
value = "*";
}
return BitsCronField.parseField(value, type);
}
? 在解析时直接被替换成 * 。也就是说,Spring 的 ? 不是 Quartz 那种"为了避免日和周冲突而引入的特殊不关心符"------它和 * 完全等价。真正的"日与周都生效、取并集"语义由 CompositeCronField/QuartzCronField 在匹配层处理。你写 0 0 0 25 12 ? 和 0 0 0 25 12 * 在 Spring 里效果一样。
16.12.4 next:下一次触发时间的计算
核心算法 next(第 240-264 行):
java
public <T extends Temporal & Comparable<? super T>> @Nullable T next(T temporal) {
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
}
private <T extends Temporal & Comparable<? super T>> @Nullable T nextOrSame(T temporal) {
for (int i = 0; i < MAX_ATTEMPTS; i++) {
T result = nextOrSameInternal(temporal);
if (result == null || result.equals(temporal)) {
return result;
}
temporal = result;
}
return null;
}
private <T extends Temporal & Comparable<? super T>> @Nullable T nextOrSameInternal(T temporal) {
for (CronField field : this.fields) {
temporal = field.nextOrSame(temporal);
if (temporal == null) {
return null;
}
}
return temporal;
}
逐段精读:
next先把种子时间加 1 纳秒 :ChronoUnit.NANOS.addTo(temporal, 1)。为什么?因为 cron 算的是"严格在种子之后的下一次",加 1 纳秒避免"种子时刻本身就匹配"被当作结果(否则会反复返回同一个时刻)。nextOrSame循环最多 366 次 :每次nextOrSameInternal把时间调整一次,直到"结果不再变化"(收敛)或超 366 次(返回 null,表示一年内无解,比如"2 月 30 日"这种不存在的日期)。nextOrSameInternal倒序遍历所有字段 :对每个CronField调field.nextOrSame(temporal)------从最粗的字段(周/月/日)到最细(秒/纳秒),逐步把时间"推"到满足该字段的下一个合法值。如果某字段把时间推过界了(比如日字段推到 32 号,月份要进位),下一个更粗的字段会再次调整,如此迭代收敛。
16.12.5 BitsCronField.nextOrSame:位运算匹配
每个字段的匹配逻辑在 BitsCronField(普通字段用位集合表示合法值)。它的 nextOrSame(第 174-211 行):
java
@Override
public <T extends Temporal & Comparable<? super T>> @Nullable T nextOrSame(T temporal) {
int current = type().get(temporal);
int next = nextSetBit(current);
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(type().get(temporal));
if (next == -1) {
temporal = type().rollForward(temporal);
next = nextSetBit(0);
}
}
if (next == current) {
return temporal;
}
else {
int count = 0;
current = type().get(temporal);
while (current != next && count++ < CronExpression.MAX_ATTEMPTS) {
temporal = type().elapseUntil(temporal, next);
current = type().get(temporal);
next = nextSetBit(current);
...
}
if (count >= CronExpression.MAX_ATTEMPTS) {
return null;
}
return type().reset(temporal);
}
}
它用一个 long bits 位图表示这个字段合法值的集合(nextSetBit 用位运算 Long.numberOfTrailingZeros 高效找下一个合法位):
- 取出当前时间在该字段上的值
current,从current开始找下一个被置位的合法值next。 - 如果当前已经合法(
next == current),直接返回。 - 否则,
elapseUntil把时间推到next,再重新读 current、找 next,循环直到收敛。 type().reset(temporal)------把所有更细的字段(比如匹配到某分钟后,把秒、纳秒清零)重置到最小值。
CronField.Type 枚举(CronField.java 第 180-188 行)为每个字段定义了它的 ChronoField、进位单位(higherOrder)和下级字段(lowerOrders),是这套时间推算的"元数据"。比如 HOUR 的 higherOrder 是 DAYS,lowerOrders 是 MINUTE/SECOND/NANO------匹配到某个小时后,要把分秒纳秒 reset 到 0。
16.12.6 CronTrigger:从完成时间算下一次
CronTrigger(spring-context/.../support/CronTrigger.java)包装一个 CronExpression,它的 nextExecution(第 116-122 行):
java
@Override
public @Nullable Instant nextExecution(TriggerContext triggerContext) {
Instant timestamp = determineLatestTimestamp(triggerContext);
ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone());
ZonedDateTime zonedTimestamp = timestamp.atZone(zone);
ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
return (nextTimestamp != null ? nextTimestamp.toInstant() : null);
}
determineLatestTimestamp(第 124-139 行)取上次完成时间 triggerContext.lastCompletion() 作为计算种子------"next execution times are calculated based on the completion time of the previous execution; therefore, overlapping executions won't occur"。注意 6.1.3 新增了 forLenientExecution/forFixedExecution 两种模式:默认(lenient)基于完成时间调度,任务跑太久会跳过错过的触发;forFixedExecution 基于上次计划时间调度,不会漏触发(会补上迟到的执行)。
16.13 源码剖析(十一):ScheduledTaskRegistrar、ReschedulingRunnable 与 TaskSchedulerRouter
任务被解析成 CronTask/FixedRateTask 等对象后,最终由 ScheduledTaskRegistrar 注册到 TaskScheduler;cron 类任务的动态重排由 ReschedulingRunnable 完成。这一节拆这三个类。
16.13.1 任务模型:CronTask / FixedRateTask / FixedDelayTask
这几个是"任务描述对象",很薄。继承关系:
IntervalTask(spring-context/.../config/IntervalTask.java):持有Runnable+interval(间隔 Duration)+initialDelay。FixedRateTask extends IntervalTask:标记"固定频率"语义(FixedRateTask.java第 51-53 行构造就调父类)。FixedDelayTask extends IntervalTask:标记"固定延迟"语义。CronTask extends TriggerTask(CronTask.java第 41-53 行):持有 Runnable +CronTrigger,构造时new CronTrigger(expression)。
它们本身不含执行逻辑,只是"任务是什么、什么时候跑"的数据载体。真正调度发生在 Registrar。
16.13.2 ScheduledTaskRegistrar:调度的登记处
ScheduledTaskRegistrar(spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java)是 @Scheduled 和编程式任务的统一登记处。关键字段(第 77-95 行):
java
private @Nullable TaskScheduler taskScheduler;
private @Nullable ScheduledExecutorService localExecutor;
private @Nullable List<TriggerTask> triggerTasks;
private @Nullable List<CronTask> cronTasks;
private @Nullable List<IntervalTask> fixedRateTasks;
private @Nullable List<IntervalTask> fixedDelayTasks;
private @Nullable List<DelayedTask> oneTimeTasks;
private final Map<Task, ScheduledTask> unresolvedTasks = new HashMap<>(16);
private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);
五类任务列表 + 已调度结果集合。afterPropertiesSet()(第 410-412 行)直接调 scheduleTasks()。
16.13.3 scheduleTasks:兜底单线程 + 逐类调度
scheduleTasks(第 418-463 行):
java
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) {
...
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
...
}
}
if (this.oneTimeTasks != null) {
for (DelayedTask task : this.oneTimeTasks) {
...
}
}
}
第一行就是本章最大坑的源码出处:
java
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
如果一直没设过 TaskScheduler,就在这里现场 Executors.newSingleThreadScheduledExecutor()------单线程 。所有 @Scheduled 任务挤在这一条线程上串行跑。这就是为什么"不配 TaskScheduler,一个慢任务会饿死其他所有定时任务"。
注意:7.0.9 里 ScheduledAnnotationBeanPostProcessor 默认走的不是这里的兜底,而是 TaskSchedulerRouter(下一小节)------router 也会在找不到任何 scheduler bean 时同样 newSingleThreadScheduledExecutor()(见 TaskSchedulerRouter.determineDefaultScheduler 第 222-224 行)。结论一致:不配就是单线程。
16.13.4 scheduleCronTask / scheduleFixedRateTask:任务如何落地
看 scheduleCronTask(第 502-517 行):
java
public @Nullable ScheduledTask scheduleCronTask(CronTask task) {
ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
boolean newTask = false;
if (scheduledTask == null) {
scheduledTask = new ScheduledTask(task);
newTask = true;
}
if (this.taskScheduler != null) {
scheduledTask.future = this.taskScheduler.schedule(task.getRunnable(), task.getTrigger());
}
else {
addCronTask(task);
this.unresolvedTasks.put(task, scheduledTask);
}
return (newTask ? scheduledTask : null);
}
逻辑:先从 unresolvedTasks 取(可能之前调度器还没就绪时登记过);没有就 new ScheduledTask。调度器就绪了就 taskScheduler.schedule(runnable, trigger) 真正提交;没就绪就先存进 cronTasks 列表。
scheduleFixedRateTask(第 526-550 行)稍复杂,处理 initialDelay:
java
public @Nullable ScheduledTask scheduleFixedRateTask(FixedRateTask task) {
...
if (this.taskScheduler != null) {
Duration initialDelay = task.getInitialDelayDuration();
if (initialDelay.toNanos() > 0) {
Instant startTime = this.taskScheduler.getClock().instant().plus(initialDelay);
scheduledTask.future =
this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), startTime, task.getIntervalDuration());
}
else {
scheduledTask.future =
this.taskScheduler.scheduleAtFixedRate(task.getRunnable(), task.getIntervalDuration());
}
}
...
}
有 initialDelay 就用"指定开始时刻 + 周期"的重载,否则用"立刻开始 + 周期"的重载。
16.13.5 ReschedulingRunnable:cron 任务的动态重排循环
cron/trigger 类任务不能用 JDK 的固定周期调度,因为每次触发时间都要重新算。ReschedulingRunnable(spring-context/.../concurrent/ReschedulingRunnable.java)就是这个适配器。它既是 Runnable 又是 ScheduledFuture。
首次调度 schedule()(第 74-84 行):
java
public @Nullable ScheduledFuture<?> schedule() {
synchronized (this.triggerContextMonitor) {
this.scheduledExecutionTime = this.trigger.nextExecution(this.triggerContext);
if (this.scheduledExecutionTime == null) {
return null;
}
Duration delay = Duration.between(this.triggerContext.getClock().instant(), this.scheduledExecutionTime);
this.currentFuture = this.executor.schedule(this, delay.toNanos(), TimeUnit.NANOSECONDS);
return this;
}
}
问 trigger 要下一次时间,算出从现在到那时的延迟,把 this(自己这个 Runnable)提交给 ScheduledExecutorService.schedule 排一个一次性任务。
然后是核心循环 run()(第 91-103 行):
java
@Override
public void run() {
Instant actualExecutionTime = this.triggerContext.getClock().instant();
super.run();
Instant completionTime = this.triggerContext.getClock().instant();
synchronized (this.triggerContextMonitor) {
Assert.state(this.scheduledExecutionTime != null, "No scheduled execution");
this.triggerContext.update(this.scheduledExecutionTime, actualExecutionTime, completionTime);
if (!obtainCurrentFuture().isCancelled()) {
schedule();
}
}
}
逐段:
- 记录
actualExecutionTime(实际开始时刻)。 super.run()------执行真正的任务(DelegatingErrorHandlingRunnable的 run,里面包着用户任务和 ErrorHandler)。- 记录
completionTime(完成时刻)。 triggerContext.update(计划时间, 实际开始, 完成时间)------把这三个时间写进上下文,供 trigger 下次计算用。if (!isCancelled()) schedule()------没被取消就递归调schedule()再排下一次。
这就是 cron 任务的"自循环":每次跑完 → 更新上下文 → 问 trigger 下一次 → schedule 一次性任务 → 到点再 run。JDK 的 ScheduledThreadPoolExecutor 只负责"延迟 delay 后跑这个 Runnable","delay 是多少"完全由 Spring 的 trigger 算。
错误处理:super.run() 里的 DelegatingErrorHandlingRunnable 已经把任务异常交给 ErrorHandler(周期任务默认 suppress + 打日志),所以异常不会冲出 run(),下一次 schedule() 照常执行------任务不会因为一次异常而死。
16.13.6 TaskSchedulerRouter:7.0.9 的调度器路由
TaskSchedulerRouter(spring-context/.../config/TaskSchedulerRouter.java,6.1 新增)是 ScheduledAnnotationBeanPostProcessor 默认持有的调度器。它本身是个 TaskScheduler,但每个调度方法都 determineTargetScheduler(task) 路由到真正的调度器。核心 determineDefaultScheduler(第 163-225 行)的解析链:
resolveSchedulerBean(TaskScheduler.class, byName=false)------按类型找唯一TaskScheduler。- 多个 → 按名字找
"taskScheduler"。 - 没有
TaskScheduler→ 找ScheduledExecutorService包成ConcurrentTaskScheduler(同样先按类型唯一、再按名字)。 - 都没有 →
Executors.newSingleThreadScheduledExecutor()兜底单线程。
而 determineTargetScheduler(第 135-146 行)还支持 @Scheduled(scheduler="xxx") 的 qualifier------如果任务是 SchedulingAwareRunnable 且带 qualifier,就按 qualifier 找对应调度器。这就是 @Scheduled 注解里 scheduler 属性(6.1 新增)的底层。
这条链回答了任务书的问题:调度器解析链在 7.0.9 被重构到了 TaskSchedulerRouter,默认名 "taskScheduler",解析顺序是"唯一 TaskScheduler bean → 名为 taskScheduler → 唯一 ScheduledExecutorService → 名为 taskScheduler → 现场建单线程"。
16.14 运行机制:两次完整调用时序
前面把零件都拆完了,这一节把它们装回两次完整的"端到端"流程里。
16.14.1 一次 @Async 方法调用的完整链路
假设我们有:
java
@Configuration
@EnableAsync
public class AsyncConfig {}
@Service
public class OrderService {
@Async("orderExecutor")
public CompletableFuture<OrderVO> createOrder(OrderDTO dto) { ... }
}
且容器里有个 @Bean("orderExecutor") ThreadPoolTaskExecutor。从 orderService.createOrder(dto) 被外部调用开始:
- 调用方拿到的是代理 :容器初始化
OrderServicebean 时,AsyncAnnotationBeanPostProcessor.postProcessAfterInitialization用AsyncAnnotationAdvisor的 pointcut(方法级@Async匹配)匹配上,给它包了一层代理(JDK 动态代理或 CGLIB,取决于proxyTargetClass)。 proxy.createOrder(dto)进入AnnotationAsyncExecutionInterceptor.invoke(继承自AsyncExecutionInterceptor)。BridgeMethodResolver.getMostSpecificMethod拿到真实方法createOrder。determineAsyncExecutor(createOrder):getExecutorQualifier读出"orderExecutor",findQualifiedExecutor(beanFactory, "orderExecutor")从容器拿到那个ThreadPoolTaskExecutor,包成AsyncTaskExecutor(它本来就是),缓存进this.executors。- 包 Callable :
Callable<Object> task = () -> { Object result = invocation.proceed(); ... }。invocation.proceed()才是真正调OrderService.createOrder,但它现在只是"被包着",还没执行。 doSubmit(task, executor, CompletableFuture.class):因为返回类型是CompletableFuture,走executor.submitCompletable(task)→CompletableFuture.runAsync(task, executor)。- 调用线程立刻返回 :一个尚未完成的
CompletableFuture。此时方法体还没跑------真正的执行被丢进了orderExecutor的线程池。 - 工作线程 :
orderExecutor的某个线程被唤醒,执行task.call():invocation.proceed()→ 真正执行OrderService.createOrder方法体。方法体里return CompletableFuture.completedFuture(vo)。回到invoke的 Callable:result instanceof Future→future.get()拿到vo并 return。 - CompletableFuture 完成 :步骤 6 返回的那个异步 CompletableFuture 被填入结果。调用方在
thenApply/get里拿到vo。 - 异常路径 :如果步骤 8 方法体抛异常,Callable 里 catch 到 → 因为返回类型是 Future,
handleError走ReflectionUtils.rethrowException,异常被塞进 CompletableFuture,调用方在 get 时拿到CompletionException。
关键结论:调用线程在步骤 6 就返回了,步骤 8-9 在 orderExecutor 的线程上跑 。自调用(this.createOrder(dto))为什么失效?因为 this 是原始对象不是代理,不走步骤 2 的拦截器。
16.14.2 一次 @Scheduled(cron) 任务的完整注册与触发时序
假设:
java
@Component
public class ReportJob {
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() { ... }
}
注册阶段(容器启动时):
@EnableScheduling→SchedulingConfiguration注册ScheduledAnnotationBeanPostProcessor。- 容器初始化
ReportJobbean 后,postProcessor 的postProcessAfterInitialization扫描到dailyReport上的@Scheduled。 processScheduled→processScheduledTask:cron 分支解析"0 0 2 * * ?"、zone,建CronTrigger(内含CronExpression),包成CronTask,registrar.scheduleCronTask------此时 taskScheduler 还没设,先存进cronTasks列表。- 容器刷新完成,
ContextRefreshedEvent触发finishRegistration:建TaskSchedulerRouter,没找到 scheduler bean → 兜底Executors.newSingleThreadScheduledExecutor()或用你配的ThreadPoolTaskScheduler。 registrar.afterPropertiesSet()→scheduleTasks():对每个CronTask调taskScheduler.schedule(runnable, trigger)。ThreadPoolTaskScheduler.schedule(task, trigger):包一个ReschedulingRunnable(task, trigger, clock, executor, errorHandler),调.schedule()。ReschedulingRunnable.schedule():trigger.nextExecution(ctx)→CronTrigger.nextExecution→CronExpression.next(now)算出下一次触发时刻(比如明天凌晨 2:00:00),executor.schedule(this, delay, NANOS)提交一个一次性延迟任务。
触发阶段(到了凌晨 2:00):
ScheduledThreadPoolExecutor的工作线程到点,执行ReschedulingRunnable.run()。run():记录 actualExecutionTime →super.run()(DelegatingErrorHandlingRunnable执行dailyReport()方法体)→ 记录 completionTime →triggerContext.update(计划时间, 开始, 完成)→ 没被取消就schedule()排下一次。- 如果 dailyReport 抛异常 :被
DelegatingErrorHandlingRunnable传给ErrorHandler(默认LOG_AND_SUPPRESS:打 error 日志,不抛)。run()正常结束,步骤 9 的schedule()照常执行------明天 2:00 还会跑。
两条链路对照看:@Async 是"代理拦截 + 一次性提交 Future",@Scheduled 是"BeanPostProcessor 扫描 + Trigger 自循环重排"。前者靠 AOP 代理切换线程,后者靠反射扫描注册定时任务。理解了这个本质差异,所有"为什么自调用失效""为什么任务串行""为什么异常被吞"都能反推出来。
16.15 扩展用法与最佳实践
16.15.1 线程池参数设计:core/max/queue 怎么定
回到 16.5 的 JDK 扩容规则:核心线程满 → 进队列 → 队列满 → 扩到 max → 满则拒绝。Spring 默认配置(core=1、max=无限、queue=无限)几乎必然串行。生产环境要显式设计:
- CPU 密集型任务(计算、序列化):线程数 ≈ CPU 核数 + 1。此时队列可以小一点(快速触发拒绝监控),max 不必很大。
- IO 密集型任务 (调用下游、查库):线程数可以开到 CPU 核数 × (1 + 等待时间/计算时间),常见 8~64。队列要有界,给突发流量缓冲,但不能用
Integer.MAX_VALUE(那等于无界队列,maxPoolSize 形同虚设)。 - 拒绝策略 :默认
AbortPolicy(抛异常)适合"宁可让调用方知道也不能丢";CallerRunsPolicy适合"允许降速但不丢任务"------让提交线程自己跑,天然形成背压;DiscardPolicy/DiscardOldestPolicy只适合"新任务可以覆盖旧任务"的场景(如状态刷新)。 keepAliveSeconds:默认 60 秒,非核心线程空闲 60 秒回收。配合allowCoreThreadTimeOut(true)可以让核心线程也回收,实现低峰期省线程。
一个反模式必须点名:直接 new ThreadPoolTaskExecutor() 不设 core/max/queue 就用 。前面源码已证明这等价于单线程池。@EnableAsync 的 javadoc 也建议:"Define a ThreadPoolTaskExecutor bean ... or ... define a AsyncConfigurer bean"。
16.15.2 线程命名与监控
16.4.2 讲过:不设线程名前缀时自动用 bean 名加 -。生产环境一定要给每个执行器起有意义的 bean 名 (如 orderExecutor、reportExecutor),这样线程栈里出现 orderExecutor-3 你立刻知道是哪个池。也可以显式 setThreadNamePrefix("async-order-")。
监控上,ThreadPoolTaskExecutor 实现了 SchedulingTaskExecutor 且核心/最大/存活时间都能运行时改(16.5.5),可以挂 JMX。建议通过 Micrometer 接入"活跃线程数、队列长度、任务拒绝数"指标------队列长度尤其关键,它是"池要撑爆了"的先行指标。
16.15.3 @Async 与 @Transactional 的组合
这是高频坑。两个注解都走 AOP,顺序由 @EnableAsync(order=...) 或 @EnableTransactionManagement(order=...) 决定。默认 order 都是最低优先级,谁先谁后不确定。关键问题:
@Async方法内部调@Transactional方法 :异步线程是新线程,事务上下文(绑定在 ThreadLocal 的TransactionSynchronizationManager)默认不会传过去。要么在@Async方法上同时标@Transactional(让异步线程自己开事务),要么用TaskDecorator把事务传播过去(不推荐,事务跨线程很危险)。- 外层
@Transactional调内层@Async:内层异步方法在线程池里跑,外层事务提交时内层可能还没跑完------外层提交了,内层却失败回滚,数据不一致。原则:@Async方法不要和外层事务共享数据,要么把异步动作放到事务提交之后(TransactionSynchronization.afterCommit)。
16.15.4 任务异常告警
void 的 @Async 方法异常只打一行 error 日志(16.9.6),线上很容易漏。生产建议:
- 实现
AsyncUncaughtExceptionHandler,把异常上报到监控(钉钉/Prometheus/错误追踪系统),通过AsyncConfigurer.getAsyncUncaughtExceptionHandler()配进去。 - 对调度任务,实现
ErrorHandler(SchedulingConfigurer.configureTasks里registrar.setErrorHandler),做同样的告警。 - 返回
CompletableFuture的方法,别忘了exceptionally/whenComplete里也要处理异常,否则异常被 future 吞掉(只有 get 才看得到)。
16.15.5 虚拟线程配置(7.0.9)
16.6.4 列了三条路径,按诉求选:
- 最简单 :
@Bean VirtualThreadTaskExecutor vtExecutor() { return new VirtualThreadTaskExecutor("vt-"); }------每任务一条虚拟线程。 - 要限流/装饰 :
ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor(); t.setVirtualThreads(true);------保留 Spring 生命周期,但注意虚拟线程下"池大小"意义弱化(虚拟线程极轻量,core/max 主要是控制并发上限而非线程数)。 - SimpleAsyncTaskExecutor 开虚拟线程:保留限流装饰能力。
虚拟线程注意事项:虚拟线程共享少数 carrier 线程,不要 synchronized 块里长时间阻塞(会 pin 住 carrier);调度密集型任务仍建议显式配 TaskScheduler。
16.16 常见问题与坑
16.16.1 @Scheduled 默认单线程导致任务串行/饥饿
这是调度侧第一大坑。源码在 16.13.3:没配 TaskScheduler 就 Executors.newSingleThreadScheduledExecutor()------单线程。所有 @Scheduled 任务在这一条线程上排队。后果:
- 一个
@Scheduled(fixedRate=1000)的任务跑了 5 秒,其他所有@Scheduled任务在这 5 秒里全部暂停。 - 固定频率任务被拖慢后,JDK 的 fixedRate 语义是"上次开始后 period 就到点",任务排到后面会"追跑"(可能连续跑好几次补)。
- cron 任务是
ReschedulingRunnable自循环,一个卡住会阻塞同线程的下一次 schedule。
解法 :显式配一个 ThreadPoolTaskScheduler(poolSize 设成合适并发数),或一个 TaskScheduler bean / 一个 ScheduledExecutorService bean------TaskSchedulerRouter.determineDefaultScheduler 会自动选用它。
java
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(8);
scheduler.setThreadNamePrefix("sched-");
return scheduler;
}
16.16.2 fixedRate 与 fixedDelay 语义混淆
fixedRate:从上一次开始时刻算间隔。如果任务执行时间 > period,任务会重叠排队(JDK 会尽量追)。fixedDelay:从上一次结束时刻算 delay。无论上次跑多久,结束后再等 delay。
源码差异:fixedRate 走 JDK scheduleAtFixedRate,fixedDelay 走 scheduleWithFixedDelay(16.10.5)。选哪个?任务耗时不稳定、不想叠加用 fixedDelay;任务要严格按时间表触发、能容忍重叠用 fixedRate。大多数"按固定间隔轮询"的场景其实应该用 fixedDelay,否则任务慢了会雪崩。
16.16.3 cron 表达式 6 段与 5 段差异
Spring cron 是 6 段 (秒 分 时 日 月 周),比 Linux crontab 多一个秒位,没有 Quartz 的第 7 段年 。详见 16.12.2:fields.length != 6 直接报错。常见错误:
- 从 Linux crontab 抄来 5 段 → Spring 报 "Cron expression must consist of 6 fields"。
- 想用
@Yearly等宏可以(resolveMacros支持),但宏展开后还是 6 段。 ?和*在 Spring 里等价(16.12.3),别纠结。
16.16.4 @Async 自调用失效
16.14.1 讲过:@Async 靠代理拦截。同类里 this.createOrder() 调的是原始对象,不经过代理,直接同步执行。这是 AOP 代理机制的固有局限,不是 bug。解法:
- 把异步方法拆到另一个 bean,跨 bean 调用(走代理)。
- 在
this上注入自己(@Lazy @Autowired OrderService self),用self.createOrder()。 - 用
AopContext.currentProxy()(需要@EnableAspectJAutoProxy(exposeProxy=true))。 - ASPECTJ 模式(
@EnableAsync(mode=ASPECTJ))织入字节码,无代理、自调用也生效------但要配 AspectJ 织入环境。
16.16.5 @Async 线程上下文丢失
ThreadLocal(MDC、SecurityContext、RequestContext)在异步线程里默认没有。16.3.3 的 TaskDecorator 是官方解法:在 decorate 里抓调用线程上下文,包进 Runnable 在工作线程恢复。
java
public class ContextCopyingDecorator implements TaskDecorator {
@Override public Runnable decorate(Runnable r) {
Map<String,String> mdc = MDC.getCopyOfContextMap();
return () -> {
Map<String,String> prev = MDC.getCopyOfContextMap();
if (mdc != null) MDC.setContextMap(mdc); else MDC.clear();
try { r.run(); } finally { MDC.setContextMap(prev); }
};
}
}
ThreadPoolTaskExecutor.setTaskDecorator(...) 配上即可。注意 16.3.3 说的:Callable 提交时 decorator 抓不到异常,不要指望在 decorator 里 catch 业务异常。
16.16.6 void 任务吞异常无感知
16.9.6:void 的 @Async 方法异常只打 error 日志;@Scheduled 任务异常被 LOG_AND_SUPPRESS_ERROR_HANDLER 吞掉。解法见 16.15.4:配自定义 AsyncUncaughtExceptionHandler / ErrorHandler 做告警。
16.16.7 容器关闭时的优雅停机
16.4.3/16.4.4 讲过:默认 waitForTasksToCompleteOnShutdown=false → shutdownNow() 中断在跑任务、丢排队任务。生产要优雅停机:
java
@Bean(destroyMethod = "shutdown")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
e.setWaitForTasksToCompleteOnShutdown(true); // shutdown() 而非 shutdownNow()
e.setAwaitTerminationSeconds(30); // 关闭时最多等 30 秒
return e;
}
注意 waitForTasksToCompleteOnShutdown=true 一定要配 awaitTerminationSeconds,否则关闭流程会一直阻塞(16.4.3)。另外 ThreadPoolTaskExecutor 默认 strictEarlyShutdown=false,ContextClosedEvent 时不提前拒绝任务------这意味着关闭窗口里还可能有任务进来;如果你要严格早停,setStrictEarlyShutdown(true)。
16.16.8 Future 超时与拒绝策略行为
@Async返回CompletableFuture,调用方get(timeout)超时会抛TimeoutException,但异步线程里的任务仍在跑 (future 超时不取消任务)。要真取消得future.cancel(true)。- 线程池队列满 + max 满时,默认
AbortPolicy抛TaskRejectedException(16.5.4)------这个异常在提交线程抛出,不在异步线程。如果你@Async方法返回 void,调用方拿到的是TaskRejectedException(因为还没切线程就被拒了),能感知;如果已经切到线程池里才拒绝,则取决于提交路径。
16.17 本章小结
本章沿着两条主线------异步执行(@Async)与定时调度(@Scheduled)------把 Spring Framework 7.0.9 从注解到 java.util.concurrent 的完整链路拆了一遍。可以提炼成几个核心判断:
- 异步靠代理,调度靠扫描 。
@Async走 AOP:@EnableAsync注册AsyncAnnotationBeanPostProcessor,它持有AsyncAnnotationAdvisor(方法级@Asyncpointcut +AnnotationAsyncExecutionInterceptoradvice),在 bean 初始化后织入代理;@Scheduled不走代理,靠ScheduledAnnotationBeanPostProcessor在初始化后反射扫描、登记进ScheduledTaskRegistrar,容器刷新后统一调度。 - 执行器适配是纯属性对号入座 。
ExecutorConfigurationSupport管生命周期(initialize/shutdown/优雅停机),ThreadPoolTaskExecutor.initializeExecutor把 core/max/queue/keepAlive 原样塞进 JDKThreadPoolExecutor,queueCapacity>0 用有界LinkedBlockingQueue、=0 用SynchronousQueue;默认 core=1、queue=无限,等价单线程池。 - 默认执行器链路 :唯一
TaskExecutorbean → 名为taskExecutor的Executorbean →new SimpleAsyncTaskExecutor()。@Async("xxx")的 qualifier 由getExecutorQualifier读出、按名查 bean,结果按方法缓存。 - 返回类型严格分支 :
CompletableFuture→submitCompletable;Future→submit;void/Kotlin Unit →submit但不返回句柄(异常进AsyncUncaughtExceptionHandler,默认只打日志);其他类型直接报错。 - 调度器解析链 :
TaskSchedulerRouter先按类型找TaskScheduler,再按名taskScheduler,再找ScheduledExecutorService,都没有才newSingleThreadScheduledExecutor()------单线程兜底是最大坑。 - cron 是 6 段、倒序字段位运算匹配 :
CronExpression.next先加 1 纳秒种子,再倒序(周月日时分秒)逐字段nextOrSame,用BitsCronField位图高效找下一个合法值,最多迭代 366 次收敛。 - cron 任务靠 ReschedulingRunnable 自循环:JDK 只支持固定间隔,Spring 用"每次跑完问 trigger 下一次、再 schedule 一次性任务"把 trigger 的灵活性翻译成一次次延迟调度;异常被 ErrorHandler suppress,任务不会死。
关键类清单:
| 类/接口 | 仓库相对路径 | 职责 |
|---|---|---|
TaskExecutor |
spring-core/src/main/java/org/springframework/core/task/TaskExecutor.java |
顶层执行抽象,继承 JDK Executor,函数式接口 |
AsyncTaskExecutor |
spring-core/.../task/AsyncTaskExecutor.java |
承诺异步,submit/submitCompletable 默认方法 |
TaskDecorator |
spring-core/.../task/TaskDecorator.java |
任务装饰,上下文传递钩子 |
TaskRejectedException |
spring-core/.../task/TaskRejectedException.java |
Spring 统一拒绝异常,继承 JDK RejectedExecutionException |
SimpleAsyncTaskExecutor |
spring-core/.../task/SimpleAsyncTaskExecutor.java |
每任务新线程,信号量限流,支持虚拟线程 |
VirtualThreadTaskExecutor |
spring-core/.../task/VirtualThreadTaskExecutor.java |
虚拟线程执行器(7.0.9 内置) |
ExecutorConfigurationSupport |
spring-context/.../scheduling/concurrent/ExecutorConfigurationSupport.java |
执行器生命周期基类,初始化/优雅停机 |
ThreadPoolTaskExecutor |
spring-context/.../scheduling/concurrent/ThreadPoolTaskExecutor.java |
ThreadPoolExecutor 的 Spring 适配 |
TaskScheduler |
spring-context/.../scheduling/TaskScheduler.java |
调度顶层接口 |
Trigger / CronTrigger |
spring-context/.../scheduling/Trigger.java / support/CronTrigger.java |
下一次触发策略 |
ThreadPoolTaskScheduler |
spring-context/.../scheduling/concurrent/ThreadPoolTaskScheduler.java |
ScheduledThreadPoolExecutor 适配 |
ReschedulingRunnable |
spring-context/.../scheduling/concurrent/ReschedulingRunnable.java |
cron 任务动态重排循环 |
@EnableAsync / AsyncConfigurationSelector / ProxyAsyncConfiguration |
spring-context/.../scheduling/annotation/ |
异步装配链 |
AsyncAnnotationBeanPostProcessor |
spring-context/.../scheduling/annotation/AsyncAnnotationBeanPostProcessor.java |
异步代理后处理器 |
AsyncAnnotationAdvisor |
spring-context/.../scheduling/annotation/AsyncAnnotationAdvisor.java |
advice + 方法级 pointcut |
AsyncExecutionAspectSupport / AsyncExecutionInterceptor |
spring-aop/.../interceptor/ |
异步执行核心:executor 解析、返回类型分支 |
AsyncUncaughtExceptionHandler / SimpleAsyncUncaughtExceptionHandler |
spring-aop/.../interceptor/ |
void 方法异常处理 |
@EnableScheduling / SchedulingConfiguration |
spring-context/.../scheduling/annotation/ |
调度装配链 |
ScheduledAnnotationBeanPostProcessor |
spring-context/.../scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java |
@Scheduled 扫描注册中枢 |
ScheduledTaskRegistrar |
spring-context/.../scheduling/config/ScheduledTaskRegistrar.java |
任务登记处,单线程兜底 |
TaskSchedulerRouter |
spring-context/.../scheduling/config/TaskSchedulerRouter.java |
调度器路由与解析链 |
CronExpression / CronField / BitsCronField |
spring-context/.../scheduling/support/ |
cron 解析与下一次触发计算 |
CronTask / FixedRateTask / FixedDelayTask / ScheduledTask |
spring-context/.../scheduling/config/ |
任务模型对象 |
TaskUtils |
spring-context/.../scheduling/support/TaskUtils.java |
默认 ErrorHandler(LOG_AND_SUPPRESS / LOG_AND_PROPAGATE) |
记住这张表和三条结论:默认配置 = 单线程、void 异步异常被吞、自调用不走代理。把这三件事在设计和评审时显式处理掉,本章覆盖的线上事故就避开了八成。