Spring Framework 7.0.9 深度剖析-16-异步与调度

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") 里那个字符串是怎么解析成一个 Executor bean 的?
  • 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 :先按类型找唯一的 TaskExecutor bean;找不到/找到多个,再按名字找名为 taskExecutor 的 Executor bean;都找不到,就现场 new 一个 SimpleAsyncTaskExecutor。
  • 调度默认 TaskScheduler :先按类型找唯一的 TaskScheduler bean;再按名字找 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 设计:

  1. "Execute the given task" ------唯一的契约就是"把这个 Runnable 执行掉",至于在哪条线程、同步还是异步、用不用池,统统由实现自己定。这就是它和 JDK Executor 完全对齐的原因:@FunctionalInterface + 继承 Executor,意味着任何一个 JDK Executor 都可以在需要 TaskExecutor 的地方被适配过来。
  2. "might return immediately ... or might block" ------这句非常关键。Spring 刻意没有在接口层承诺"一定异步"。SyncTaskExecutor(同包 SyncTaskExecutor.java)就是一个在调用者线程里同步执行 的实现;而 SimpleAsyncTaskExecutor、ThreadPoolTaskExecutor 才是异步的。"到底是不是异步"这个承诺被下放到了子接口 AsyncTaskExecutor 身上。
  3. 抛出 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);
}

逐段解读:

  1. submit(Runnable) 的默认实现 :把用户任务包成一个 FutureTask(task, null)(返回值固定为 null 的 FutureTask),然后调 execute(future) 把这个 FutureTask 自己作为 Runnable 提交出去。这里有个精妙点------FutureTask 既是 Future(给调用方拿结果)又是 Runnable(给执行器跑),所以"提交任务拿 Future"和"实际跑任务"用的是同一个对象。执行器跑的是 future.run(),它内部会执行用户的 task 并把结果存进 future。
  2. submit(Callable) :同理,包成 FutureTask<T>,注意它调的是 execute(future, TIMEOUT_INDEFINITE)------那个已废弃的带超时的 execute(Runnable, long)。这个 execute(Runnable, long) 在 5.3.16 已废弃,默认实现就是直接 execute(task) 忽略超时。
  3. submitCompletable(6.0 新增) :两个重载都用 CompletableFuture.runAsync(task, this) / FutureUtils.callAsync(task, this)。注意 this 传进去了------因为 AsyncTaskExecutor 本身就是 JDK Executor,所以 CompletableFuture 可以直接把它当异步执行器用。这就是为什么 @Async 方法返回 CompletableFuture 时,框架能拿到一个真正异步的 CompletableFuture。
  4. 两个 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;
}

几个关键设计:

  1. 继承 CustomizableThreadFactory ------它自己就是一个 ThreadFactory(threadFactory = this 的默认值就来自这里)。也就是说,如果你不单独配 ThreadFactory,这个执行器 bean 本身就能当线程工厂用,线程名前缀、优先级这些都由它自己产生。
  2. 同时实现 InitializingBean + DisposableBean + SmartLifecycle + ApplicationListener<ContextClosedEvent> ------这意味着它完整接入了 Spring 容器生命周期:初始化时 afterPropertiesSet()、销毁时 destroy()、还能响应 ContextClosedEvent 做"提前软停机"。
  3. 默认拒绝策略是 AbortPolicy :rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy()。这是 JDK 的默认策略------队列满了直接抛 RejectedExecutionException。很多人以为 Spring 默认是"调用者运行"(CallerRunsPolicy),错了,默认是 Abort。
  4. 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);

