文章目录
-
- [一、 背景](#一、 背景)
- [二、 核心概念:为什么要异步?](#二、 核心概念:为什么要异步?)
- [三、 三种核心的异步处理方式](#三、 三种核心的异步处理方式)
-
- [1. `Callable` (最简单的异步)](#1.
Callable(最简单的异步)) - [2. `DeferredResult` (解耦生产者与消费者)](#2.
DeferredResult(解耦生产者与消费者)) - [3. `ResponseBodyEmitter` / `SseEmitter` (流式响应)](#3.
ResponseBodyEmitter/SseEmitter(流式响应)) - [4. 响应式类型 (`Flux` / `Mono`)](#4. 响应式类型 (
Flux/Mono))
- [1. `Callable` (最简单的异步)](#1.
- [四、 深入原理:请求生命周期](#四、 深入原理:请求生命周期)
- [五、 Spring Boot 3 的新特性:虚拟线程](#五、 Spring Boot 3 的新特性:虚拟线程)
- [六、 配置与异常处理](#六、 配置与异常处理)
-
- [1. 异步请求超时配置](#1. 异步请求超时配置)
- [2. 异步拦截器](#2. 异步拦截器)
- [3. 异步请求中的异常处理](#3. 异步请求中的异常处理)
- [七、 总结与避坑指南](#七、 总结与避坑指南)
- [八、 参考文档](#八、 参考文档)
一、 背景
在 Spring Boot 3 (基于 Spring Framework 6 和 Jakarta EE 10) 中,Spring MVC 对异步请求的支持已经达到了非常成熟且深度的整合水平。这不仅仅是简单的"多线程",而是建立在 Servlet 3.1+ 规范之上的完整异步处理模型。
以下是对 Spring MVC 异步请求处理的全面讲解:
二、 核心概念:为什么要异步?
在传统的同步 Servlet 模型中:
- 请求到达,Tomcat 线程池分配一个线程。
- 线程执行业务逻辑(如查数据库、调用外部 API)。
- 线程在等待 IO 响应期间被阻塞。
- 响应返回,线程释放。
痛点:高并发下,线程池会被大量等待中的请求耗尽,导致服务无法响应新请求。
Spring MVC 异步处理:
- 请求到达,Tomcat 线程分配。
- 线程触发耗时操作(如返回
Callable或DeferredResult),立即退出方法,释放回线程池。 - 耗时操作在独立线程或 IO 多路复用机制中执行。
- 执行完毕后,触发回调,重新请求一个 Tomcat 线程来处理响应。
优势:Tomcat 线程不再被 IO 阻塞,极大地提高了服务器的吞吐能力。
三、 三种核心的异步处理方式
Spring MVC 提供了三种主要的异步返回类型,分别对应不同的业务场景:
1. Callable (最简单的异步)
适用场景 :需要在独立线程中执行的耗时任务,Spring 内部会使用配置好的 TaskExecutor 来执行它。
java
@GetMapping("/callable")
public Callable<String> processData() {
// 这个 lambda 会由 Spring 提交到线程池执行
return () -> {
Thread.sleep(2000); // 模拟耗时
return "Callable Result";
};
}
- 流程 :主线程返回
Callable-> Spring 调用线程池执行 -> 等待结果 -> 分发结果给容器。 - 注意 :在 Spring Boot 3.2+ 支持虚拟线程的环境下,
Callable可以配置为在虚拟线程中执行,从而避免阻塞平台线程。
2. DeferredResult (解耦生产者与消费者)
适用场景 :异步编程模型的核心。适用于需要在不同线程(如消息队列监听器、外部事件)中设置结果的场景。
java
// 1. 创建一个 DeferredResult 对象,设置超时时间
DeferredResult<String> result = new DeferredResult<>(5000L);
// 2. 模拟在另一个线程(如 MQ 消费者、定时任务)中设置结果
new Thread(() -> {
try { Thread.sleep(2000); } catch (Exception e) {}
// 当这里调用 setResult 时,Servlet 容器会重新唤醒请求处理
result.setResult("Hello World");
}).start();
// 3. Controller 方法立即返回,此时还没有结果
return result;
- 优势:完全解耦。请求线程和处理线程没有任何直接关联,非常适合网关转发、长轮询等场景。
3. ResponseBodyEmitter / SseEmitter (流式响应)
适用场景 :需要分批次、多次发送数据给客户端(如 AI 对话、实时日志推送)。SseEmitter 是 ResponseBodyEmitter 的子类,专门用于 Server-Sent Events。
java
@GetMapping("/stream")
public SseEmitter streamData() {
SseEmitter emitter = new SseEmitter();
// 在其他线程中向 emitter 发送数据
executorService.execute(() -> {
try {
emitter.send("First chunk");
Thread.sleep(1000);
emitter.send("Second chunk");
emitter.complete(); // 结束流
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
4. 响应式类型 (Flux / Mono)
这是 Spring Boot 3 混合架构的亮点。如果你的 Controller 返回 Flux 或 Mono,Spring MVC 会自动将其适配为异步处理。
- 返回
Mono:类似于DeferredResult或Callable。 - 返回
Flux:自动转换为ResponseBodyEmitter流式处理。
四、 深入原理:请求生命周期
当异步请求发生时,Servlet 容器会经历以下阶段:
-
初始分发 (REQUEST):
- Tomcat 接收请求。
- Spring MVC
DispatcherServlet处理请求。 - Controller 返回异步类型(如
Flux)。 - 关键动作 :调用
request.startAsync(),开启异步模式。 - 主线程结束,返回 Tomcat 线程池。
-
异步阶段:
- 业务逻辑在后台执行(Reactive 线程池或自定义线程池)。
- 此阶段 不占用 Tomcat 线程。
-
异步分发 (ASYNC):
- 当后台任务完成(或
Flux发射数据时),Servlet 容器会发起一个新的分发。 - 这个分发的类型是
DispatcherType.ASYNC。 DispatcherServlet再次介入,负责将结果写回响应流。
- 当后台任务完成(或
重点:过滤器 和拦截器 的行为差异。
- 过滤器 :默认只在 REQUEST 阶段执行。如果想拦截 ASYNC 阶段,需在
web.xml或FilterRegistrationBean中显式配置DispatcherType.ASYNC。 - 拦截器 :默认会拦截所有阶段 (包括 ASYNC)。这就是为什么你的
SaInterceptor会在流式数据写回时被再次触发的原因。
五、 Spring Boot 3 的新特性:虚拟线程
在 Spring Boot 3.2+ 中,如果运行在 JDK 21 上,异步处理有了新玩法。
传统上,异步编程是为了解决线程阻塞问题,但代码写起来复杂(回调地狱)。
虚拟线程 让你可以重新写同步阻塞 的代码,但获得异步非阻塞的性能:
java
// Spring Boot 3.2 + JDK 21
// 开启虚拟线程后,这个简单的阻塞代码不会阻塞 Tomcat 的平台线程
@GetMapping("/blocking-with-virtual")
public String blocking() throws InterruptedException {
Thread.sleep(1000); // 阻塞的是虚拟线程,底层 OS 线程不阻塞
return "Done";
}
这改变了异步请求的格局:
- 简单业务 :使用虚拟线程 + 同步代码,不再需要
Flux或Callable。 - 流式业务 :依然需要
Flux或SseEmitter,因为这是流式数据模型,不仅仅是线程模型。
六、 配置与异常处理
1. 异步请求超时配置
异步请求不能无限等待,默认超时通常是 30 秒。
yaml
spring:
mvc:
async:
request-timeout: 30000 # 毫秒
2. 异步拦截器
为了处理异步生命周期中的事件(如超时、完成),Spring 提供了 AsyncHandlerInterceptor。
java
public class MyAsyncInterceptor implements AsyncHandlerInterceptor {
@Override
public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 在异步处理开始之后调用(主线程退出前)
// 可以在这里清理 ThreadLocal,或者记录日志
System.out.println("Async handling started, main thread released.");
}
}
3. 异步请求中的异常处理
异步阶段抛出的异常,会被 Spring MVC 捕获,并通过 ASYNC 分发转发给异常处理器。
- 如果返回
DeferredResult,可以通过result.setErrorResult(e)处理。 - 如果返回
Flux,异常会导致流终止,并被全局异常处理器捕获(前提是异常处理器能处理 SSE 类型的响应,否则会报 Converter 错误)。
七、 总结与避坑指南
在 Spring Boot 3 中使用 Spring MVC 异步请求:
- 首选响应式类型 :如果涉及到流式输出(SSE),
Flux是最优雅的方案。 - 警惕 ThreadLocal :任何基于
ThreadLocal的组件(Sa-Token, Spring Security, MDC 日志追踪)在异步分发阶段都会失效。- 解决方案 :要么使用
AsyncHandlerInterceptor手动传播上下文;要么在主线程入口处提取数据,显式传参(推荐)。
- 解决方案 :要么使用
- 拦截器配置 :必须显式排除
DispatcherType.ASYNC,除非你明确需要在数据写回时做拦截。 - 过滤器配置 :如果你需要在 ASYNC 阶段也执行某些过滤器(如重新绑定上下文),记得配置
dispatcherTypes包含ASYNC。
理解了 "REQUEST -> startAsync -> ASYNC Dispatch" 这一流程,你就掌握了 Spring MVC 异步处理的核心密码。