Spring Web MVC的异步请求解读

文章目录

    • [一、 背景](#一、 背景)
    • [二、 核心概念:为什么要异步?](#二、 核心概念:为什么要异步?)
    • [三、 三种核心的异步处理方式](#三、 三种核心的异步处理方式)
      • [1. `Callable` (最简单的异步)](#1. Callable (最简单的异步))
      • [2. `DeferredResult` (解耦生产者与消费者)](#2. DeferredResult (解耦生产者与消费者))
      • [3. `ResponseBodyEmitter` / `SseEmitter` (流式响应)](#3. ResponseBodyEmitter / SseEmitter (流式响应))
      • [4. 响应式类型 (`Flux` / `Mono`)](#4. 响应式类型 (Flux / Mono))
    • [四、 深入原理:请求生命周期](#四、 深入原理:请求生命周期)
    • [五、 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 模型中:

  1. 请求到达,Tomcat 线程池分配一个线程。
  2. 线程执行业务逻辑(如查数据库、调用外部 API)。
  3. 线程在等待 IO 响应期间被阻塞
  4. 响应返回,线程释放。

痛点:高并发下,线程池会被大量等待中的请求耗尽,导致服务无法响应新请求。

Spring MVC 异步处理

  1. 请求到达,Tomcat 线程分配。
  2. 线程触发耗时操作(如返回 CallableDeferredResult),立即退出方法,释放回线程池。
  3. 耗时操作在独立线程或 IO 多路复用机制中执行。
  4. 执行完毕后,触发回调,重新请求一个 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 对话、实时日志推送)。SseEmitterResponseBodyEmitter 的子类,专门用于 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 返回 FluxMono,Spring MVC 会自动将其适配为异步处理。

  • 返回 Mono:类似于 DeferredResultCallable
  • 返回 Flux:自动转换为 ResponseBodyEmitter 流式处理。

四、 深入原理:请求生命周期

当异步请求发生时,Servlet 容器会经历以下阶段:

  1. 初始分发 (REQUEST)

    • Tomcat 接收请求。
    • Spring MVC DispatcherServlet 处理请求。
    • Controller 返回异步类型(如 Flux)。
    • 关键动作 :调用 request.startAsync(),开启异步模式。
    • 主线程结束,返回 Tomcat 线程池。
  2. 异步阶段

    • 业务逻辑在后台执行(Reactive 线程池或自定义线程池)。
    • 此阶段 不占用 Tomcat 线程
  3. 异步分发 (ASYNC)

    • 当后台任务完成(或 Flux 发射数据时),Servlet 容器会发起一个新的分发
    • 这个分发的类型是 DispatcherType.ASYNC
    • DispatcherServlet 再次介入,负责将结果写回响应流。

重点:过滤器 和拦截器 的行为差异。

  • 过滤器 :默认只在 REQUEST 阶段执行。如果想拦截 ASYNC 阶段,需在 web.xmlFilterRegistrationBean 中显式配置 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";
}

这改变了异步请求的格局:

  • 简单业务 :使用虚拟线程 + 同步代码,不再需要 FluxCallable
  • 流式业务 :依然需要 FluxSseEmitter,因为这是流式数据模型,不仅仅是线程模型。

六、 配置与异常处理

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 异步请求:

  1. 首选响应式类型 :如果涉及到流式输出(SSE),Flux 是最优雅的方案。
  2. 警惕 ThreadLocal :任何基于 ThreadLocal 的组件(Sa-Token, Spring Security, MDC 日志追踪)在异步分发阶段都会失效。
    • 解决方案 :要么使用 AsyncHandlerInterceptor 手动传播上下文;要么在主线程入口处提取数据,显式传参(推荐)。
  3. 拦截器配置 :必须显式排除 DispatcherType.ASYNC,除非你明确需要在数据写回时做拦截。
  4. 过滤器配置 :如果你需要在 ASYNC 阶段也执行某些过滤器(如重新绑定上下文),记得配置 dispatcherTypes 包含 ASYNC

理解了 "REQUEST -> startAsync -> ASYNC Dispatch" 这一流程,你就掌握了 Spring MVC 异步处理的核心密码。


八、 参考文档

相关推荐
PPPPickup2 小时前
easymall---人工客服(SpringAI版)无敌复用框架!
spring·ai编程
Thomas.Sir2 小时前
SpringMVC 工作原理深入解析
spring·设计模式·mvc·spring mvc
Mr.45672 小时前
SpringBoot整合RabbitMQ进阶:告别繁琐,用统一配置管理所有队列与交换机
spring boot·rabbitmq
sanggou2 小时前
Spring Cloud负载均衡组件到底是哪一个?
spring·spring cloud·负载均衡
xiaoye37083 小时前
Spring Bean 生命周期自定义扩展示例
java·spring boot·spring
弹简特3 小时前
【JavaEE17-后端部分】 MyBatis 入门第一篇:准备工作与第一个查询
spring boot·spring·mybatis
Java水解3 小时前
Spring Boot 数据缓存与性能优化
spring boot·后端
她说..3 小时前
Redis 中常用的操作方法
java·数据库·spring boot·redis·缓存
Voyager_43 小时前
吃透设计模式:从原理到落地(如何选型),Java/Spring开发场景
java·spring·设计模式