逐段解读:

  1. 线程名前缀的自动兜底 :if (!this.threadNamePrefixSet && this.beanName != null)------如果你没手动设线程名前缀,就用 bean 名加一个 - 做前缀。这就是为什么你定义一个 @Bean("myExecutor") ThreadPoolTaskExecutor,它产生的线程默认叫 myExecutor-1、myExecutor-2。这对线上排查线程栈至关重要:给执行器起个有意义的 bean 名,就等于免费获得了可读的线程名。
  2. 虚拟线程分支 :this.virtualThreads 为 true 时,线程工厂不再是自己,而是 new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory()------即把虚拟线程工厂从 VirtualThreadTaskExecutor 里拿出来用。这就是 6.2 之后 setVirtualThreads(true) 的底层入口。
  3. 模板方法 initializeExecutor :真正 new ThreadPoolExecutor/ScheduledThreadPoolExecutor 的动作下放到子类。基类拿到 factory 和拒绝策略后,统一构造 ExecutorLifecycleDelegate(负责 start/stop/isRunning 的暂停恢复语义)。
  4. 谁调 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;
}

逐段拆:

  1. createQueue(this.queueCapacity) 决定队列类型------这是 queueCapacity 语义的关键,下一小节专门讲。
  2. 七个参数原样塞进 JDK ThreadPoolExecutor :corePoolSize、maxPoolSize、keepAliveSeconds(注意单位是 TimeUnit.SECONDS)、queue、threadFactory、rejectedExecutionHandler。没有任何魔法------Spring 的适配就是把属性值对号入座。这意味着 JDK 线程池的全部语义(扩容顺序、回收逻辑、拒绝策略)在这里完整保留。
  3. 匿名子类重写 execute :在这里插入 TaskDecorator。如果配了 decorator,先把 command 装饰一遍,再把"装饰后→原始"的映射记进 decoratedTaskMap,然后 super.execute(decorated)。注意 decorator 是在提交时 (调用线程里)执行的,不是在工作线程里------decorate(command) 这一步拿得到调用方的 ThreadLocal,这正是它能做上下文传递的原因。
  4. beforeExecute/afterExecute 回调桥接回基类 :基类 ExecutorConfigurationSupport 的 beforeExecute/afterExecute 负责 lifecycleDelegate 的 start/stop 计数(实现 isRunning()、stop(Runnable) 等待任务完成的语义)。这里匿名子类把它桥接出去。
  5. 两个可选开关 :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 mimics Executors.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);
	}
}
  1. isActive() 检查 :关闭后(closed 这个 AtomicBoolean 置 true)直接拒绝,抛 TaskRejectedException("... is not active")。
  2. TaskDecorator 先包一层 :和 ThreadPoolTaskExecutor 一样,decorate 在提交线程执行。
  3. 并发限流分支 :isThrottleActive()(即设了 concurrencyLimit)且不是"立即任务"时,先 concurrencyThrottle.beforeAccess() 拿许可,再 doExecute;如果建线程失败,afterAccess() 释放许可再抛异常。
  4. 任务追踪分支 :如果配了 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 提供了两条虚拟线程路径:

  1. 直接 new VirtualThreadTaskExecutor("vt-")------最简单,每任务一条虚拟线程,无池、无限流。
  2. SimpleAsyncTaskExecutor.setVirtualThreads(true)------保留了 SimpleAsyncTaskExecutor 的并发限流、TaskDecorator、终止超时等能力,只是把"new 平台线程"换成"new 虚拟线程"(见 newThread() 里的 virtualThreadDelegate 分支)。
  3. 前面 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;
}

四个属性:

  1. annotation() :自定义哪个注解算"异步标记"。默认是 Annotation.class(即不限制,用内置的 @Async 和 EJB 的 @Asynchronous)。可以换成你自己的注解类型。
  2. proxyTargetClass():是否强制 CGLIB 代理。默认 false(优先 JDK 动态代理)。javadoc 提醒通常建议依赖全局代理配置,而不是在这里单独设。
  3. mode() :advice 怎么织入。默认 PROXY;可选 ASPECTJ(走编译期/加载期织入,连自调用都能拦截)。
  4. 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);
}

逐段:

  1. 注入所有 AsyncConfigurer 类型的 bean。如果一个都没有,executor 和 exceptionHandler 就是"空供应商"。
  2. 有多个直接报错 :"Only one AsyncConfigurer may exist"------全局只能有一个异步配置定制。
  3. 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")。逐行:

  1. bpp.configure(this.executor, this.exceptionHandler)------把 AsyncConfigurer 提供的执行器/异常处理器供应商塞进去。
  2. 读 @EnableAsync(annotation=...):如果用户自定义了异步注解类型,setAsyncAnnotationType。
  3. 读 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);
}

逐段:

  1. 默认识别三种异步注解 :Spring 自己的 @Async,加上 EJB 3.1 的 jakarta.ejb.Asynchronous,再加上 Jakarta Concurrency 的 jakarta.enterprise.concurrent.Asynchronous。后两个用 ClassUtils.forName + catch ClassNotFoundException------classpath 上没有就静默跳过。这就是为什么你的方法标了 EJB 的 @Asynchronous 也能被 Spring 异步化。
  2. buildAdvice (第 159-165 行):new 一个 AnnotationAsyncExecutionInterceptor(传 null 默认执行器),然后 interceptor.configure(executor, exceptionHandler)------把供应商配进去。这个 interceptor 就是真正干活的 advice。
  3. 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());
}

逐段精读:

  1. 解析目标方法 :AopUtils.getTargetClass 拿到被代理的目标类,BridgeMethodResolver.getMostSpecificMethod 处理泛型桥接方法(泛型擦除后,桥接方法上可能没有注解,要找到真正的用户方法)。这一步是为了后面按方法查 executor qualifier。
  2. determineAsyncExecutor(userMethod):算出这次调用该用哪个执行器。这是下一小节的重点。算出 null 直接抛异常。
  3. 把整个"真正调用目标方法"包成一个 Callable<Object> task :注意 invocation.proceed() 在这里------它才是真正执行 @Async 业务方法的那一步,但它被包进了 Callable,将要被丢到执行器的工作线程里跑 。调用线程走到 doSubmit 就立刻返回了。
  4. 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() 是为了拿到真实返回值。
  5. 异常处理 :工作线程里业务方法抛异常,会从 invocation.proceed() 或 future.get() 里冒出来。ExecutionException 拆开取 cause,其他 Throwable 直接拿,都交给 handleError。
  6. 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;
}

逐段:

  1. 缓存 :this.executors 是个 ConcurrentHashMap<Method, AsyncTaskExecutor>------每个方法解析过一次就缓存,下次直接拿。所以 qualifier 解析、bean 查找只发生一次。
  2. getExecutorQualifier(method) :抽象方法,由子类 AnnotationAsyncExecutionInterceptor 实现------就是读 @Async("xxx") 里的 value()。
  3. 占位符解析 :qualifier 可能是 ${my.executor} 这种占位符,用 embeddedValueResolver.resolveStringValue 解析。
  4. 有 qualifier → findQualifiedExecutor:按 qualifier 名在 BeanFactory 里找 Executor bean。
  5. 无 qualifier → this.defaultExecutor.get() :用默认执行器(SingletonSupplier,第一次才真正求值)。
  6. 类型适配 :找到的 targetExecutor 如果本身是 AsyncTaskExecutor 就直接用;否则包一层 TaskExecutorAdapter(把裸 JDK Executor 适配成 Spring AsyncTaskExecutor)。

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;
}

这条查找链是:

  1. beanFactory.getBean(TaskExecutor.class) :按类型找唯一 的 TaskExecutor bean。注意注释:为什么找 TaskExecutor 而不是裸 Executor?因为裸 Executor 会把 ScheduledExecutorService 也匹配上,而那个不适合跑 @Async。
  2. 找到多个 TaskExecutor(NoUniqueBeanDefinitionException)→ 按名字找 "taskExecutor" 的 Executor。
  3. 一个都没有(NoSuchBeanDefinitionException)→ 同样按名字找 "taskExecutor"。
  4. 都找不到 → 返回 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);
	}
}

这就是任务书点名的"返回类型分支",逐支:

  1. CompletableFuture :调 executor.submitCompletable(task)------返回一个真正异步的 CompletableFuture。业务方法里 return CompletableFuture.completedFuture(v),框架用这个异步 CompletableFuture 把结果桥接出去。
  2. Future(含 ListenableFuture 等) :调 executor.submit(task),返回 Future<?>。
  3. void 或 Kotlin Unit :executor.submit(task) 提交,但不把 Future 返回给调用方(return null)。任务照样在后台跑,但调用方拿不到句柄------这就是"void 方法异常被吞"的根源。
  4. 其他类型 :直接抛 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);

三类调度语义泾渭分明:

  1. schedule(task, trigger) :按一个 Trigger(如 CronTrigger)反复触发。trigger 返回 null 表示"不再触发",此时方法返回 null。
  2. scheduleAtFixedRate :固定频率------从上一次开始算间隔。
  3. 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;
}

逐段:

  1. 跳过基础设施 bean :AOP 基础设施 bean、TaskScheduler、ScheduledExecutorService 自己不扫描(避免把调度器 bean 自己当任务扫)。
  2. AopProxyUtils.ultimateTargetClass(bean) :如果 bean 是代理,拿到最终目标类------因为 @Scheduled 标在目标类方法上,不是代理类上。
  3. nonAnnotatedClasses 缓存 :扫过一遍发现没有 @Scheduled 的类加进这个集合,下次直接跳过------性能优化。
  4. MethodIntrospector.selectMethods :反射找出所有带 @Scheduled/@Schedules(可重复注解)的方法,getMergedRepeatableAnnotations 处理 @Schedules 容器注解。
  5. 对每个方法上的每个 @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();
}
  1. 没 ApplicationContext 时 (独立环境),afterSingletonsInstantiated 里就 finishRegistration。
  2. 有 ApplicationContext 时 ,finishRegistration 推迟到 ContextRefreshedEvent(见 onApplicationEvent 第 640-655 行)------等容器完全刷新后再统一调度,这样能给其他 ContextRefreshedEvent 监听器(比如 Spring Batch 的 job 注册)留出机会。
  3. 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);
	}
}

逐段:

  1. resolveMacros (第 222-230 行):把 @yearly/@monthly/@daily/@hourly 等宏替换成标准六段表达式(宏表见第 50-58 行,比如 @daily → 0 0 0 * * *)。
  2. 空格切分后必须恰好 6 段 :fields.length != 6 直接报错。这就是 Spring cron 是 6 段(秒 分 时 日 月 周)的铁律------比 Linux crontab(5 段,无秒)多一个秒位。没有第 7 段"年"位(不像 Quartz)。
  3. 逐字段委托 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;
}

逐段精读:

  1. next 先把种子时间加 1 纳秒 :ChronoUnit.NANOS.addTo(temporal, 1)。为什么?因为 cron 算的是"严格在种子之后的下一次",加 1 纳秒避免"种子时刻本身就匹配"被当作结果(否则会反复返回同一个时刻)。
  2. nextOrSame 循环最多 366 次 :每次 nextOrSameInternal 把时间调整一次,直到"结果不再变化"(收敛)或超 366 次(返回 null,表示一年内无解,比如"2 月 30 日"这种不存在的日期)。
  3. 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 高效找下一个合法位):

  1. 取出当前时间在该字段上的值 current,从 current 开始找下一个被置位的合法值 next。
  2. 如果当前已经合法(next == current),直接返回。
  3. 否则,elapseUntil 把时间推到 next,再重新读 current、找 next,循环直到收敛。
  4. 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();
		}
	}
}

逐段:

  1. 记录 actualExecutionTime(实际开始时刻)。
  2. super.run()------执行真正的任务(DelegatingErrorHandlingRunnable 的 run,里面包着用户任务和 ErrorHandler)。
  3. 记录 completionTime(完成时刻)。
  4. triggerContext.update(计划时间, 实际开始, 完成时间)------把这三个时间写进上下文,供 trigger 下次计算用。
  5. 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 行)的解析链:

  1. resolveSchedulerBean(TaskScheduler.class, byName=false)------按类型找唯一 TaskScheduler。
  2. 多个 → 按名字找 "taskScheduler"。
  3. 没有 TaskScheduler → 找 ScheduledExecutorService 包成 ConcurrentTaskScheduler(同样先按类型唯一、再按名字)。
  4. 都没有 → 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) 被外部调用开始:

  1. 调用方拿到的是代理 :容器初始化 OrderService bean 时,AsyncAnnotationBeanPostProcessor.postProcessAfterInitialization 用 AsyncAnnotationAdvisor 的 pointcut(方法级 @Async 匹配)匹配上,给它包了一层代理(JDK 动态代理或 CGLIB,取决于 proxyTargetClass)。
  2. proxy.createOrder(dto) 进入 AnnotationAsyncExecutionInterceptor.invoke(继承自 AsyncExecutionInterceptor)。
  3. BridgeMethodResolver.getMostSpecificMethod 拿到真实方法 createOrder。
  4. determineAsyncExecutor(createOrder):getExecutorQualifier 读出 "orderExecutor",findQualifiedExecutor(beanFactory, "orderExecutor") 从容器拿到那个 ThreadPoolTaskExecutor,包成 AsyncTaskExecutor(它本来就是),缓存进 this.executors。
  5. 包 Callable :Callable<Object> task = () -> { Object result = invocation.proceed(); ... }。invocation.proceed() 才是真正调 OrderService.createOrder,但它现在只是"被包着",还没执行。
  6. doSubmit(task, executor, CompletableFuture.class) :因为返回类型是 CompletableFuture,走 executor.submitCompletable(task) → CompletableFuture.runAsync(task, executor)。
  7. 调用线程立刻返回 :一个尚未完成的 CompletableFuture。此时方法体还没跑------真正的执行被丢进了 orderExecutor 的线程池。
  8. 工作线程 :orderExecutor 的某个线程被唤醒,执行 task.call():invocation.proceed() → 真正执行 OrderService.createOrder 方法体。方法体里 return CompletableFuture.completedFuture(vo)。回到 invoke 的 Callable:result instanceof Future → future.get() 拿到 vo 并 return。
  9. CompletableFuture 完成 :步骤 6 返回的那个异步 CompletableFuture 被填入结果。调用方在 thenApply/get 里拿到 vo。
  10. 异常路径 :如果步骤 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() { ... }
}

注册阶段(容器启动时):

  1. @EnableScheduling → SchedulingConfiguration 注册 ScheduledAnnotationBeanPostProcessor。
  2. 容器初始化 ReportJob bean 后,postProcessor 的 postProcessAfterInitialization 扫描到 dailyReport 上的 @Scheduled。
  3. processScheduled → processScheduledTask:cron 分支解析 "0 0 2 * * ?"、zone,建 CronTrigger(内含 CronExpression),包成 CronTask,registrar.scheduleCronTask------此时 taskScheduler 还没设,先存进 cronTasks 列表。
  4. 容器刷新完成,ContextRefreshedEvent 触发 finishRegistration:建 TaskSchedulerRouter,没找到 scheduler bean → 兜底 Executors.newSingleThreadScheduledExecutor() 或用你配的 ThreadPoolTaskScheduler。
  5. registrar.afterPropertiesSet() → scheduleTasks():对每个 CronTask 调 taskScheduler.schedule(runnable, trigger)。
  6. ThreadPoolTaskScheduler.schedule(task, trigger):包一个 ReschedulingRunnable(task, trigger, clock, executor, errorHandler),调 .schedule()。
  7. ReschedulingRunnable.schedule():trigger.nextExecution(ctx) → CronTrigger.nextExecution → CronExpression.next(now) 算出下一次触发时刻(比如明天凌晨 2:00:00),executor.schedule(this, delay, NANOS) 提交一个一次性延迟任务。

触发阶段(到了凌晨 2:00):

  1. ScheduledThreadPoolExecutor 的工作线程到点,执行 ReschedulingRunnable.run()。
  2. run():记录 actualExecutionTime → super.run()(DelegatingErrorHandlingRunnable 执行 dailyReport() 方法体)→ 记录 completionTime → triggerContext.update(计划时间, 开始, 完成) → 没被取消就 schedule() 排下一次。
  3. 如果 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),线上很容易漏。生产建议:

  1. 实现 AsyncUncaughtExceptionHandler,把异常上报到监控(钉钉/Prometheus/错误追踪系统),通过 AsyncConfigurer.getAsyncUncaughtExceptionHandler() 配进去。
  2. 对调度任务,实现 ErrorHandler(SchedulingConfigurer.configureTasks 里 registrar.setErrorHandler),做同样的告警。
  3. 返回 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 的完整链路拆了一遍。可以提炼成几个核心判断:

  1. 异步靠代理,调度靠扫描 。@Async 走 AOP:@EnableAsync 注册 AsyncAnnotationBeanPostProcessor,它持有 AsyncAnnotationAdvisor(方法级 @Async pointcut + AnnotationAsyncExecutionInterceptor advice),在 bean 初始化后织入代理;@Scheduled 不走代理,靠 ScheduledAnnotationBeanPostProcessor 在初始化后反射扫描、登记进 ScheduledTaskRegistrar,容器刷新后统一调度。
  2. 执行器适配是纯属性对号入座 。ExecutorConfigurationSupport 管生命周期(initialize/shutdown/优雅停机),ThreadPoolTaskExecutor.initializeExecutor 把 core/max/queue/keepAlive 原样塞进 JDK ThreadPoolExecutor,queueCapacity>0 用有界 LinkedBlockingQueue、=0 用 SynchronousQueue;默认 core=1、queue=无限,等价单线程池。
  3. 默认执行器链路 :唯一 TaskExecutor bean → 名为 taskExecutor 的 Executor bean → new SimpleAsyncTaskExecutor()。@Async("xxx") 的 qualifier 由 getExecutorQualifier 读出、按名查 bean,结果按方法缓存。
  4. 返回类型严格分支 :CompletableFuture → submitCompletable;Future → submit;void/Kotlin Unit → submit 但不返回句柄(异常进 AsyncUncaughtExceptionHandler,默认只打日志);其他类型直接报错。
  5. 调度器解析链 :TaskSchedulerRouter 先按类型找 TaskScheduler,再按名 taskScheduler,再找 ScheduledExecutorService,都没有才 newSingleThreadScheduledExecutor()------单线程兜底是最大坑。
  6. cron 是 6 段、倒序字段位运算匹配 :CronExpression.next 先加 1 纳秒种子,再倒序(周月日时分秒)逐字段 nextOrSame,用 BitsCronField 位图高效找下一个合法值,最多迭代 366 次收敛。
  7. 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 异步异常被吞、自调用不走代理。把这三件事在设计和评审时显式处理掉,本章覆盖的线上事故就避开了八成。

相关推荐
高频因子挖掘机1 小时前
500只股票 × 多年日线:数据获取与本地存储如何分工?
后端·github·api
wflynn1 小时前
GitHub 日榜趋势速报 | 2026-10-16
开源·github
张立立1 小时前
取名不想只靠翻字典?我做了个诗词取名工具,还能分析三才五格
后端
十年Java程序媛1 小时前
Java 多线程基础与锁机制|进程线程、synchronized、volatile、Lock 面试全梳理
java·spring boot·面试
弈栈录1 小时前
大模型服务抽象:兼容 OpenAI、通义千问与本地模型
java·后端·架构
鶴哥只手遮天1 小时前
Spring Framework 7.0.9 深度剖析-06-Bean 生命周期与依赖注入原理
java·后端·开源
唐钰小宝B1 小时前
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
java·开发语言·windows
golang学习记1 小时前
最新版VS Code开发Spring Boot项目保姆级教程
java·spring boot·vscode·后端
高频因子挖掘机2 小时前
别急着搭数据平台:量化策略开发如何用最小闭环验证想法
后端·github·api