轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

  • Spring

    • 基础

    • 框架

      • Spring容器初始化过程和Bean生命周期探究
      • Spring容器:从依赖管理到注解配置全解析
      • Spring事件探秘
      • Spring AOP的应用
      • Spring 事务管理
      • Spring中的资源访问:Resource接口
      • Spring中的验证、数据绑定和类型转换
      • Spring表达式语言(SpELl)
      • Spring中的属性占位符
      • Spring数据缓冲区与编解码器详解
      • Spring对于原生镜像和AOT的支持
      • Spring中的数据访问:JDBC、R2DBC、ORM、Object-XML
      • Spring中的Web访问:Servlet API支持
      • Spring中的Web访问:WebSocket支持
      • Spring中的Web访问:响应式栈 WebFlux
        • 一、WebFlux
          • 1、章节总结
          • 2、概述
          • 3、定义“响应式”
          • 4、响应式 API
          • 5、编程模型
          • 6、适用性
          • 7、服务器
          • 8、性能
          • 9、并发模型
          • 9.1、调用阻塞式 API
          • 9.2、可变状态
          • 9.3、线程模型
          • 9.4、配置
          • 10、响应式核心
          • 11、HttpHandler
          • 11.1、Netty
          • 11.2、Undertow
          • 11.3、Tomcat
          • 11.4、Jetty
          • 11.5、容器
          • 12、API
          • 12.1、特殊的 Bean 类型
          • 12.2、表单数据
          • 12.3、多部分数据
          • 12.4、转发头
          • 12.5、非标准头
          • a、- Forwarded - Host
          • b、- Forwarded - Port
          • c、- Forwarded - Proto
          • d、- Forwarded - Ssl
          • e、- Forwarded - Prefix
          • 12.6、ForwardedHeaderTransformer
          • 12.7、安全考虑
          • 13、过滤器
          • 13.1、CORS
          • 13.2、处理程序
          • 14、异常
          • 15、编解码器
          • 15.1、JSON
          • 15.2、表单数据
          • 15.3、多部分
          • 15.4、协议缓冲区
          • 15.5、限制
          • 15.6、流式处理
          • 15.7、DataBuffer
          • 16、日志记录
          • 16.1、日志 ID
          • 16.2、敏感数据
          • 16.3、追加器
          • 16.4、自定义编解码器
          • 17、DispatcherHandler
          • 18、特殊Bean类型
          • 19、WebFlux配置
          • 20、处理流程
          • 21、结果处理
          • 22、异常处理
          • 23、视图解析
          • 23.1、处理过程
          • 23.2、重定向
          • 23.3、内容协商
          • 24、注解式控制器
          • 24.1、@Controller
          • 25、代理
          • 25.1、请求映射
          • 26、@RequestMapping
          • 27、模式
          • 28、模式比较
          • 29、可消费的媒体类型
          • 30、可生产的媒体类型
          • 31、参数和请求头
          • 32、HEAD、OPTIONS
          • 33、自定义注解
          • 34、显式注册
          • 35、@HttpExchange
          • 35.1、处理方法
          • a、章节概要
          • b、方法参数
          • c、返回值
          • d、类型转换
          • e、矩阵变量
          • f、@RequestParam
          • 提示
          • g、@RequestHeader
          • h、@CookieValue
          • i、@ModelAttribute
          • j、@SessionAttributes
          • k、@SessionAttribute
          • l、@RequestAttribute
          • m、多部分内容
          • PartEvent
          • n、@RequestBody
          • o、HttpEntity
          • p、@ResponseBody
          • q、ResponseEntity
          • r、JSON
          • 35.2、视图
          • 35.3、Model
          • 35.4、DataBinder
          • 36、前置说明
          • 37、模型设计
          • 37.1、验证
          • 37.2、异常
          • 38、媒体类型映射
          • 39、方法参数
          • 40、返回值
          • 40.1、控制器通知
          • 41、函数式端点
          • 41.1、概述
          • 41.2、HandlerFunction
          • a、ServerRequest
          • b、ServerResponse
          • c、处理函数类
          • d、验证
          • 41.3、RouterFunction
          • a、谓词
          • b、路由
          • c、嵌套路由
          • 41.4、提供资源服务
          • a、重定向到资源
          • b、从根位置提供资源服务
          • 41.5、运行服务器
          • 41.6、过滤处理函数
          • 42、链接
          • 42.1、UriComponents
          • 42.2、UriBuilder
          • 42.3、解析
          • 42.4、编码
          • 43、CORS
          • 43.1、简介
          • 43.2、处理过程
          • 43.3、带凭证的请求
          • 43.4、@CrossOrigin
          • 43.5、全局配置
          • 43.6、WebFilter
          • 44、错误响应
          • 44.1、渲染
          • 44.2、非标准字段
          • 44.3、自定义和国际化
          • 44.4、客户端处理
          • 45、Security
          • 46、缓存
          • 46.1、CacheControl
          • 46.2、控制器
          • 46.3、静态资源
          • 47、视图技术
          • 47.1、Thymeleaf
          • 47.2、FreeMarker
          • a、视图配置
          • b、配置
          • c、表单处理
          • 绑定宏
          • 表单宏
          • 47.3、脚本视图
          • a、要求
          • b、脚本模板
          • 47.4、片段
          • 47.5、和 XML
          • 48、配置
          • 48.1、启用 WebFlux 配置
          • 48.2、配置 API
          • 48.3、转换与格式化
          • 48.4、验证
          • 48.5、内容类型解析器
          • 48.6、消息编解码器
          • 48.7、视图解析器
          • 48.8、静态资源
          • 48.9、路径匹配
          • 48.10、阻塞执行
          • 48.11、WebSocketService
          • 48.12、高级配置模式
          • 49、HTTP/2
        • 二、WebClient
          • 1、章节总结
          • 2、配置
          • 3、最大内存大小(MaxInMemorySize)
          • 4、Netty
          • 4.1、资源
          • 4.2、超时设置
          • 5、HttpClient
          • 6、Jetty
          • 7、HttpComponents
          • 8、retrieve()
          • 9、交换
          • 10、请求体
          • 10.1、表单数据
          • 10.2、多部分数据
          • a、PartEvent
          • 11、过滤器
          • 12、属性
          • 13、上下文
          • 14、同步使用
          • 15、测试
        • 三、HTTP接口客户端
        • 四、WebSockets
          • 1、WebSocket简介
          • 1.1、HTTP与WebSocket的对比
          • 1.2、何时使用WebSocket
          • 2、API
          • 2.1、服务器
          • 2.2、WebSocketHandler
          • 2.3、DataBuffer
          • 2.4、握手
          • 2.5、服务器配置
          • 2.6、CORS
          • 2.7、客户端
        • 五、测试
          • 1、RSocket
          • 1.1、概述
          • a、协议
          • b、实现
          • c、支持
          • 1.2、RSocketRequester
          • a、客户端请求者
          • 连接设置
          • 策略
          • 客户端响应者
          • 高级用法
          • b、服务器请求者
          • c、请求
          • 1.3、注解响应者
          • a、服务器响应者
          • b、客户端响应者
          • c、@MessageMapping
          • d、@RSocketExchange
          • e、@ConnectMapping
          • 1.4、MetadataExtractor
          • 1.5、接口
          • a、方法参数
          • b、返回值
        • 六、响应式库
      • Spring中的集成测试与单元测试
      • Spring与多种技术的集成
      • Spring框架版本新特性
    • Spring Boot

    • 集成

  • 其他语言

  • 工具

  • 后端
  • Spring
  • 框架
轩辕李
2024-06-28
目录

Spring中的Web访问:响应式栈 WebFlux

本文涵盖了对基于 响应式流(Reactive Streams) (opens new window) API构建的响应式栈Web应用程序的支持,这些应用程序可运行在非阻塞服务器上,例如Netty、Undertow和Servlet容器。各章节分别介绍了 Spring WebFlux 框架、响应式的 WebClient、测试 支持,以及 响应式库。

# 一、WebFlux

Spring框架中最初包含的Web框架Spring Web MVC,是专门为Servlet API和Servlet容器设计的。响应式栈Web框架Spring WebFlux在5.0版本中被引入。它完全是非阻塞的,支持响应式流 (opens new window)的背压机制,并且可以运行在如Netty、Undertow和Servlet容器等服务器上。

Spring框架中的这两个Web框架的名称与其对应的源模块名称一致(spring - webmvc (opens new window)和spring - webflux (opens new window)),并且可以并存。每个模块都是可选的。应用程序可以使用其中一个模块,或者在某些情况下同时使用两个模块,例如,使用Spring MVC控制器搭配响应式的WebClient。

# 1、章节总结

  • 概述
  • 响应式核心
  • DispatcherHandler
  • 注解式控制器
  • 函数式端点
  • URI链接
  • 跨域资源共享(CORS)
  • 错误响应
  • Web安全
  • HTTP缓存
  • 视图技术
  • WebFlux配置
  • HTTP/2

# 2、概述

为什么要创建 Spring WebFlux?

部分原因在于需要一个非阻塞的 Web 栈,以便用少量线程处理并发,并凭借更少的硬件资源实现扩展。Servlet 的非阻塞 I/O 与 Servlet API 的其他部分有所背离,Servlet API 的契约要么是同步的(如 Filter、Servlet),要么是阻塞的(如 getParameter、getPart)。这便是推动创建一个新的通用 API 作为任何非阻塞运行时基础的动力。这一点非常重要,因为像 Netty 这样的服务器在异步、非阻塞领域已广为人知。

另一个原因则与函数式编程相关。正如 Java 5 引入注解创造了诸多机会(例如带注解的 REST 控制器或单元测试)一样,Java 8 引入的 lambda 表达式为 Java 中的函数式 API 提供了发展契机。这对非阻塞应用和延续风格的 API(如 CompletableFuture 和 ReactiveX (opens new window) 所推广的)是一大利好,这些 API 允许声明式地组合异步逻辑。从编程模型层面来看,Java 8 使得 Spring WebFlux 能够在带注解的控制器之外,还提供函数式 Web 端点。

# 3、定义“响应式”

我们已经提及了“非阻塞”和“函数式”,那么“响应式”究竟是什么意思呢?

“响应式”这个术语指的是围绕对变化做出响应而构建的编程模型 —— 网络组件对 I/O 事件做出响应,UI 控制器对鼠标事件做出响应,诸如此类。从这个意义上说,非阻塞就是响应式的,因为我们不再处于阻塞状态,而是在操作完成或数据可用时对通知做出响应。

Spring 团队还将另一个重要机制与“响应式”相关联,那就是非阻塞背压。在同步的命令式代码中,阻塞调用是一种自然的背压形式,它会强制调用者等待。而在非阻塞代码中,控制事件速率就变得至关重要,这样可以避免快速的生产者压垮其目标接收方。

Reactive Streams 是一个小型规范 (opens new window)(Java 9 中也采用 (opens new window)),它定义了带有背压的异步组件之间的交互方式。例如,一个数据仓库(充当发布者 (opens new window))可以生成数据,而一个 HTTP 服务器(充当订阅者 (opens new window))可以将这些数据写入响应中。Reactive Streams 的主要目的是让订阅者控制发布者生成数据的速度。

# 4、响应式 API

Reactive Streams 在互操作性方面发挥着重要作用。它对库和基础设施组件很有吸引力,但作为应用程序 API 的实用性较低,因为它的级别太低。应用程序需要一个更高级、更丰富的函数式 API 来组合异步逻辑 —— 类似于 Java 8 的 Stream API,但不限于集合。这正是响应式库发挥作用的地方。

Reactor (opens new window) 是 Spring WebFlux 首选的响应式库。它提供了 Mono (opens new window) 和 Flux (opens new window) API 类型,通过一组丰富的操作符(与 ReactiveX 的操作符词汇表 (opens new window)一致)来处理 0..1(Mono)和 0..N(Flux)的数据序列。Reactor 是一个 Reactive Streams 库,因此它的所有操作符都支持非阻塞背压。Reactor 主要专注于服务器端 Java,它与 Spring 紧密合作开发。

WebFlux 需要 Reactor 作为核心依赖,但它可以通过 Reactive Streams 与其他响应式库进行互操作。一般来说,WebFlux API 接受一个普通的 Publisher 作为输入,在内部将其适配为 Reactor 类型,使用该类型进行处理,并返回 Flux 或 Mono 作为输出。因此,你可以传递任何 Publisher 作为输入,并且可以对输出应用操作,但你需要将输出适配为另一个响应式库使用。只要可行(例如带注解的控制器),WebFlux 会透明地适配 RxJava 或其他响应式库的使用。更多详细信息请参阅响应式库。

注意:除了响应式 API,WebFlux 还可以与 Kotlin 中的 协程 API 一起使用,协程 API 提供了一种更命令式的编程风格。

# 5、编程模型

spring-web 模块包含了 Spring WebFlux 所基于的响应式基础,包括 HTTP 抽象、支持的服务器的 Reactive Streams 适配器、编解码器以及一个核心的 WebHandler API,它类似于 Servlet API,但具有非阻塞的契约。

在此基础上,Spring WebFlux 提供了两种编程模型供选择:

  • 带注解的控制器:与 Spring MVC 保持一致,基于 spring-web 模块中的相同注解。Spring MVC 和 WebFlux 控制器都支持响应式(Reactor 和 RxJava)返回类型,因此很难区分它们。一个显著的区别是 WebFlux 还支持响应式的 @RequestBody 参数。
  • 函数式端点:基于 lambda 的轻量级函数式编程模型。你可以将其视为一个小型库或一组实用工具,应用程序可以使用它们来路由和处理请求。与带注解的控制器的主要区别在于,应用程序需要从头到尾负责请求处理,而不是通过注解声明意图并等待回调。

# 6、适用性

应该选择 Spring MVC 还是 WebFlux 呢?

这是一个很自然会问到的问题,但这样的提问方式会造成一种不合理的二分法。实际上,两者可以协同工作,以扩展可用选项的范围。这两者的设计旨在保持连续性和一致性,它们可以并存使用,而且双方的反馈都对彼此有益。下图展示了两者之间的关系、它们的共同点以及各自独特的支持功能: image

我们建议你考虑以下具体要点:

  • 如果你现有的 Spring MVC 应用运行良好,那么无需更改。命令式编程是编写、理解和调试代码最简单的方式。你可以选择的库最多,因为从历史上看,大多数库都是阻塞式的。
  • 如果你正在寻找一个非阻塞的 Web 栈,Spring WebFlux 不仅能提供与该领域其他框架相同的执行模型优势,还让你可以从多种服务器(Netty、Tomcat、Jetty、Undertow 和 Servlet 容器)中挑选,编程模型也有选择空间(带注解的控制器和函数式 Web 端点),响应式库方面同样有多种选择(Reactor、RxJava 等)。
  • 要是你对用于 Java 8 lambda 或 Kotlin 的轻量级函数式 Web 框架感兴趣,就可以使用 Spring WebFlux 的函数式 Web 端点。对于需求没那么复杂的小型应用或微服务来说,这种选择也不错,因为能获得更高的透明度和控制权。
  • 在微服务架构中,你的应用可以是混合的,既可以有 Spring MVC 或 Spring WebFlux 控制器,也能使用 Spring WebFlux 函数式端点。两个框架都支持基于注解的编程模型,这让知识复用变得容易,同时你也能根据具体任务选对工具。
  • 评估应用的一个简单办法是查看它的依赖关系。如果你要使用阻塞式持久化 API(如 JPA、JDBC)或网络 API,至少对于常见架构而言,Spring MVC 是最佳选择。虽然从技术上讲,使用 Reactor 和 RxJava 可以在单独的线程上进行阻塞调用,但这样就无法充分发挥非阻塞 Web 栈的优势。
  • 如果你有一个会调用远程服务的 Spring MVC 应用,可以试试响应式 WebClient。你可以从 Spring MVC 控制器方法中直接返回响应式类型(Reactor、RxJava 等)。每次调用的延迟越高,或者调用之间的相互依赖越紧密,得到的好处就越明显。Spring MVC 控制器也能调用其他响应式组件。
  • 如果你的团队规模较大,要记住转向非阻塞、函数式和声明式编程的学习曲线很陡峭。一个不用完全切换的实用启动方式是使用响应式 WebClient。在此之后,先从小规模开始,然后评估收益。我们预计,对于大量应用而言,这种转变是不必要的。如果你不确定要关注哪些好处,可以先了解非阻塞 I/O 的工作原理(比如单线程 Node.js 中的并发情况)及其影响。

# 7、服务器

Spring WebFlux 支持在 Tomcat、Jetty、Servlet 容器以及 Netty 和 Undertow 等非 Servlet 运行时环境中运行。所有服务器都被适配到一个底层的通用 API ,这样高层的编程模型就能在不同服务器上得到支持。

Spring WebFlux 本身没有内置启动或停止服务器的功能。不过,你可以轻松地组装一个由 Spring 配置和 WebFlux 基础设施构成的应用,并用几行代码就运行起来。

Spring Boot 有一个 WebFlux 启动器可以自动完成这些步骤。默认情况下,启动器使用 Netty,但你通过更改 Maven 或 Gradle 依赖来切换到 Tomcat、Jetty 或 Undertow 也很容易。Spring Boot 默认选择 Netty,是因为它在异步、非阻塞领域使用更广泛,还能让客户端和服务器共享资源。

Tomcat 和 Jetty 既可以和 Spring MVC 搭配使用,也能用于 WebFlux。但要记住,使用方式有很大不同。Spring MVC 依赖 Servlet 阻塞 I/O,并且如果应用需要,它们可以直接使用 Servlet API。而 Spring WebFlux 依赖 Servlet 非阻塞 I/O,它通过一个底层适配器来使用 Servlet API,不会直接暴露 API 供应用使用。

注意:强烈建议不要在 WebFlux 应用中映射 Servlet 过滤器或直接操作 Servlet API。由于上述原因,在同一上下文中混合使用阻塞 I/O 和非阻塞 I/O 会导致运行时问题。

对于 Undertow,Spring WebFlux 直接使用 Undertow API,而不涉及 Servlet API。

# 8、性能

性能有很多特性和含义。通常情况下,响应式和非阻塞方式并不能让应用运行得更快。不过在某些情况下是可以的,比如使用 WebClient 并行执行远程调用。但以非阻塞的方式实现功能需要做更多工作,这可能会稍微增加处理时间。

响应式和非阻塞的主要预期好处是能够通过少量、固定数量的线程和较少的内存实现扩展。这能让应用在负载下更有弹性,因为其扩展方式更具可预测性。不过,要观察到这些好处,你需要有一定的延迟(包括一些缓慢且不可预测的网络 I/O 操作)。这正是响应式栈开始展现优势的地方,而且差异可能非常显著。

# 9、并发模型

Spring MVC 和 Spring WebFlux 都支持带注解的控制器,但在并发模型以及对阻塞和线程的默认假设上存在关键差异。

在 Spring MVC(以及一般的 Servlet 应用)中,假设应用可以阻塞当前线程(例如进行远程调用时)。因此,Servlet 容器使用一个大型线程池来应对请求处理过程中可能出现的阻塞情况。

而在 Spring WebFlux(以及一般的非阻塞服务器)中,假设应用不会阻塞。所以,非阻塞服务器使用一个小型的、固定大小的线程池(事件循环工作线程)来处理请求。

提示:“扩展”和“少量线程”听起来可能相互矛盾,但从不阻塞当前线程(而是依靠回调)意味着你不需要额外的线程,因为不存在需要处理的阻塞调用。

# 9.1、调用阻塞式 API

如果你确实需要使用阻塞式库怎么办呢?Reactor 和 RxJava 都提供了 publishOn 操作符,可将处理继续到另一个线程上。这意味着有一个简单的解决办法。不过要记住,阻塞式 API 并不适合这种并发模型。

# 9.2、可变状态

在 Reactor 和 RxJava 中,你通过操作符声明逻辑。在运行时,会形成一个响应式管道,数据在不同的阶段按顺序处理。这样做的一个关键好处是,应用无需保护可变状态,因为管道内的应用代码永远不会同时被调用。

# 9.3、线程模型

在运行 Spring WebFlux 的服务器上,你会看到哪些线程呢?

  • 在一个“纯粹”的 Spring WebFlux 服务器(例如,没有数据访问或其他可选依赖)上,你预计会看到一个线程用于服务器,还有几个线程用于请求处理(通常和 CPU 核心的数量一样多)。不过,Servlet 容器可能会启动更多线程(例如,Tomcat 会启动 10 个),以支持 Servlet 的(阻塞)I/O 和 Servlet 3.1 的(非阻塞)I/O 使用情况。
  • 响应式 WebClient 以事件循环的方式运行。因此,你会看到与之相关的少量、固定数量的处理线程(例如,使用 Reactor Netty 连接器时的 reactor-http-nio-)。不过,如果 Reactor Netty 同时用于客户端和服务器,默认情况下,两者会共享事件循环资源。
  • Reactor 和 RxJava 提供了名为调度器的线程池抽象,与 publishOn 操作符一起使用,以将处理切换到不同的线程池。这些调度器的名称暗示了一种特定的并发策略 —— 例如,“parallel”(用于线程数量有限的 CPU 密集型工作)或“elastic”(用于线程数量较多的 I/O 密集型工作)。如果你看到这样的线程,意味着某些代码正在使用特定线程池的 Scheduler 策略。
  • 数据访问库和其他第三方依赖也可能创建和使用它们自己的线程。

# 9.4、配置

Spring 框架不提供启动和停止服务器的支持。要为服务器配置线程模型,你需要使用特定服务器的配置 API,或者,如果你使用 Spring Boot,可以查看每个服务器的 Spring Boot 配置选项。你可以直接配置 WebClient。对于其他库,请参阅它们各自的文档。

# 10、响应式核心

spring-web 模块为响应式 Web 应用程序提供了以下基础支持:

  • 对于服务器请求处理,有两个级别的支持。
    • HttpHandler (opens new window):使用非阻塞 I/O 和响应式流背压进行 HTTP 请求处理的基本契约,同时还提供了适用于 Reactor Netty、Undertow、Tomcat、Jetty 以及任何 Servlet 容器的适配器。
    • WebHandler API (opens new window):这是一个稍高级别的通用 Web API,用于进行请求处理。在此基础上构建了如注解控制器和函数式端点等具体的编程模型。
  • 在客户端方面,有一个基础的 ClientHttpConnector 契约,用于使用非阻塞 I/O 和响应式流背压执行 HTTP 请求,同时还提供了适用于 Reactor Netty (opens new window)、响应式 Jetty HttpClient (opens new window) 和 Apache HttpComponents (opens new window) 的适配器。应用程序中使用的更高级别的 WebClient 是基于这个基础契约构建的。
  • 为客户端和服务器提供用于序列化和反序列化 HTTP 请求和响应内容的 编解码器 (opens new window)。

# 11、HttpHandler

HttpHandler (opens new window) 是一个简单的契约,它只有一个处理请求和响应的方法。其设计故意保持极简,主要目的是为不同的 HTTP 服务器 API 提供一个最小化的抽象。

下表描述了支持的服务器 API:

服务器名称 使用的服务器 API 响应式流支持
Netty Netty API Reactor Netty (opens new window)
Undertow Undertow API spring - web:Undertow 到响应式流的桥接
Tomcat Servlet 非阻塞 I/O;Tomcat API 用于读写 ByteBuffer 而非 byte[] spring - web:Servlet 非阻塞 I/O 到响应式流的桥接
Jetty Servlet 非阻塞 I/O;Jetty API 用于写 ByteBuffer 而非 byte[] spring - web:Servlet 非阻塞 I/O 到响应式流的桥接
Servlet 容器 Servlet 非阻塞 I/O spring - web:Servlet 非阻塞 I/O 到响应式流的桥接

下表描述了服务器依赖(另见 支持的版本 (opens new window)):

服务器名称 组 ID 构件名称
Reactor Netty io.projectreactor.netty reactor - netty
Undertow io.undertow undertow - core
Tomcat org.apache.tomcat.embed tomcat - embed - core
Jetty org.eclipse.jetty jetty - server, jetty - servlet

以下代码片段展示了如何将 HttpHandler 适配器与每个服务器 API 结合使用:

# 11.1、Netty

HttpHandler handler =...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bindNow();

# 11.2、Undertow

HttpHandler handler =...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

# 11.3、Tomcat

HttpHandler handler =...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();

# 11.4、Jetty

HttpHandler handler =...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();

# 11.5、容器

若要将应用以 WAR 包形式部署到任何 Servlet 容器中,你可以在 WAR 包中扩展并包含 AbstractReactiveWebInitializer (opens new window) 类。该类使用 ServletHttpHandlerAdapter 包装一个 HttpHandler,并将其注册为一个 Servlet。

# 12、API

org.springframework.web.server 包基于 HttpHandler (opens new window) 契约,通过多个 WebExceptionHandler (opens new window)、多个 WebFilter (opens new window) 和单个 WebHandler (opens new window) 组件构成的链,提供了一个通用的 Web API 来处理请求。这个链可以通过 WebHttpHandlerBuilder 来组装,只需指定一个 Spring ApplicationContext 即可,组件会在其中自动检测 自动检测 (opens new window),也可以直接向构建器注册组件。

HttpHandler 的目标是简单地抽象不同 HTTP 服务器的使用,而 WebHandler API 旨在提供一系列广泛应用于 Web 应用程序的特性,例如:

  • 带有属性的用户会话。
  • 请求属性。
  • 请求的解析后的 Locale 或 Principal。
  • 访问解析和缓存后的表单数据。
  • 对多部分数据的抽象。
  • 等等。

# 12.1、特殊的 Bean 类型

下表列出了 WebHttpHandlerBuilder 可以在 Spring 应用上下文中自动检测到的组件,或者可以直接向其注册的组件:

Bean 名称 Bean 类型 数量 描述
<any> WebExceptionHandler 0..N 处理 WebFilter 链和目标 WebHandler 抛出的异常。更多详情,请参阅 异常 (opens new window)。
<any> WebFilter 0..N 在过滤器链的其余部分和目标 WebHandler 之前和之后应用拦截逻辑。更多详情,请参阅 过滤器 (opens new window)。
webHandler WebHandler 1 请求处理器。
webSessionManager WebSessionManager 0..1 ServerWebExchange 方法暴露的 WebSession 实例的管理器。默认为 DefaultWebSessionManager。
serverCodecConfigurer ServerCodecConfigurer 0..1 用于访问 HttpMessageReader 实例,以解析表单数据和多部分数据,然后通过 ServerWebExchange 上的方法暴露它们。默认为 ServerCodecConfigurer.create()。
localeContextResolver LocaleContextResolver 0..1 ServerWebExchange 方法暴露的 LocaleContext 的解析器。默认为 AcceptHeaderLocaleContextResolver。
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 处理转发类型的头,可提取并删除它们,或仅删除它们。默认情况下不使用。

# 12.2、表单数据

ServerWebExchange 提供了以下方法来访问表单数据:

Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchange 使用配置的 HttpMessageReader 来将表单数据(application/x-www-form-urlencoded)解析为 MultiValueMap。默认情况下,FormHttpMessageReader 由 ServerCodecConfigurer bean 配置(请参阅 Web Handler API (opens new window))。

# 12.3、多部分数据

ServerWebExchange 提供了以下方法来访问多部分数据:

Mono<MultiValueMap<String, Part>> getMultipartData();

DefaultServerWebExchange 使用配置的 HttpMessageReader<MultiValueMap<String, Part>> 来将 multipart/form-data、multipart/mixed 和 multipart/related 内容解析为 MultiValueMap。默认情况下,使用的是 DefaultPartHttpMessageReader,它不依赖任何第三方库。或者,也可以使用 SynchronossPartHttpMessageReader,它基于 Synchronoss NIO Multipart (opens new window) 库。这两者都通过 ServerCodecConfigurer bean 进行配置(请参阅 Web Handler API (opens new window))。

若要以流式方式解析多部分数据,可以使用 PartEventHttpMessageReader 返回的 Flux<PartEvent>,而不是使用 @RequestPart,因为后者意味着通过名称像 Map 一样访问各个部分,因此需要解析整个多部分数据。相比之下,可以使用 @RequestBody 将内容解码为 Flux<PartEvent>,而无需将其收集到 MultiValueMap 中。

# 12.4、转发头

当请求经过如负载均衡器等代理时,主机、端口和协议可能会发生变化,这使得从客户端角度创建指向正确主机、端口和协议的链接变得具有挑战性。

RFC 7239 (opens new window) 定义了 Forwarded HTTP 头,代理可以使用该头来提供有关原始请求的信息。

# 12.5、非标准头

还有其他非标准头,包括 X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。

# a、- Forwarded - Host

虽然不是标准头,但 X-Forwarded-Host: <host> (opens new window) 是事实上的标准头,用于将原始主机信息传递给下游服务器。例如,如果将 https://example.com/resource 的请求发送到代理,代理将请求转发到 http://localhost:8080/resource ,则可以发送 X-Forwarded-Host: example.com 头来告知服务器原始主机是 example.com。

# b、- Forwarded - Port

虽然不是标准头,但 X-Forwarded-Port: <port> 是事实上的标准头,用于将原始端口信息传递给下游服务器。例如,如果将 https://example.com/resource 的请求发送到代理,代理将请求转发到 http://localhost:8080/resource ,则可以发送 X-Forwarded-Port: 443 头来告知服务器原始端口是 443。

# c、- Forwarded - Proto

虽然不是标准头,但 X-Forwarded-Proto: (https|http) (opens new window) 是事实上的标准头,用于将原始协议(例如 https、http)信息传递给下游服务器。例如,如果将 https://example.com/resource 的请求发送到代理,代理将请求转发到 http://localhost:8080/resource ,则可以发送 X-Forwarded-Proto: https 头来告知服务器原始协议是 https。

# d、- Forwarded - Ssl

虽然不是标准头,但 X-Forwarded-Ssl: (on|off) 是事实上的标准头,用于将原始协议(例如 https、http)信息传递给下游服务器。例如,如果将 https://example.com/resource 的请求发送到代理,代理将请求转发到 http://localhost:8080/resource ,则可以发送 X-Forwarded-Ssl: on 头来告知服务器原始协议是 https。

# e、- Forwarded - Prefix

虽然不是标准头,但 X-Forwarded-Prefix: <prefix> (opens new window) 是事实上的标准头,用于将原始 URL 路径前缀信息传递给下游服务器。

X-Forwarded-Prefix 的使用可能因部署场景而异,并且需要具有灵活性,以允许替换、删除或前置目标服务器的路径前缀。

  • 场景 1:覆盖路径前缀

    https://example.com/api/{path} -> http://localhost:8080/app1/{path}
    

    前缀是捕获组 {path} 之前的路径的起始部分。对于代理,前缀是 /api,而对于服务器,前缀是 /app1。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api 以使用原始前缀 /api 覆盖服务器前缀 /app1。

  • 场景 2:删除路径前缀 有时候,应用程序可能希望删除前缀。例如,考虑以下代理到服务器的映射:

    https://app1.example.com/{path} -> http://localhost:8080/app1/{path}
    https://app2.example.com/{path} -> http://localhost:8080/app2/{path}
    

    代理没有前缀,而应用程序 app1 和 app2 分别有路径前缀 /app1 和 /app2。代理可以发送 X-Forwarded-Prefix: 以使用空前缀覆盖服务器前缀 /app1 和 /app2。

    注意:这种部署场景的常见情况是,每个生产应用服务器需要支付许可费用,因此更倾向于在每个服务器上部署多个应用程序以降低费用。另一个原因是在同一服务器上运行更多应用程序,以共享服务器运行所需的资源。

在这些场景中,由于同一服务器上有多个应用程序,应用程序需要有一个非空的上下文根。然而,这在公共 API 的 URL 路径中应该不可见,因为应用程序可能使用不同的子域名,这样做有以下好处:

  • 增强安全性,例如同源策略。

  • 应用程序可以独立扩展(不同的域名指向不同的 IP 地址)。

  • 场景 3:插入路径前缀 在其他情况下,可能需要前置一个前缀。例如,考虑以下代理到服务器的映射:

    https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
    

    在这种情况下,代理的前缀是 /api/app1,服务器的前缀是 /app1。代理可以发送 X-Forwarded-Prefix: /api/app1 以使用原始前缀 /api/app1 覆盖服务器前缀 /app1。

# 12.6、ForwardedHeaderTransformer

ForwardedHeaderTransformer 是一个组件,它根据转发头修改请求的主机、端口和协议,然后删除这些头。如果你将其声明为名称为 forwardedHeaderTransformer 的 bean,它将被 检测 (opens new window) 并使用。

注意:在 5.1 版本中,ForwardedHeaderFilter 已被弃用,由 ForwardedHeaderTransformer 取代,这样可以在交换创建之前更早地处理转发头。如果仍然配置了该过滤器,它将被从过滤器列表中移除,并使用 ForwardedHeaderTransformer 代替。

# 12.7、安全考虑

对于转发头需要考虑安全性,因为应用程序无法知道这些头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么应该在信任边界的代理处配置删除来自外部的不可信转发流量。你还可以将 ForwardedHeaderTransformer 配置为 removeOnly=true,在这种情况下,它将删除头但不使用它们。

# 13、过滤器

在 WebHandler API (opens new window) 中,你可以使用 WebFilter 在过滤器处理链的其余部分和目标 WebHandler 之前和之后应用拦截逻辑。使用 WebFlux 配置 (opens new window) 时,注册一个 WebFilter 非常简单,只需将其声明为 Spring bean,并(可选)通过在 bean 声明上使用 @Order 注解或实现 Ordered 接口来指定优先级。

# 13.1、CORS

Spring WebFlux 通过控制器上的注解为 CORS 配置提供了细粒度的支持。但是,当与 Spring Security 一起使用时,建议使用内置的 CorsFilter,它必须在 Spring Security 的过滤器链之前排序。

有关更多详细信息,请参阅 CORS (opens new window) 部分和 CORS WebFilter (opens new window)。

# 13.2、处理程序

你可能希望你的控制器端点能够匹配 URL 路径中带有或不带有尾随斜杠的路由。例如,"GET /home" 和 "GET /home/" 都应该由使用 @GetMapping("/home") 注解的控制器方法处理。

在所有映射声明中添加带有尾随斜杠的变体并不是处理这种用例的最佳方式。UrlHandlerFilter Web 过滤器就是为此目的而设计的。它可以配置为:

  • 当接收到带有尾随斜杠的 URL 时,以 HTTP 重定向状态响应,将浏览器重定向到没有尾随斜杠的 URL 变体。
  • 更改请求,使其看起来就像请求没有发送尾随斜杠一样,并继续处理该请求。

以下是如何为博客应用程序实例化和配置 UrlHandlerFilter 的示例:

UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
        // 将会把 "/blog/my-blog-post/" 以 HTTP 308 重定向到 "/blog/my-blog-post"
       .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
        // 将会把请求 "/admin/user/account/" 转换为 "/admin/user/account" 并继续处理
       .trailingSlashHandler("/admin/**").mutateRequest()
       .build();

# 14、异常

在 WebHandler API (opens new window) 中,你可以使用 WebExceptionHandler 处理 WebFilter 链和目标 WebHandler 抛出的异常。使用 WebFlux 配置 (opens new window) 时,注册一个 WebExceptionHandler 很简单,只需将其声明为 Spring bean,并(可选)通过在 bean 声明上使用 @Order 注解或实现 Ordered 接口来指定优先级。

以下表格描述了可用的 WebExceptionHandler 实现:

异常处理器 描述
ResponseStatusExceptionHandler 通过将响应设置为异常的 HTTP 状态码,处理 ResponseStatusException (opens new window) 类型的异常。
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandler 的扩展,还可以确定任何异常上的 @ResponseStatus 注解的 HTTP 状态码。该处理器在 WebFlux 配置 (opens new window) 中声明。

# 15、编解码器

spring-web 和 spring-core 模块支持通过非阻塞 I/O 和响应式流背压,将字节内容与高级对象之间进行序列化和反序列化。以下是对这种支持的描述:

  • Encoder (opens new window) 和 Decoder (opens new window) 是独立于 HTTP 进行内容编码和解码的低级契约。
  • HttpMessageReader (opens new window) 和 HttpMessageWriter (opens new window) 是对 HTTP 消息内容进行编码和解码的契约。
  • Encoder 可以用 EncoderHttpMessageWriter 包装,以便在 Web 应用程序中使用,而 Decoder 可以用 DecoderHttpMessageReader 包装。
  • DataBuffer (opens new window) 抽象了不同的字节缓冲区表示形式(例如,Netty ByteBuf、java.nio.ByteBuffer 等),所有编解码器都基于此工作。有关此主题的更多信息,请参阅 "Spring Core" 部分中的 数据缓冲区和编解码器 (opens new window)。

spring-core 模块提供 byte[]、ByteBuffer、DataBuffer、Resource 和 String 的编码器和解码器实现。spring-web 模块提供 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 等编码器和解码器,以及用于表单数据、多部分内容、服务器发送事件等的仅适用于 Web 的 HTTP 消息读取器和写入器实现。

ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和自定义应用程序中使用的编解码器。有关配置 HTTP 消息编解码器 (opens new window) 的部分。

# 15.1、JSON

当 Jackson 库存在时,支持 JSON 和二进制 JSON(Smile (opens new window))。

Jackson2Decoder 的工作方式如下:

  • 使用 Jackson 的异步、非阻塞解析器将字节块流聚合为 TokenBuffer,每个 TokenBuffer 表示一个 JSON 对象。
  • 每个 TokenBuffer 被传递给 Jackson 的 ObjectMapper 以创建一个高级对象。
  • 当解码为单值发布者(例如 Mono)时,有一个 TokenBuffer。
  • 当解码为多值发布者(例如 Flux)时,一旦接收到足够的字节以形成一个完整的对象,每个 TokenBuffer 就会被传递给 ObjectMapper。输入内容可以是 JSON 数组,也可以是任何 行分隔的 JSON (opens new window) 格式,如 NDJSON、JSON Lines 或 JSON Text Sequences。

Jackson2Encoder 的工作方式如下:

  • 对于单值发布者(例如 Mono),只需通过 ObjectMapper 进行序列化。
  • 对于具有 application/json 媒体类型的多值发布者,默认情况下使用 Flux#collectToList() 收集值,然后序列化结果集合。
  • 对于具有流式媒体类型(如 application/x-ndjson 或 application/stream+x-jackson-smile)的多值发布者,使用 行分隔的 JSON (opens new window) 格式逐个编码、写入和刷新每个值。其他流式媒体类型可以向编码器注册。
  • 对于 SSE,Jackson2Encoder 会针对每个事件调用,并刷新输出以确保无延迟传递。

注意:默认情况下,Jackson2Encoder 和 Jackson2Decoder 都不支持 String 类型的元素。相反,默认假设字符串或字符串序列表示序列化的 JSON 内容,由 CharSequenceEncoder 渲染。如果需要从 Flux<String> 渲染 JSON 数组,请使用 Flux#collectToList() 并编码 Mono<List<String>>。

# 15.2、表单数据

FormHttpMessageReader 和 FormHttpMessageWriter 支持对 application/x-www-form-urlencoded 内容进行解码和编码。

在服务器端,表单内容通常需要从多个地方访问,ServerWebExchange 提供了一个专用的 getFormData() 方法,该方法通过 FormHttpMessageReader 解析内容,然后缓存结果以便重复访问。请参阅 WebHandler API (opens new window) 部分中的 表单数据 (opens new window)。

一旦使用了 getFormData() 方法,就不能再从请求体中读取原始的原始内容。因此,应用程序应始终通过 ServerWebExchange 访问缓存的表单数据,而不是从原始请求体中读取。

# 15.3、多部分

MultipartHttpMessageReader 和 MultipartHttpMessageWriter 支持对 "multipart/form-data"、"multipart/mixed" 和 "multipart/related" 内容进行解码和编码。MultipartHttpMessageReader 委托给另一个 HttpMessageReader 进行实际解析,将其转换为 Flux<Part>,然后简单地将这些部分收集到 MultiValueMap 中。默认情况下,使用 DefaultPartHttpMessageReader,但可以通过 ServerCodecConfigurer 进行更改。有关 DefaultPartHttpMessageReader 的更多信息,请参阅 DefaultPartHttpMessageReader 的 Javadoc (opens new window)。

在服务器端,多部分表单内容可能需要从多个地方访问,ServerWebExchange 提供了一个专用的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以便重复访问。请参阅 WebHandler API (opens new window) 部分中的 多部分数据 (opens new window)。

一旦使用了 getMultipartData() 方法,就不能再从请求体中读取原始的原始内容。因此,应用程序必须始终使用 getMultipartData() 来重复以类似 Map 的方式访问各个部分,否则可以依靠 SynchronossPartHttpMessageReader 一次性访问 Flux<Part>。

# 15.4、协议缓冲区

ProtobufEncoder 和 ProtobufDecoder 支持对 com.google.protobuf.Message 类型的 "application/x-protobuf"、"application/octet-stream" 和 "application/vnd.google.protobuf" 内容进行解码和编码。如果内容类型中带有 "delimited" 参数(如 "application/x-protobuf;delimited=true"),它们还支持值流。这需要 "com.google.protobuf:protobuf-java" 库,版本为 3.29 及以上。

ProtobufJsonDecoder 和 ProtobufJsonEncoder 变体支持在 Protobuf 消息和 JSON 文档之间进行读写操作。它们需要 "com.google.protobuf:protobuf-java-util" 依赖项。请注意,JSON 变体不支持读取消息流,更多详细信息请参阅 ProtobufJsonDecoder 的 Javadoc (opens new window)。

# 15.5、限制

Decoder 和 HttpMessageReader 实现可能会缓冲部分或全部输入流,可以配置其在内存中缓冲的最大字节数限制。在某些情况下,会发生缓冲是因为输入被聚合并表示为单个对象,例如带有 @RequestBody byte[] 的控制器方法、x-www-form-urlencoded 数据等。在流式处理时也可能会发生缓冲,例如当分割输入流时,如分隔文本、JSON 对象流等。对于这些流式处理情况,该限制适用于流中一个对象关联的字节数。

要配置缓冲区大小,你可以检查给定的 Decoder 或 HttpMessageReader 是否暴露了 maxInMemorySize 属性,如果是,则 Javadoc 中将包含有关默认值的详细信息。在服务器端,ServerCodecConfigurer 提供了一个单一的地方来设置所有编解码器,请参阅 HTTP 消息编解码器 (opens new window)。在客户端,所有编解码器的限制可以在 WebClient.Builder (opens new window) 中更改。

对于 多部分解析 (opens new window),maxInMemorySize 属性限制非文件部分的大小。对于文件部分,它确定了将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart 属性来限制每个部分的磁盘使用量。还有一个 maxParts 属性来限制多部分请求中的总部分数。若要在 WebFlux 中配置这三个属性,需要向 ServerCodecConfigurer 提供一个预配置的 MultipartHttpMessageReader 实例。

# 15.6、流式处理

在向 HTTP 响应进行流式传输时(例如 text/event-stream、application/x-ndjson),定期发送数据非常重要,以便尽早可靠地检测到客户端断开连接。这样的发送可以是仅包含注释的空 SSE 事件,或任何其他“无操作”数据,这些数据实际上可作为心跳信号。

# 15.7、DataBuffer

DataBuffer 是 WebFlux 中字节缓冲区的表示形式。本参考文档的 Spring Core 部分在 数据缓冲区和编解码器 (opens new window) 部分有更多相关内容。需要理解的关键点是,在像 Netty 这样的一些服务器上,字节缓冲区是池化和引用计数的,在使用后必须释放以避免内存泄漏。

WebFlux 应用程序通常不需要关注此类问题,除非它们直接消费或生成数据缓冲区,而不是依赖编解码器在高级对象之间进行转换,或者除非它们选择创建自定义编解码器。对于这种情况,请参考 数据缓冲区和编解码器 (opens new window) 中的信息,尤其是 使用 DataBuffer (opens new window) 部分。

# 16、日志记录

Spring WebFlux 中的 DEBUG 级日志设计得简洁、最小化且易于阅读。它专注于那些反复有用的高价值信息,而不是仅在调试特定问题时才有用的其他信息。

TRACE 级日志通常遵循与 DEBUG 相同的原则(例如也不应产生大量日志),但可用于调试任何问题。此外,某些日志消息在 TRACE 级别和 DEBUG 级别可能显示不同的详细程度。

良好的日志记录来自于对日志的使用经验。如果你发现任何不符合上述目标的内容,请告知我们。

# 16.1、日志 ID

在 WebFlux 中,单个请求可能会在多个线程上运行,线程 ID 对于关联属于特定请求的日志消息没有用处。因此,WebFlux 日志消息默认以特定于请求的 ID 为前缀。

在服务器端,日志 ID 存储在 ServerWebExchange 属性(LOG_ID_ATTRIBUTE (opens new window))中,而基于该 ID 的完全格式化的前缀可以从 ServerWebExchange#getLogPrefix() 获得。在 WebClient 端,日志 ID 存储在 ClientRequest 属性(LOG_ID_ATTRIBUTE (opens new window))中,而完全格式化的前缀可以从 ClientRequest#logPrefix() 获得。

# 16.2、敏感数据

DEBUG 和 TRACE 日志可能会记录敏感信息。因此,表单参数和头信息默认被屏蔽,你必须明确启用它们的完整日志记录。

以下示例展示了如何为服务器端请求启用:

@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}

以下示例展示了如何为客户端请求启用:

Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
       .exchangeStrategies(strategies -> strategies.codecs(consumer))
       .build();

# 16.3、追加器

像 SLF4J 和 Log4J 2 这样的日志库提供了异步日志记录器,避免了阻塞。虽然这些日志记录器有其自身的缺点,例如可能会丢弃无法排队记录的消息,但它们是目前在响应式、非阻塞应用程序中使用的最佳选择。

# 16.4、自定义编解码器

应用程序可以注册自定义编解码器,以支持额外的媒体类型或默认编解码器不支持的特定行为。

开发人员表达的一些配置选项会在默认编解码器上强制执行。自定义编解码器可能希望与这些偏好保持一致,例如 强制执行缓冲限制 (opens new window) 或 记录敏感数据 (opens new window)。

以下示例展示了如何为客户端请求注册自定义编解码器:

WebClient webClient = WebClient.builder()
       .codecs(configurer -> {
            CustomDecoder decoder = new CustomDecoder();
            configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
       .build();

# 17、DispatcherHandler

与Spring MVC类似,Spring WebFlux也是围绕前端控制器模式设计的。在这种模式中,中央WebHandler(即DispatcherHandler)提供了共享的请求处理算法,而实际工作则由可配置的委托组件完成。这种模型非常灵活,能够支持多种工作流程。

DispatcherHandler会从Spring配置中发现所需的委托组件。它本身也是一个Spring Bean,并实现了ApplicationContextAware接口,以便访问其运行所在的上下文。如果将DispatcherHandler声明为名为webHandler的Bean,它会被WebHttpHandlerBuilder发现。WebHttpHandlerBuilder会组装一个请求处理链,相关内容可参考WebHandler API。

WebFlux应用中的Spring配置通常包含以下内容:

  • 名为webHandler的DispatcherHandler
  • WebFilter和WebExceptionHandler Bean
  • 特殊的DispatcherHandler Bean
  • 其他

下面的示例展示了如何将配置传递给WebHttpHandlerBuilder来构建处理链:

ApplicationContext context =...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();

最终得到的HttpHandler可以与服务器适配器一起使用。

# 18、特殊Bean类型

DispatcherHandler会委托特殊的Bean来处理请求并生成合适的响应。这里所说的“特殊Bean”是指实现了WebFlux框架契约的Spring管理的Object实例。这些实例通常带有内置契约,但你可以自定义其属性、扩展它们或替换它们。

以下表格列出了DispatcherHandler检测到的特殊Bean。需要注意的是,在更低的层级还会检测到其他一些Bean(请参阅Web Handler API中的特殊Bean类型)。

Bean类型 说明
HandlerMapping 它根据某些条件将请求映射到处理程序,具体条件因HandlerMapping的实现而异,比如带注释的控制器、简单的URL模式映射等。主要的HandlerMapping实现包括用于处理@RequestMapping注解方法的RequestMappingHandlerMapping、用于处理函数式端点路由的RouterFunctionMapping以及用于显式注册URI路径模式和WebHandler实例的SimpleUrlHandlerMapping。
HandlerAdapter 帮助DispatcherHandler调用映射到请求的处理程序,而无需考虑处理程序的实际调用方式。例如,调用带注释的控制器需要解析注解,HandlerAdapter的主要目的是将这些细节屏蔽在DispatcherHandler之外。
HandlerResultHandler 处理处理程序调用的结果并最终完成响应。请参阅结果处理。

# 19、WebFlux配置

应用程序可以声明处理请求所需的基础架构Bean(在Web Handler API和DispatcherHandler中列出)。然而,在大多数情况下,WebFlux配置是最佳起点。它会声明所需的Bean,并提供更高级别的配置回调API以进行自定义。

注意:Spring Boot依赖WebFlux配置来配置Spring WebFlux,并且还提供了许多额外的便捷选项。

# 20、处理流程

DispatcherHandler按以下步骤处理请求:

  • 依次询问每个HandlerMapping是否能找到匹配的处理程序,使用找到的第一个匹配项。
  • 如果找到处理程序,则通过合适的HandlerAdapter运行它,将执行的返回值公开为HandlerResult。
  • 将HandlerResult传递给合适的HandlerResultHandler,通过直接写入响应或使用视图渲染来完成处理。

# 21、结果处理

通过HandlerAdapter调用处理程序的返回值会与一些额外的上下文一起包装为HandlerResult,并传递给第一个声明支持该结果的HandlerResultHandler。以下表格展示了可用的HandlerResultHandler实现,所有这些实现都在WebFlux配置中声明:

结果处理程序类型 返回值 默认顺序
ResponseEntityResultHandler 通常来自@Controller实例的ResponseEntity。 0
ServerResponseResultHandler 通常来自函数式端点的ServerResponse。 0
ResponseBodyResultHandler 处理来自@ResponseBody方法或@RestController类的返回值。 100
ViewResolutionResultHandler CharSequence、View (opens new window)、Model (opens new window)、Map、Rendering (opens new window)或任何其他Object都被视为模型属性。另请参阅视图解析。 Integer.MAX_VALUE

# 22、异常处理

HandlerAdapter实现可以处理调用请求处理程序(如控制器方法)时内部产生的异常。但是,如果请求处理程序返回异步值,则异常可能会被延迟处理。

HandlerAdapter可以将其异常处理机制作为DispatchExceptionHandler设置在返回的HandlerResult上。当设置了该机制时,DispatcherHandler也会将其应用于结果处理。

HandlerAdapter也可以选择实现DispatchExceptionHandler。在这种情况下,DispatcherHandler会将其应用于在映射处理程序之前出现的异常,例如,在处理程序映射期间或更早(如在WebFilter中)出现的异常。

另请参阅“注解控制器”部分中的异常处理或WebHandler API部分中的异常处理。

# 23、视图解析

视图解析允许使用HTML模板和模型向浏览器渲染内容,而无需绑定到特定的视图技术。在Spring WebFlux中,视图解析通过专用的HandlerResultHandler来支持,该处理程序使用ViewResolver实例将表示逻辑视图名称的字符串映射到View实例,然后使用该View渲染响应。

Web应用程序需要使用视图渲染库来支持此用例。

# 23.1、处理过程

传递给ViewResolutionResultHandler的HandlerResult包含处理程序的返回值和请求处理期间添加的属性的模型。返回值按以下方式处理:

  • String或CharSequence:表示逻辑视图名称,将通过配置的ViewResolver实现列表将其解析为View。
  • void:根据请求路径(去掉前导和尾随斜杠)选择默认视图名称,并将其解析为View。当未提供视图名称(例如,返回模型属性)或返回异步值(例如,Mono为空完成)时,也会发生这种情况。
  • Rendering (opens new window):用于视图解析场景的API。可以在IDE中使用代码补全功能来探索选项。
  • Model或Map:添加到请求模型中的额外模型属性。
  • 其他:任何其他返回值(除了通过BeanUtils#isSimpleProperty (opens new window)确定的简单类型)都被视为模型属性添加到模型中。属性名称根据约定 (opens new window)从类名派生,除非处理程序方法带有@ModelAttribute注解。

模型可以包含异步、响应式类型(例如,来自Reactor或RxJava)。在渲染之前,AbstractView会将这些模型属性解析为具体值并更新模型。单值响应式类型会解析为单个值或无值(如果为空),而多值响应式类型(例如,Flux<T>)会被收集并解析为List<T>。

在Spring配置中添加ViewResolutionResultHandler Bean即可轻松配置视图解析。WebFlux配置提供了专门的视图解析配置API。

# 23.2、重定向

视图名称中特殊的redirect:前缀允许你执行重定向。UrlBasedViewResolver(及其子类)会将其识别为重定向指令,视图名称的其余部分就是重定向URL。

这样做的最终效果与控制器返回RedirectView或Rendering.redirectTo("abc").build()相同,但现在控制器本身可以使用逻辑视图名称。像redirect:/some/resource这样的视图名称是相对于当前应用程序的,而像redirect:https://example.com/arbitrary/path这样的视图名称则会重定向到绝对URL。

注意:与Servlet栈不同,Spring WebFlux不支持“FORWARD”调度,因此不支持forward:前缀。

# 23.3、内容协商

ViewResolutionResultHandler支持内容协商。它会将请求的媒体类型与每个选定View支持的媒体类型进行比较,使用第一个支持请求媒体类型的View。

为了支持JSON和XML等媒体类型,Spring WebFlux提供了HttpMessageWriterView,这是一种特殊的View,通过HttpMessageWriter进行渲染。通常,你可以通过WebFlux配置将其配置为默认视图。如果默认视图与请求的媒体类型匹配,则总是会被选中并使用。

# 24、注解式控制器

Spring WebFlux 提供了基于注解的编程模型,其中 @Controller 和 @RestController 组件使用注解来表达请求映射、请求输入、处理异常等。注解式控制器具有灵活的方法签名,无需继承基类或实现特定接口。

以下代码展示了一个基本示例:

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}

在上述示例中,该方法返回一个 String 类型的数据,并将其写入响应体。

# 24.1、@Controller

你可以使用标准的 Spring Bean 定义来定义控制器 Bean。@Controller 注解允许自动检测,并与 Spring 对类路径中 @Component 类的自动检测以及为它们自动注册 Bean 定义的通用支持保持一致。它还可以作为被注解类的一种原型,表明其作为 Web 组件的角色。

为了启用对这些 @Controller Bean 的自动检测,你可以在 Java 配置中添加组件扫描,如下例所示:

@Configuration
@ComponentScan("org.example.web") // 1. 扫描 org.example.web 包
public class WebConfiguration {

    //...
}

@RestController 是一个组合注解,它本身被元注解为 @Controller 和 @ResponseBody,表示一个控制器,其每个方法都继承类型级别的 @ResponseBody 注解,因此直接写入响应体,而不是进行视图解析和使用 HTML 模板渲染。

# 25、代理

在某些情况下,你可能需要在运行时使用 AOP 代理来装饰控制器。例如,如果你选择在控制器上直接使用 @Transactional 注解。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。当这些注解直接放在控制器上时,会自动使用基于类的代理。

如果控制器实现了一个接口,并且需要 AOP 代理,你可能需要显式配置基于类的代理。例如,使用 @EnableTransactionManagement 时,你可以将其改为 @EnableTransactionManagement(proxyTargetClass = true);使用 <tx:annotation-driven/> 时,可以将其改为 <tx:annotation-driven proxy-target-class="true"/>。

注意:从 6.0 版本开始,使用接口代理时,Spring WebFlux 不再仅根据接口上的类型级别 @RequestMapping 注解来检测控制器。请启用基于类的代理,否则接口也必须有 @Controller 注解。

# 25.1、请求映射

本节讨论带注解的控制器的请求映射。

# 26、@RequestMapping

@RequestMapping 注解用于将请求映射到控制器方法。它有各种属性,可以根据 URL、HTTP 方法、请求参数、请求头和媒体类型进行匹配。你可以在类级别使用它来表示共享映射,也可以在方法级别使用它来细化到特定的端点映射。

@RequestMapping 还有特定 HTTP 方法的快捷变体:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

上述注解是自定义注解。可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,因为 @RequestMapping 默认会匹配所有 HTTP 方法。同时,仍然需要在类级别使用 @RequestMapping 来表示共享映射。

注意:@RequestMapping 不能与在同一元素(类、接口或方法)上声明的其他 @RequestMapping 注解一起使用。如果在同一元素上检测到多个 @RequestMapping 注解,将记录一条警告信息,并且仅使用第一个映射。这也适用于组合的 @RequestMapping 注解,如 @GetMapping、@PostMapping 等。

以下示例展示了如何在类和方法级别使用映射:

@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        //...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        //...
    }
}

# 27、模式

你可以使用通配符模式和通配符来映射请求。

模式 描述 示例
? 匹配一个字符 "/pages/t?st.html" 匹配 "/pages/test.html" 和 "/pages/t3st.html"
* 匹配路径段中的零个或多个字符 "/resources/*.png" 匹配 "/resources/file.png";"/projects/*/versions" 匹配 "/projects/spring/versions",但不匹配 "/projects/spring/boot/versions"
** 匹配到路径末尾的零个或多个路径段 "/resources/**" 匹配 "/resources/file.png" 和 "/resources/images/file.png";"/resources/**/file.png" 无效,因为 ** 只能放在路径末尾
{name} 匹配一个路径段,并将其捕获为名为 "name" 的变量 "/projects/{project}/versions" 匹配 "/projects/spring/versions",并捕获 project=spring
{name:[a-z]}+ 将正则表达式 [a-z]+ 作为名为 "name" 的路径变量匹配 "/projects/{project:[a-z]}/versions" 匹配 "/projects/spring/versions",但不匹配 "/projects/spring1/versions"
{*path} 匹配到路径末尾的零个或多个路径段,并将其捕获为名为 "path" 的变量 "/resources/{*file}" 匹配 "/resources/images/file.png",并捕获 file=/images/file.png

如以下示例所示,捕获的 URI 变量可以使用 @PathVariable 来访问:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    //...
}

你可以在类和方法级别声明 URI 变量,如下例所示:

@Controller
@RequestMapping("/owners/{ownerId}") // (1)
public class OwnerController {

    @GetMapping("/pets/{petId}") // (2)
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        //...
    }
}
  1. 类级别的 URI 映射。
  2. 方法级别的 URI 映射。

URI 变量会自动转换为适当的类型,否则会抛出 TypeMismatchException。默认情况下支持简单类型(int、long、Date 等),你也可以为任何其他数据类型注册支持。请参阅类型转换和 DataBinder。

URI 变量可以显式命名(例如,@PathVariable("customId")),但如果名称相同,并且你使用 -parameters 编译器标志编译代码,则可以省略该细节。

语法 {*varName} 声明一个 URI 变量,该变量可以匹配零个或多个剩余的路径段。例如,/resources/{*path} 匹配 /resources/ 下的所有文件,"path" 变量会捕获 /resources 下的完整路径。

语法 {varName:regex} 声明一个带有正则表达式的 URI 变量,其语法为:{varName:regex}。例如,对于 URL /spring - web - 3.0.5.jar,以下方法可以提取名称、版本和文件扩展名:

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    //...
}

URI 路径模式还可以包含嵌入的 ${…​} 占位符,这些占位符在启动时会通过 PropertySourcesPlaceholderConfigurer 针对本地、系统、环境和其他属性源进行解析。例如,你可以使用此功能根据一些外部配置来参数化基本 URL。

注意:Spring WebFlux 使用 PathPattern 和 PathPatternParser 来支持 URI 路径匹配。这两个类都位于 spring - web 中,专门设计用于在运行时匹配大量 URI 路径模式的 Web 应用程序中的 HTTP URL 路径。

Spring WebFlux 不支持后缀模式匹配,而 Spring MVC 中像 /person 这样的映射也会匹配 /person.*。如果需要基于 URL 进行内容协商,我们建议使用查询参数,这种方式更简单、更明确,并且更不容易受到基于 URL 路径的攻击。

# 28、模式比较

当多个模式匹配一个 URL 时,必须进行比较以找出最佳匹配。这是通过 PathPattern.SPECIFICITY_COMPARATOR 完成的,它会寻找更具体的模式。

对于每个模式,会根据 URI 变量和通配符的数量计算一个分数,其中 URI 变量的得分低于通配符。总得分较低的模式获胜。如果两个模式得分相同,则选择更长的模式。

全匹配模式(例如 **、{*varName})不参与评分,而是始终排在最后。如果两个都是全匹配模式,则选择更长的模式。

# 29、可消费的媒体类型

你可以根据请求的 Content - Type 来细化请求映射,如下例所示:

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    //...
}

consumes 属性还支持否定表达式,例如 !text/plain 表示除 text/plain 之外的任何内容类型。

你可以在类级别声明共享的 consumes 属性。但是,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 consumes 属性会覆盖而不是扩展类级别的声明。

提示:MediaType 为常用的媒体类型提供了常量,例如 APPLICATION_JSON_VALUE 和 APPLICATION_XML_VALUE。

# 30、可生产的媒体类型

你可以根据 Accept 请求头和控制器方法生成的内容类型列表来细化请求映射,如下例所示:

@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    //...
}

媒体类型可以指定字符集,也支持否定表达式,例如 !text/plain 表示除 text/plain 之外的任何内容类型。

你可以在类级别声明共享的 produces 属性。但是,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。

提示:MediaType 为常用的媒体类型提供了常量,例如 APPLICATION_JSON_VALUE、APPLICATION_XML_VALUE。

# 31、参数和请求头

你可以根据查询参数条件来细化请求映射。你可以检查查询参数是否存在(myParam)、是否不存在(!myParam)或特定的值(myParam = myValue)。以下示例检查参数的值:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") // (1)
public void findPet(@PathVariable String petId) {
    //...
}
  1. 检查 myParam 是否等于 myValue。

你也可以对请求头条件使用相同的方式,如下例所示:

@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // (1)
public void findPet(@PathVariable String petId) {
    //...
}
  1. 检查 myHeader 是否等于 myValue。

# 32、HEAD、OPTIONS

@GetMapping 和 @RequestMapping(method = HttpMethod.GET) 在请求映射方面透明地支持 HTTP HEAD。控制器方法无需更改。在 HttpHandler 服务器适配器中应用的响应包装器会确保设置 Content - Length 头为实际写入响应的字节数,而无需实际写入响应。

默认情况下,HTTP OPTIONS 请求的处理方式是将 Allow 响应头设置为所有具有匹配 URL 模式的 @RequestMapping 方法中列出的 HTTP 方法列表。

对于未声明 HTTP 方法的 @RequestMapping,Allow 头会设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应该始终声明支持的 HTTP 方法(例如,使用特定 HTTP 方法的变体 @GetMapping、@PostMapping 等)。

你可以显式地将 @RequestMapping 方法映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这不是必需的。

# 33、自定义注解

Spring WebFlux 支持使用组合注解进行请求映射。这些注解本身使用 @RequestMapping 进行元注解,并组合起来以更窄、更具体的目的重新声明 @RequestMapping 的部分(或全部)属性。

@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 和 @PatchMapping 是组合注解的例子。可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用默认匹配所有 HTTP 方法的 @RequestMapping。如果你需要实现组合注解的示例,可以查看它们的声明方式。

注意:@RequestMapping 不能与在同一元素(类、接口或方法)上声明的其他 @RequestMapping 注解一起使用。如果在同一元素上检测到多个 @RequestMapping 注解,将记录一条警告信息,并且仅使用第一个映射。这也适用于组合的 @RequestMapping 注解,如 @GetMapping、@PostMapping 等。

Spring WebFlux 还支持使用自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要继承 RequestMappingHandlerMapping 并重写 getCustomMethodCondition 方法,在该方法中你可以检查自定义属性并返回自己的 RequestCondition。

# 34、显式注册

你可以以编程方式注册处理方法,这可用于动态注册或高级场景,例如同一处理器在不同 URL 下的不同实例。以下示例展示了如何进行操作:

@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) throws NoSuchMethodException { // (1)

        RequestMappingInfo info = RequestMappingInfo
              .paths("/user/{id}").methods(RequestMethod.GET).build(); // (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); // (3)

        mapping.registerMapping(info, handler, method); // (4)
    }
}
  1. 注入目标处理程序和控制器的处理程序映射。
  2. 准备请求映射元数据。
  3. 获取处理方法。
  4. 添加注册信息。

# 35、@HttpExchange

虽然 @HttpExchange 的主要目的是通过生成的代理来抽象 HTTP 客户端代码,但放置此类注解的HTTP 接口是一个对客户端和服务器使用都中立的契约。除了简化客户端代码之外,在某些情况下,HTTP 接口也可以是服务器向客户端暴露其 API 的便捷方式。这会增加客户端和服务器之间的耦合,通常对于公共 API 来说不是一个好的选择,但对于内部 API 可能正是所需要的。这是 Spring Cloud 中常用的一种方法,这也是为什么 @HttpExchange 作为 @RequestMapping 的替代方案,被支持用于控制器类的服务器端处理。

例如:

@HttpExchange("/persons")
interface PersonService {

    @GetExchange("/{id}")
    Person getPerson(@PathVariable Long id);

    @PostExchange
    void add(@RequestBody Person person);
}

@RestController
class PersonController implements PersonService {

    public Person getPerson(@PathVariable Long id) {
        //...
    }

    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        //...
    }
}

@HttpExchange 和 @RequestMapping 存在差异。@RequestMapping 可以通过路径模式、HTTP 方法等匹配任意数量的请求,而 @HttpExchange 则声明一个具有具体 HTTP 方法、路径和内容类型的单一端点。

对于方法参数和返回值,一般来说,@HttpExchange 支持的方法参数是 @RequestMapping 的子集。值得注意的是,它排除了任何服务器端特定的参数类型。

# 35.1、处理方法

@RequestMapping 处理方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。

# a、章节概要
  • 方法参数
  • 返回值
  • 类型转换
  • 矩阵变量
  • @RequestParam
  • @RequestHeader
  • @CookieValue
  • @ModelAttribute
  • @SessionAttributes
  • @SessionAttribute
  • @RequestAttribute
  • 多部分内容
  • @RequestBody
  • HttpEntity
  • @ResponseBody
  • ResponseEntity
  • Jackson JSON
# b、方法参数

下面的表格展示了受支持的控制器方法参数。

对于需要阻塞 I/O 进行解析的参数(例如读取请求体),支持使用响应式类型(Reactor、RxJava 或其他响应式库)。这一点会在 “说明” 列中注明。对于不需要阻塞操作的参数,不建议使用响应式类型。

JDK 1.8 中的 java.util.Optional 可以作为方法参数,与具有 required 属性的注解(例如 @RequestParam、@RequestHeader 等)结合使用,其效果等同于 required=false。

控制器方法参数 说明
ServerWebExchange 可访问完整的 ServerWebExchange,它是 HTTP 请求和响应、请求和会话属性、checkNotModified 方法等的容器。
ServerHttpRequest, ServerHttpResponse 可访问 HTTP 请求或响应。
WebSession 可访问会话。除非添加了属性,否则不会强制启动新会话。支持响应式类型。
java.security.Principal 当前经过身份验证的用户 —— 如果已知,可能是特定的 Principal 实现类。支持响应式类型。
org.springframework.http.HttpMethod 请求的 HTTP 方法。
java.util.Locale 当前请求的区域设置,由可用的最具体的 LocaleResolver 确定,实际上就是已配置的 LocaleResolver/LocaleContextResolver。
java.util.TimeZone + java.time.ZoneId 与当前请求关联的时区,由 LocaleContextResolver 确定。
@PathVariable 用于访问 URI 模板变量。请参阅 URI 模式)。
@MatrixVariable 用于访问 URI 路径段中的名值对。请参阅矩阵变量。
@RequestParam 用于访问查询参数。参数值会转换为声明的方法参数类型。请参阅 @RequestParam。
注意,@RequestParam 的使用是可选的,例如用于设置其属性。请参阅表格后面的 “任何其他参数”。
@RequestHeader 用于访问请求头。头信息值会转换为声明的方法参数类型。请参阅 @RequestHeader。
@CookieValue 用于访问 Cookie。Cookie 值会转换为声明的方法参数类型。请参阅 @CookieValue。
@RequestBody 用于访问 HTTP 请求体。使用 HttpMessageReader 实例将请求体内容转换为声明的方法参数类型。支持响应式类型。请参阅 @RequestBody。
HttpEntity<B> 用于访问请求头和请求体。请求体会使用 HttpMessageReader 实例进行转换。支持响应式类型。请参阅 HttpEntity。
@RequestPart 用于访问 multipart/form-data 请求中的某个部分。支持响应式类型。请参阅多部分内容和多部分数据。
java.util.Map 或 org.springframework.ui.Model 用于访问在 HTML 控制器中使用的模型,该模型在视图渲染时会暴露给模板。
@ModelAttribute 用于访问模型中已有的属性(若不存在则会实例化),并应用数据绑定和验证。请参阅 @ModelAttribute 以及 Model) 和 DataBinder)。
注意,@ModelAttribute 的使用是可选的,例如用于设置其属性。请参阅表格后面的 “任何其他参数”。
Errors 或 BindingResult 用于访问命令对象(即 @ModelAttribute 参数)的验证和数据绑定错误。Errors 或 BindingResult 参数必须紧跟在经过验证的方法参数之后声明。
SessionStatus + 类级别的 @SessionAttributes 用于标记表单处理完成,这会触发清理通过类级别的 @SessionAttributes 注解声明的会话属性。更多详情请参阅 @SessionAttributes。
UriComponentsBuilder 用于准备相对于当前请求的主机、端口、方案和上下文路径的 URL。请参阅 URI 链接。
@SessionAttribute 用于访问任意会话属性,与通过类级别的 @SessionAttributes 声明存储在会话中的模型属性不同。更多详情请参阅 @SessionAttribute。
@RequestAttribute 用于访问请求属性。更多详情请参阅 @RequestAttribute。
任何其他参数 如果方法参数与上述情况均不匹配,默认情况下,如果它是简单类型(由 BeanUtils#isSimpleProperty (opens new window) 判断),则会解析为 @RequestParam;否则,会解析为 @ModelAttribute。
# c、返回值

下表展示了受支持的控制器方法返回值。请注意,来自Reactor、RxJava等库的响应式类型或其他通常对所有返回值都支持。

对于像Flux这样期望返回多个值的返回类型,元素会在产生时即被流式传输,而不会进行缓冲。这是默认行为,因为将大量元素存储在内存中并不高效。如果媒体类型暗示是无限流(例如,application/json+stream),则值会逐个写入并刷新。否则,值会逐个写入,刷新操作会单独进行。

注意:如果在将元素编码为JSON时发生错误,响应可能已经被写入并提交,此时就无法返回适当的错误响应。在某些情况下,应用程序可以选择通过缓冲元素并一次性进行编码,以牺牲内存效率为代价来更好地处理此类错误。控制器可以返回一个Flux<List<B>>;Reactor为此提供了一个专门的操作符Flux#collectList()。

控制器方法返回值 描述
@ResponseBody 返回值通过HttpMessageWriter实例进行编码,并写入响应。请参阅 @ResponseBody。
HttpEntity<B>、ResponseEntity<B> 返回值指定完整的响应,包括HTTP头,并且主体通过HttpMessageWriter实例进行编码并写入响应。请参阅 ResponseEntity。
HttpHeaders 用于返回带有头信息但无主体的响应。
ErrorResponse 用于在主体中渲染RFC 9457错误响应详细信息,请参阅 错误响应。
ProblemDetail 用于在主体中渲染RFC 9457错误响应详细信息,请参阅 错误响应。
String 一个视图名称,将通过ViewResolver实例进行解析,并与隐式模型一起使用 —— 隐式模型由命令对象和@ModelAttribute方法确定。处理方法还可以通过声明一个Model参数来以编程方式丰富模型(前面已描述)。
View 一个View实例,用于与隐式模型一起渲染 —— 隐式模型由命令对象和@ModelAttribute方法确定。处理方法还可以通过声明一个Model参数来以编程方式丰富模型(前面已描述)。
java.util.Map、org.springframework.ui.Model 要添加到隐式模型中的属性,视图名称根据请求路径隐式确定。
@ModelAttribute 要添加到模型中的一个属性,视图名称根据请求路径隐式确定。
注意,@ModelAttribute是可选的。请参阅此表后面的“任何其他返回值”。
Rendering 用于处理模型和视图渲染场景的API。
FragmentsRendering、Flux<Fragment>、Collection<Fragment> 用于渲染一个或多个带有各自视图和模型的片段。有关更多详细信息,请参阅 HTML片段。
void 具有void返回类型(可能是异步的,例如Mono<Void>)的方法(或返回值为null),如果它还具有ServerHttpResponse、ServerWebExchange参数或@ResponseStatus注解,则被认为已完全处理了响应。如果控制器进行了有效的ETag或lastModified时间戳检查,情况也是如此。详细信息请参阅 控制器。
如果上述情况都不满足,void返回类型也可以表示REST控制器的“无响应主体”或HTML控制器的默认视图名称选择。
Flux<ServerSentEvent>、Observable<ServerSentEvent>或其他响应式类型 用于发送服务器发送事件。当只需要写入数据时,可以省略ServerSentEvent包装器(但是,必须通过produces属性在映射中请求或声明text/event-stream)。
其他返回值 如果以其他方式返回的值仍未得到处理,则将其视为模型属性,除非它是由 BeanUtils#isSimpleProperty (opens new window) 确定的简单类型,在这种情况下,它仍未得到处理。
# d、类型转换

一些用于表示基于字符串的请求输入的注解控制器方法参数(例如 @RequestParam、@RequestHeader、@PathVariable、@MatrixVariable 和 @CookieValue),如果参数声明为 String 以外的类型,则可能需要进行类型转换。

对于这种情况,会根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(如 int、long、Date 等)。类型转换可以通过 WebDataBinder 进行自定义(参见 DataBinder),也可以通过向 FormattingConversionService 注册 Formatters 来实现(参见 Spring 字段格式化)。

类型转换中的一个实际问题是对空字符串源值的处理。如果空字符串在类型转换后变为 null,则会被视为缺失值。对于 Long、UUID 等目标类型,可能会出现这种情况。如果希望允许注入 null,可以在参数注解上使用 required 标志,或者将参数声明为 @Nullable。

# e、矩阵变量

RFC 3986 (opens new window) 讨论了路径段中的名值对。在Spring WebFlux中,我们根据蒂姆·伯纳斯 - 李的一篇“旧文章” (opens new window) 将其称为“矩阵变量”,不过它们也可以被称为URI路径参数。

矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔。例如,"/cars;color=red,green;year=2012"。多个值也可以通过重复的变量名来指定,例如"color=red;color=green;color=blue"。

与Spring MVC不同,在WebFlux中,URL中矩阵变量的有无不会影响请求映射。换句话说,你无需使用URI变量来掩盖可变内容。也就是说,如果你想从控制器方法中访问矩阵变量,需要在预期有矩阵变量的路径段中添加一个URI变量。以下示例展示了具体做法:

// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
    // petId == 42
    // q == 11
}

由于所有路径段都可能包含矩阵变量,有时你可能需要明确指出矩阵变量预期所在的路径变量,如下例所示:

// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {
    // q1 == 11
    // q2 == 22
}

可以将矩阵变量定义为可选的,并指定默认值,如下例所示:

// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
    // q == 1
}

若要获取所有矩阵变量,可以使用MultiValueMap,如下例所示:

// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
# f、@RequestParam

你可以使用 @RequestParam 注解将查询参数绑定到控制器方法的参数上。以下代码片段展示了其用法:

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    //...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { // (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    //...
}
  1. 使用 @RequestParam。
# 提示

Servlet API 的“请求参数”概念将查询参数、表单数据和多部分内容合并为一个。然而,在 WebFlux 中,每个部分都可以通过 ServerWebExchange 单独访问。虽然 @RequestParam 仅绑定到查询参数,但你可以使用数据绑定将查询参数、表单数据和多部分内容应用到命令对象上。

使用 @RequestParam 注解的方法参数默认是必需的,但你可以通过将 @RequestParam 的 required 标志设置为 false,或者使用 java.util.Optional 包装参数来指定该方法参数是可选的。

如果目标方法参数类型不是 String,则会自动进行类型转换。请参阅类型转换。

当 @RequestParam 注解声明在 Map<String, String> 或 MultiValueMap<String, String> 参数上时,该映射将填充所有查询参数。

请注意,@RequestParam 的使用是可选的 —— 例如,用于设置其属性。默认情况下,任何简单值类型(由 BeanUtils#isSimpleProperty (opens new window) 确定)且未被其他参数解析器解析的参数,都将被视为使用了 @RequestParam 注解。

# g、@RequestHeader

你可以使用 @RequestHeader 注解将请求头绑定到控制器中的方法参数上。

以下示例展示了一个带有请求头的请求:

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

以下示例获取了 Accept-Encoding 和 Keep-Alive 头的值:

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, // (1) 获取 Accept-Encoding 头的值
        @RequestHeader("Keep-Alive") long keepAlive) {    // (2) 获取 Keep-Alive 头的值
    //...
}

如果目标方法参数类型不是 String,则会自动进行类型转换。请参阅 类型转换。

当 @RequestHeader 注解用于 Map<String, String>、MultiValueMap<String, String> 或 HttpHeaders 参数时,该映射将填充所有请求头的值。

提示:系统内置支持将逗号分隔的字符串转换为字符串数组或集合,以及类型转换系统已知的其他类型。例如,使用 @RequestHeader("Accept") 注解的方法参数可以是 String 类型,也可以是 String[] 或 List<String> 类型。

# h、@CookieValue

您可以使用 @CookieValue 注解将 HTTP cookie 的值绑定到控制器方法的参数上。

以下示例展示了一个包含 cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下代码示例演示了如何获取 cookie 的值:

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { // (1)获取 cookie 值
    //...
}

如果目标方法参数的类型不是 String,系统会自动进行类型转换。请参阅类型转换。

# i、@ModelAttribute

@ModelAttribute 方法参数注解可将表单数据、查询参数、URI 路径变量和请求头绑定到模型对象上。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } // 1. 绑定到 Pet 类的一个实例

表单数据和查询参数的优先级高于 URI 变量和请求头,只有当它们不覆盖同名的请求参数时,URI 变量和请求头才会被包含进来。请求头名称中的短横线会被去除。

Pet 实例可以通过以下方式获取:

  • 从模型中获取,该实例可能已由 Model 添加到模型中。
  • 如果模型属性已在类级别的 @SessionAttributes 中列出,则可以从 HTTP 会话中获取。
  • 通过默认构造函数进行实例化。
  • 通过“主构造函数”进行实例化,构造函数的参数与 Servlet 请求参数相匹配。参数名称通过字节码中保留的运行时参数名称来确定。

默认情况下,构造函数和属性的数据绑定都会应用。然而,模型对象的设计需要仔细考虑,出于安全原因,建议使用专门为 Web 绑定定制的对象,或仅应用构造函数绑定。如果仍需使用属性绑定,则应设置 allowedFields 模式,以限制可以设置的属性。有关详细信息和示例配置,请参见模型设计。

使用构造函数绑定时,可以通过 @BindParam 注解自定义请求参数名称。例如:

class Account {

    private final String firstName;

    public Account(@BindParam("first-name") String firstName) {
        this.firstName = firstName;
    }
}

注意:@BindParam 也可以放在与构造函数参数对应的字段上。虽然 @BindParam 是开箱即用的,但你也可以通过在 DataBinder 上设置 DataBinder.NameResolver 来使用不同的注解。

构造函数绑定支持 List、Map 和数组参数,可以从单个字符串(例如用逗号分隔的列表)转换而来,也可以基于索引键(如 accounts[2].name 或 account[KEY].name)。

与 Spring MVC 不同,WebFlux 支持模型中的响应式类型,例如 Mono<Account>。你可以声明带有或不带有响应式类型包装器的 @ModelAttribute 参数,它将相应地解析为实际值。

如果数据绑定导致错误,默认情况下会抛出 WebExchangeBindException,但你也可以在 @ModelAttribute 之后立即添加 BindingResult 参数,以便在控制器方法中处理这些错误。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // 1. 添加一个 BindingResult
    if (result.hasErrors()) {
        return "petForm";
    }
    //...
}

要使用 BindingResult 参数,你必须在其之前声明 @ModelAttribute 参数,且不能带有响应式类型包装器。如果你想使用响应式类型,可以直接通过它处理错误。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
       .flatMap(pet -> {
            //...
        })
       .onErrorResume(ex -> {
            //...
        });
}

通过添加 jakarta.validation.Valid 注解或 Spring 的 @Validated 注解(请参阅Bean 验证和Spring 验证),可以在数据绑定后自动应用验证。例如:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // 1. 在模型属性参数上使用 @Valid 注解
    if (result.hasErrors()) {
        return "petForm";
    }
    //...
}

如果由于其他参数带有 @Constraint 注解而应用了方法验证,则会抛出 HandlerMethodValidationException。请参阅控制器方法验证部分。

提示:使用 @ModelAttribute 是可选的。默认情况下,任何不是由 BeanUtils#isSimpleProperty 确定的简单值类型,并且没有被其他参数解析器解析的参数,都将被视为隐式的 @ModelAttribute。

警告:使用 GraalVM 编译为原生镜像时,上述隐式 @ModelAttribute 支持无法对相关数据绑定反射提示进行适当的提前推断。因此,建议在 GraalVM 原生镜像中使用时,使用 @ModelAttribute 显式注解方法参数。

# j、@SessionAttributes

@SessionAttributes 用于在请求之间将模型属性存储在 WebSession 中。它是一个类级别的注解,用于声明特定控制器使用的会话属性。该注解通常会列出模型属性的名称,或者应该透明地存储在会话中以供后续请求访问的模型属性的类型。

考虑以下示例:

@Controller
@SessionAttributes("pet") // (1)
public class EditPetForm {
    //...
}
  1. 使用 @SessionAttributes 注解。

在第一次请求时,当名为 pet 的模型属性被添加到模型中,它会自动提升并保存到 WebSession 中。它会一直保留在那里,直到另一个控制器方法使用 SessionStatus 方法参数来清除该存储,如下例所示:

@Controller
@SessionAttributes("pet") // (1)
public class EditPetForm {

    //...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) { // (2)
        if (errors.hasErrors()) {
            //...
        }
        status.setComplete();
        //...
    }
}
  1. 使用 @SessionAttributes 注解。
  2. 使用 SessionStatus 变量。
# k、@SessionAttribute

如果你需要访问全局管理的现有会话属性(即在控制器外部管理,例如通过过滤器),且这些属性可能存在也可能不存在,你可以在方法参数上使用 @SessionAttribute 注解,如下例所示:

@GetMapping("/")
public String handle(@SessionAttribute User user) { // 1. 使用 `@SessionAttribute`
    //...
}
  1. 使用 @SessionAttribute。

对于需要添加或移除会话属性的用例,可以考虑将 WebSession 注入到控制器方法中。

对于在控制器工作流中将会话用作模型属性的临时存储场景,可以考虑使用 SessionAttributes,详见 @SessionAttributes。

# l、@RequestAttribute

和@SessionAttribute类似,你可以使用@RequestAttribute注解来访问此前创建好的请求属性(例如,由WebFilter创建),如下例所示:

@GetMapping("/")
public String handle(@RequestAttribute Client client) { // 1. 使用 `@RequestAttribute`
    //...
}
  1. 使用@RequestAttribute。
# m、多部分内容

正如 多部分数据 中所解释的,ServerWebExchange 提供了访问多部分内容的途径。在控制器中处理文件上传表单(例如来自浏览器的表单)的最佳方式是将数据绑定到 命令对象,如下示例所示:

class MyForm {

    private String name;

    private MultipartFile file;

    //...
}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        //...
    }
}

在 RESTful 服务场景中,你还可以从非浏览器客户端提交多部分请求。以下示例展示了如何同时使用文件和 JSON:

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... 文件数据...

你可以使用 @RequestPart 来访问各个部分,如下示例所示:

@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, // (1)
                     @RequestPart("file-data") FilePart file) { // (2)
    //...
}
  1. 使用 @RequestPart 获取元数据。
  2. 使用 @RequestPart 获取文件。

要对原始部分内容进行反序列化(例如转换为 JSON,类似于 @RequestBody),你可以声明一个具体的目标 Object,而不是 Part,如下示例所示:

@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { // (1)
    //...
}
  1. 使用 @RequestPart 获取元数据。

你可以将 @RequestPart 与 jakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,这将触发标准 Bean 验证。验证错误会导致抛出 WebExchangeBindException,并返回 400(BAD_REQUEST)响应。该异常包含一个 BindingResult,其中包含错误详情,也可以在控制器方法中通过声明带有异步包装器的参数,然后使用与错误相关的操作符来处理:

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // 使用 onError* 操作符之一...
}

如果由于其他参数带有 @Constraint 注解而进行方法验证,则会抛出 HandlerMethodValidationException。有关详细信息,请参阅 验证。

要将所有多部分数据作为 MultiValueMap 访问,你可以使用 @RequestBody,如下示例所示:

@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { // (1)
    //...
}
  1. 使用 @RequestBody。
# PartEvent

若要以流式顺序访问多部分数据,你可以将 @RequestBody 与 Flux<PartEvent> 一起使用。在多部分 HTTP 消息中,每个部分至少会产生一个 PartEvent,其中包含头部信息和该部分内容的缓冲区。

  • 表单字段将产生一个 单一 的 FormPartEvent,其中包含该字段的值。
  • 文件上传将产生 一个或多个 FilePartEvent 对象,其中包含上传时使用的文件名。如果文件足够大,需要分割到多个缓冲区中,则第一个 FilePartEvent 之后会跟随后续的事件。

例如:

@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { // (1)
    allPartsEvents.windowUntil(PartEvent::isLast) // (2)
           .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { // (3)
                if (signal.hasValue()) {
                    PartEvent event = signal.get();
                    if (event instanceof FormPartEvent formEvent) { // (4)
                        String value = formEvent.value();
                        // 处理表单字段
                    }
                    else if (event instanceof FilePartEvent fileEvent) { // (5)
                        String filename = fileEvent.filename();
                        Flux<DataBuffer> contents = partEvents.map(PartEvent::content); // (6)
                        // 处理文件上传
                    }
                    else {
                        return Mono.error(new RuntimeException("Unexpected event: " + event));
                    }
                }
                else {
                    return partEvents; // 完成或错误信号
                }
            }));
}
  1. 使用 @RequestBody。
  2. 某个特定部分的最后一个 PartEvent 的 isLast() 会被设置为 true,并且后面可能会跟随属于后续部分的额外事件。这使得 isLast 属性适合作为 Flux::windowUntil 操作符的谓词,以便将所有部分的事件分割成每个都属于单个部分的窗口。
  3. Flux::switchOnFirst 操作符允许你判断正在处理的是表单字段还是文件上传。
  4. 处理表单字段。
  5. 处理文件上传。
  6. 必须完全消耗、中继或释放主体内容,以避免内存泄漏。

接收到的部分事件也可以通过 WebClient 转发到另一个服务。有关详细信息,请参阅 多部分数据。

# n、@RequestBody

可以使用 @RequestBody 注解,通过 HttpMessageReader 将请求体读取并反序列化为一个 Object。以下示例使用了一个 @RequestBody 参数:

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    //...
}

与 Spring MVC 不同,在 WebFlux 中,@RequestBody 方法参数支持响应式类型,并且能进行完全无阻塞的读取以及(客户端到服务器的)流式处理。

@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    //...
}

可以使用 WebFlux 配置 中的 HTTP 消息编解码器 选项来配置或自定义消息读取器。

可以将 @RequestBody 与 jakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,这将应用标准的 Bean 验证。验证错误会引发 WebExchangeBindException,并返回 400(BAD_REQUEST)响应。该异常包含一个带有错误详细信息的 BindingResult,可以在控制器方法中通过声明带有异步包装器的参数,然后使用与错误相关的操作符来处理:

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // 使用 onError* 操作符之一...
}

也可以声明一个 Errors 参数来访问验证错误,但在这种情况下,请求体不能是 Mono,并且会首先被解析:

@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, Errors errors) {
    // 使用 onError* 操作符之一...
}

如果由于其他参数带有 @Constraint 注解而应用了方法验证,则会抛出 HandlerMethodValidationException。更多详细信息,请参阅 验证 部分。

# o、HttpEntity

HttpEntity 与使用 @RequestBody 大体相同,但它基于一个容器对象,该对象能暴露请求头和请求体。以下示例展示了如何使用 HttpEntity:

@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    //...
}
# p、@ResponseBody

你可以在方法上使用 @ResponseBody 注解,通过 HttpMessageWriter 将返回值序列化为响应体。以下示例展示了具体用法:

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    //...
}

@ResponseBody 也可在类级别使用,这种情况下,所有控制器方法都会继承该注解。这正是 @RestController 的作用,它不过是一个标记了 @Controller 和 @ResponseBody 的元注解。

@ResponseBody 支持响应式类型,这意味着你可以返回 Reactor 或 RxJava 类型,并将它们产生的异步值渲染到响应中。更多详细信息,请参阅 流式处理 和 JSON 渲染。

你可以将 @ResponseBody 方法与 JSON 序列化视图结合使用。详情请参阅 Jackson JSON。

你可以使用 WebFlux 配置 中的 HTTP 消息编解码器 选项来配置或自定义消息写入。

# q、ResponseEntity

ResponseEntity 类似于 @ResponseBody,但它还可以设置状态码和响应头。例如:

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body =... ;
    String etag =... ;
    return ResponseEntity.ok().eTag(etag).body(body);
}

WebFlux 支持使用单个值的 响应式类型 来异步生成 ResponseEntity,并且对于响应体还支持单值和多值的响应式类型。这使得使用 ResponseEntity 可以实现多种异步响应,具体如下:

  • ResponseEntity<Mono<T>> 或 ResponseEntity<Flux<T>> 可以立即确定响应状态和响应头,而响应体则在稍后异步提供。如果响应体由 0 到 1 个值组成,则使用 Mono;如果响应体可以产生多个值,则使用 Flux。
  • Mono<ResponseEntity<T>> 会在稍后异步提供响应状态、响应头和响应体。这样可以根据异步请求处理的结果来改变响应状态和响应头。
  • Mono<ResponseEntity<Mono<T>>> 或 Mono<ResponseEntity<Flux<T>>> 也是一种可行但不太常见的选择。它们会先异步提供响应状态和响应头,然后再异步提供响应体。
# r、JSON

Spring 提供了对 Jackson JSON 库的支持。

# 35.2、视图

Spring WebFlux 为 Jackson 的序列化视图 (opens new window) 提供了内置支持,这允许我们仅渲染 Object 中部分字段。若要在使用 @ResponseBody 或 ResponseEntity 的控制器方法中使用它,可以使用 Jackson 的 @JsonView 注解来激活序列化视图类,示例如下:

@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

注意:@JsonView 允许传入一个视图类数组,但每个控制器方法只能指定一个。如果需要激活多个视图,请使用组合接口。

# 35.3、Model

你可以使用 @ModelAttribute 注解,具体方式如下:

  • 在 @RequestMapping 方法的 方法参数 中使用,从模型中创建或访问对象,并通过 WebDataBinder 将其绑定到请求。
  • 作为 @Controller 或 @ControllerAdvice 类中的方法级注解,帮助在任何 @RequestMapping 方法调用之前初始化模型。
  • 在 @RequestMapping 方法上使用,将其返回值标记为模型属性。

本节讨论 @ModelAttribute 方法,即上述列表中的第二项。一个控制器可以有任意数量的 @ModelAttribute 方法。在同一控制器中,所有此类方法都会在 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在多个控制器之间共享。更多详情请参阅 控制器通知 部分。

@ModelAttribute 方法的方法签名非常灵活。它们支持许多与 @RequestMapping 方法相同的参数(除了 @ModelAttribute 本身以及与请求体相关的任何参数)。

以下示例展示了如何使用 @ModelAttribute 方法:

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // 添加更多...
}

以下示例只添加一个属性:

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}

注意:当没有显式指定名称时,会根据类型选择一个默认名称,具体解释请参阅 Conventions (opens new window) 的 Java 文档。你始终可以使用重载的 addAttribute 方法或通过 @ModelAttribute 上的名称属性(针对返回值)来指定显式名称。

与 Spring MVC 不同,Spring WebFlux 明确支持在模型中使用响应式类型(例如 Mono<Account> 或 io.reactivex.Single<Account>)。在调用 @RequestMapping 时,如果 @ModelAttribute 参数声明时没有使用包装器,这些异步模型属性可以透明地解析为其实际值(并更新模型),如下例所示:

@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    //...
}

此外,任何具有响应式类型包装器的模型属性都会在视图渲染之前解析为其实际值(并更新模型)。

你还可以将 @ModelAttribute 用作 @RequestMapping 方法的方法级注解,在这种情况下,@RequestMapping 方法的返回值将被解释为模型属性。在 HTML 控制器中,这通常不是必需的,因为这是默认行为,除非返回值是一个 String,否则它会被解释为视图名称。@ModelAttribute 还可以帮助自定义模型属性名称,如下例所示:

@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    //...
    return account;
}

# 35.4、DataBinder

# 36、前置说明

带有 @Controller 或 @ControllerAdvice 注解的类可以有 @InitBinder 方法,用于初始化 WebDataBinder 实例。而 WebDataBinder 实例能够实现以下功能:

  • 将请求参数绑定到模型对象。
  • 将请求中的字符串值转换为对象属性类型。
  • 在渲染 HTML 表单时,将模型对象的属性格式化为字符串。

在使用 @Controller 注解的控制器中,对 DataBinder 的自定义设置仅在该控制器内部生效,甚至可以通过注解精确到特定的模型属性。而对于使用 @ControllerAdvice 注解的类,其自定义设置可以应用于所有控制器或部分控制器。

可以在 DataBinder 中注册 PropertyEditor、Converter 和 Formatter 组件,来进行类型转换。另外,也可以使用 WebFlux 配置),在全局共享的 FormattingConversionService 中注册 Converter 和 Formatter 组件。

以下是 Java 示例代码,展示了如何使用 @InitBinder 注解初始化 WebDataBinder:

@Controller
public class FormController {

    @InitBinder // 1. 使用 @InitBinder 注解
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    //...
}
  1. 使用 @InitBinder 注解。

另外,当通过共享的 FormattingConversionService 使用基于 Formatter 的设置时,可以采用相同的方法来注册特定于控制器的 Formatter 实例,示例如下:

@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); // 1. 添加自定义格式化器(这里是 DateFormatter)
    }

    //...
}
  1. 添加自定义格式化器(这里是 DateFormatter)。

# 37、模型设计

Web 请求的数据绑定是指将请求参数绑定到模型对象。默认情况下,请求参数可以绑定到模型对象的任何公共属性。这意味着恶意客户端可能会为模型对象图中存在但不希望被设置的属性提供额外的值。因此,在设计模型对象时需要谨慎考虑。

提示:模型对象及其嵌套对象图有时也被称为“命令对象”、“表单支持对象”或“POJO(普通 Java 对象)”。

一个好的做法是使用专用的模型对象,而不是直接将 JPA 或 Hibernate 实体等领域模型用于 Web 数据绑定。例如,在一个用于更改电子邮件地址的表单中,可以创建一个 ChangeEmailForm 模型对象,该对象只声明输入所需的属性:

public class ChangeEmailForm {

    private String oldEmailAddress;
    private String newEmailAddress;

    public void setOldEmailAddress(String oldEmailAddress) {
        this.oldEmailAddress = oldEmailAddress;
    }

    public String getOldEmailAddress() {
        return this.oldEmailAddress;
    }

    public void setNewEmailAddress(String newEmailAddress) {
        this.newEmailAddress = newEmailAddress;
    }

    public String getNewEmailAddress() {
        return this.newEmailAddress;
    }

}

另一个好的做法是应用 构造函数绑定,它只使用请求参数作为构造函数的参数,而忽略其他输入。这与属性绑定不同,属性绑定默认会绑定每个与匹配属性对应的请求参数。

如果专用模型对象和构造函数绑定都不足以满足需求,必须使用属性绑定时,强烈建议在 WebDataBinder 中注册 allowedFields 模式(区分大小写),以防止设置意外的属性。例如:

@Controller
public class ChangeEmailController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
    }

    // @RequestMapping 方法等

}

也可以注册 disallowedFields 模式(不区分大小写)。但是,建议使用“允许”配置而不是“禁止”配置,因为它更加明确,更不容易出错。

默认情况下,构造函数绑定和属性绑定都会使用。如果只想使用构造函数绑定,可以通过 @InitBinder 方法在控制器内部局部地设置 WebDataBinder 的 declarativeBinding 标志,或者通过 @ControllerAdvice 全局设置。打开此标志后,将仅使用构造函数绑定,除非配置了 allowedFields 模式,否则不会使用属性绑定。例如:

@Controller
public class MyController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.setDeclarativeBinding(true);
    }

    // @RequestMapping 方法等

}

# 37.1、验证

Spring WebFlux 为 @RequestMapping 方法提供了内置的 验证,包括 Java Bean 验证。验证可以在两个级别上应用:

  1. 方法参数级验证:如果方法参数使用 Jakarta 的 @Valid 或 Spring 的 @Validated 注解,并且后面没有紧接 Errors 或 BindingResult 参数,而且不需要进行方法级验证(后续讨论),则 @ModelAttribute、@RequestBody 和 @RequestPart 参数解析器会单独验证方法参数。这种情况下抛出的异常是 WebExchangeBindException。
  2. 方法级验证:当 @Min、@NotBlank 等 @Constraint 注解直接声明在方法参数上或方法上(针对返回值)时,必须应用方法级验证。方法级验证会取代方法参数级验证,因为它通过 @Valid 同时涵盖了方法参数约束和嵌套约束。这种情况下抛出的异常是 HandlerMethodValidationException。

应用程序必须处理 WebExchangeBindException 和 HandlerMethodValidationException,因为根据控制器方法签名的不同,这两种异常都有可能抛出。不过,这两种异常的设计非常相似,可以用几乎相同的代码来处理。主要区别在于,前者针对单个对象,后者针对方法参数列表。

注意:@Valid 不是一个约束注解,而是用于对象内部的嵌套约束。因此,单独使用 @Valid 不会触发方法级验证。另一方面,@NotNull 是一个约束注解,将其添加到带有 @Valid 的参数上会触发方法级验证。具体对于空值检查,你也可以使用 @RequestBody 或 @ModelAttribute 的 required 标志。

方法级验证可以与 Errors 或 BindingResult 方法参数结合使用。但是,只有当所有验证错误都出现在紧跟 Errors 的方法参数上时,才会调用控制器方法。如果任何其他方法参数上存在验证错误,则会抛出 HandlerMethodValidationException。

你可以通过 WebFlux 配置) 全局配置 Validator,也可以通过 @Controller 或 @ControllerAdvice 中的 @InitBinder 方法 进行局部配置。你还可以使用多个验证器。

注意:如果控制器类上有 @Validated 注解,则会通过 AOP 代理应用 方法级验证。为了利用 Spring Framework 6.1 中为 Spring MVC 新增的内置方法级验证支持,你需要从控制器类上移除 @Validated 注解。

错误响应) 部分提供了关于如何处理 WebExchangeBindException 和 HandlerMethodValidationException 的详细信息,以及如何通过 MessageSource 和特定于区域设置和语言的资源束来定制它们的渲染。

对于方法级验证错误的进一步自定义处理,你可以扩展 ResponseEntityExceptionHandler,或者在控制器或 @ControllerAdvice 中使用 @ExceptionHandler 方法,并直接处理 HandlerMethodValidationException。该异常包含一个 ParameterValidationResult 列表,这些结果按方法参数对验证错误进行分组。你可以遍历这些结果,或者根据控制器方法参数类型提供带有回调方法的访问者:

HandlerMethodValidationException ex =... ;

ex.visitResults(new HandlerMethodValidationException.Visitor() {

    @Override
    public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
        //...
    }

    @Override
    public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
        //...
    }

    @Override
    public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
        //...
    }

    @Override
    public void other(ParameterValidationResult result) {
        //...
    }
});

# 37.2、异常

@Controller 和 @ControllerAdvice 类可以包含 @ExceptionHandler 方法,用来处理控制器方法抛出的异常。下面的例子中包含了一个这样的处理方法:

import java.io.IOException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Controller
public class SimpleController {

    @ExceptionHandler(IOException.class)
    public ResponseEntity<String> handle() {
        return ResponseEntity.internalServerError().body("Could not read file storage");
    }

}

异常匹配既可以针对被抛出的顶级异常(也就是直接抛出 IOException),也可以针对顶级包装异常中的直接原因(例如,IllegalStateException 包装的 IOException)。

对于匹配异常类型,建议像前面的例子那样,将目标异常声明为方法参数。或者,注解声明可以缩小要匹配的异常类型范围。我们通常建议在方法参数签名中尽量明确,并在 @ControllerAdvice 中声明主要的根异常映射,并通过相应的顺序进行优先级排序。详细内容见MVC 部分。

注意:WebFlux 中的 @ExceptionHandler 方法支持与 @RequestMapping 方法相同的方法参数和返回值,但不包括与请求体和 @ModelAttribute 相关的方法参数。

Spring WebFlux 中对 @ExceptionHandler 方法的支持由处理 @RequestMapping 方法的 HandlerAdapter 提供。更多详情见 DispatcherHandler(../dispatcher-handler.html)。

# 38、媒体类型映射

除了异常类型,@ExceptionHandler 方法还可以声明可生产的媒体类型。这允许根据 HTTP 客户端请求的媒体类型(通常在 HTTP 请求头的 "Accept" 字段中指定)来优化错误响应。

应用程序可以针对相同的异常类型,直接在注解中声明可生产的媒体类型:

@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
    return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}

@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
    model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
    return "errorView";
}

这里的方法处理相同的异常类型,但不会被视为重复方法。相反,请求 "application/json" 的 API 客户端将收到一个 JSON 格式的错误信息,而浏览器将得到一个 HTML 错误视图。每个 @ExceptionHandler 注解可以声明多种可生产的媒体类型,错误处理阶段的内容协商机制会决定使用哪种内容类型。

# 39、方法参数

@ExceptionHandler 方法支持与 @RequestMapping 方法相同的方法参数,但前提是请求体可能尚未被消费。

# 40、返回值

@ExceptionHandler 方法支持与 @RequestMapping 方法相同的返回值。

# 40.1、控制器通知

通常情况下,@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法仅适用于声明它们的 @Controller 类(或类层次结构)内部。如果你希望这些方法能更广泛地应用(跨控制器),可以将它们声明在使用 @ControllerAdvice 或 @RestControllerAdvice 注解的类中。

@ControllerAdvice 使用 @Component 进行注解,这意味着这些类可以通过 组件扫描 注册为 Spring Bean。@RestControllerAdvice 是一个组合注解,它同时使用了 @ControllerAdvice 和 @ResponseBody 进行注解,这本质上意味着 @ExceptionHandler 方法的返回结果会通过消息转换直接输出到响应体中(而不是通过视图解析或模板渲染)。

在启动时,处理 @RequestMapping 和 @ExceptionHandler 方法的基础设施类会检测使用 @ControllerAdvice 注解的 Spring Bean,然后在运行时应用这些 Bean 中的方法。全局的 @ExceptionHandler 方法(来自 @ControllerAdvice)会在局部方法(来自 @Controller)之后应用。相反,全局的 @ModelAttribute 和 @InitBinder 方法会在局部方法之前应用。

默认情况下,@ControllerAdvice 方法适用于每个请求(即所有控制器),但你可以通过使用该注解的属性将其作用范围缩小到部分控制器,如下例所示:

// 针对所有使用 @RestController 注解的控制器
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// 针对特定包下的所有控制器
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// 针对可以赋值给特定类的所有控制器
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

上述示例中的选择器会在运行时进行评估,如果大量使用可能会对性能产生负面影响。更多详情请参阅 @ControllerAdvice (opens new window) 的 Javadoc。

# 41、函数式端点

Spring WebFlux 包含 WebFlux.fn,它是一种轻量级的函数式编程模型,在该模型中,函数用于路由和处理请求,并且契约设计为不可变的。它是基于注解的编程模型的替代方案,但同样运行在 响应式核心 基础之上。

# 41.1、概述

在 WebFlux.fn 中,HTTP 请求由 HandlerFunction 处理:这是一个接收 ServerRequest 并返回延迟的 ServerResponse (即 Mono<ServerResponse>)的函数。请求和响应对象的契约都是不可变的,它们提供了对 HTTP 请求和响应的 JDK 8 友好访问方式。HandlerFunction 相当于基于注解的编程模型中 @RequestMapping 方法的主体。

传入的请求通过 RouterFunction 路由到处理函数:这是一个接收 ServerRequest 并返回延迟的 HandlerFunction (即 Mono<HandlerFunction>)的函数。当路由函数匹配时,返回一个处理函数;否则返回一个空的 Mono。RouterFunction 相当于 @RequestMapping 注解,但主要区别在于路由函数不仅提供数据,还提供行为。

RouterFunctions.route() 提供了一个路由构建器,便于创建路由,如下例所示:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository =...;
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route() 
    // (1) 使用 route() 创建路由
   .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
   .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
   .POST("/person", handler::createPerson)
   .build();

public class PersonHandler {

    //...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        //...
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        //...
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {
        //...
    }
}

运行 RouterFunction 的一种方法是将其转换为 HttpHandler,并通过其中一个内置的 服务器适配器 进行安装:

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

大多数应用程序可以通过 WebFlux Java 配置运行,请参阅 运行服务器。

# 41.2、HandlerFunction

ServerRequest 和 ServerResponse 是不可变的接口,提供了对 HTTP 请求和响应的 JDK 8 友好访问方式。请求和响应都针对主体流提供了 响应式流 (opens new window) 背压。请求主体用 Reactor Flux 或 Mono 表示。响应主体用任何响应式流 Publisher 表示,包括 Flux 和 Mono。有关更多信息,请参阅 响应式库。

# a、ServerRequest

ServerRequest 提供了对 HTTP 方法、URI、标头和查询参数的访问,而对主体的访问则通过 body 方法提供。

以下示例将请求主体提取到 Mono<String> 中:

Mono<String> string = request.bodyToMono(String.class);

以下示例将主体提取到 Flux<Person> 中,其中 Person 对象是从某种序列化形式(如 JSON 或 XML)解码而来的:

Flux<Person> people = request.bodyToFlux(Person.class);

前面的示例是使用更通用的 ServerRequest.body(BodyExtractor) 的快捷方式,该方法接受 BodyExtractor 函数策略接口。工具类 BodyExtractors 提供了对许多实例的访问。例如,前面的示例也可以写成如下形式:

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

以下示例展示了如何访问表单数据:

Mono<MultiValueMap<String, String>> map = request.formData();

以下示例展示了如何将多部分数据作为映射访问:

Mono<MultiValueMap<String, Part>> map = request.multipartData();

以下示例展示了如何以流式方式逐个访问多部分数据:

Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
allPartEvents.windowUntil(PartEvent::isLast)
       .concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
            if (signal.hasValue()) {
                PartEvent event = signal.get();
                if (event instanceof FormPartEvent formEvent) {
                    String value = formEvent.value();
                    // 处理表单字段
                } else if (event instanceof FilePartEvent fileEvent) {
                    String filename = fileEvent.filename();
                    Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
                    // 处理文件上传
                } else {
                    return Mono.error(new RuntimeException("Unexpected event: " + event));
                }
            } else {
                return partEvents; // 完成或错误信号
            }
        }));

请注意,必须完全消耗、传递或释放 PartEvent 对象的主体内容,以避免内存泄漏。

# b、ServerResponse

ServerResponse 提供了对 HTTP 响应的访问,由于它是不可变的,因此可以使用 build 方法来创建它。可以使用构建器设置响应状态、添加响应标头或提供主体。以下示例创建了一个带有 JSON 内容的 200 (OK) 响应:

Mono<Person> person =...;
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

以下示例展示了如何构建一个带有 Location 标头且无主体的 201 (CREATED) 响应:

URI location =...;
ServerResponse.created(location).build();

根据使用的编解码器,可以传递提示参数来自定义主体的序列化或反序列化方式。例如,要指定 Jackson JSON 视图 (opens new window):

ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
# c、处理函数类

可以将处理函数写成 lambda 表达式,如下例所示:

HandlerFunction<ServerResponse> helloWorld =
    request -> ServerResponse.ok().bodyValue("Hello World");

这样很方便,但在应用程序中需要多个函数,多个内联 lambda 表达式可能会变得混乱。因此,将相关的处理函数组合到一个处理类中是很有用的,该类的作用类似于基于注解的应用程序中的 @Controller。例如,以下类公开了一个响应式 Person 存储库:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    // (1) listPeople 是一个处理函数,以 JSON 格式返回存储库中找到的所有 Person 对象
    public Mono<ServerResponse> listPeople(ServerRequest request) { 
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    // (2) createPerson 是一个处理函数,用于存储请求主体中包含的新 Person 对象
    public Mono<ServerResponse> createPerson(ServerRequest request) { 
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    // (3) getPerson 是一个处理函数,返回由 id 路径变量标识的单个人员
    public Mono<ServerResponse> getPerson(ServerRequest request) { 
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
              .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
              .switchIfEmpty(ServerResponse.notFound().build());
    }
}
# d、验证

函数式端点可以使用 Spring 的 验证工具 对请求主体进行验证。例如,假设有一个针对 Person 的自定义 Spring 验证器 实现:

public class PersonHandler {

    // (1) 创建 Validator 实例
    private final Validator validator = new PersonValidator(); 

    //...

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // (2) 应用验证
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); 
        return ok().build(repository.savePerson(person));
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            // (3) 抛出异常以返回 400 响应
            throw new ServerWebInputException(errors.toString()); 
        }
    }
}

处理函数还可以通过创建并注入基于 LocalValidatorFactoryBean 的全局 Validator 实例来使用标准的 Bean 验证 API(JSR - 303)。请参阅 Spring 验证。

# 41.3、RouterFunction

路由函数用于将请求路由到相应的 HandlerFunction。通常,您不必自己编写路由函数,而是使用 RouterFunctions 实用类中的方法来创建一个。RouterFunctions.route()(无参数)为您提供了一个流畅的构建器来创建路由函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 则提供了一种直接创建路由的方法。

一般来说,建议使用 route() 构建器,因为它为典型的映射场景提供了方便的快捷方式,而无需使用难以发现的静态导入。例如,路由函数构建器提供了 GET(String, HandlerFunction) 方法来为 GET 请求创建映射;以及 POST(String, HandlerFunction) 方法用于 POST 请求。

除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变体,它接受一个 RequestPredicate 作为参数,通过该参数可以表达额外的约束条件。

# a、谓词

您可以编写自己的 RequestPredicate,但 RequestPredicates 实用类提供了基于请求路径、HTTP 方法、内容类型等常用的实现。以下示例使用请求谓词基于 Accept 标头创建一个约束:

RouterFunction<ServerResponse> route = RouterFunctions.route()
   .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
         request -> ServerResponse.ok().bodyValue("Hello World")).build();

可以使用以下方法将多个请求谓词组合在一起:

  • RequestPredicate.and(RequestPredicate) — 两个谓词都必须匹配。
  • RequestPredicate.or(RequestPredicate) — 任意一个谓词匹配即可。

RequestPredicates 中的许多谓词都是组合而成的。例如,RequestPredicates.GET(String) 由 RequestPredicates.method(HttpMethod) 和 RequestPredicates.path(String) 组合而成。上面显示的示例也使用了两个请求谓词,因为构建器在内部使用了 RequestPredicates.GET,并将其与 accept 谓词组合在一起。

# b、路由

路由函数按顺序进行评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,在声明时,更具体的路由应该放在通用路由之前。在将路由函数注册为 Spring Bean 时,这一点也很重要,后面会进行描述。请注意,这种行为与基于注解的编程模型不同,在基于注解的编程模型中,会自动选择 "最具体" 的控制器方法。

使用路由函数构建器时,所有定义的路由都会组合成一个从 build() 返回的 RouterFunction。还有其他方法可以将多个路由函数组合在一起:

  • RouterFunctions.route() 构建器上的 add(RouterFunction)
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) — 是 RouterFunction.and() 与嵌套的 RouterFunctions.route() 的快捷方式。

以下示例展示了四个路由的组合:

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository =...;
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute =...;

RouterFunction<ServerResponse> route = route()
    // (1) GET /person/{id} 且 Accept 标头匹配 JSON 的请求将路由到 PersonHandler.getPerson
   .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) 
    // (2) GET /person 且 Accept 标头匹配 JSON 的请求将路由到 PersonHandler.listPeople
   .GET("/person", accept(APPLICATION_JSON), handler::listPeople) 
    // (3) POST /person 且无额外谓词的请求将映射到 PersonHandler.createPerson
   .POST("/person", handler::createPerson) 
    // (4) otherRoute 是在其他地方创建的路由函数,并添加到当前构建的路由中
   .add(otherRoute) 
   .build();
# c、嵌套路由

一组路由函数通常具有一个共享的谓词,例如共享路径。在上面的示例中,共享谓词将是一个与 /person 匹配的路径谓词,该路径被三个路由使用。在使用注解时,您可以通过使用映射到 /person 的类型级 @RequestMapping 注解来消除这种重复。在 WebFlux.fn 中,路径谓词可以通过路由函数构建器上的 path 方法进行共享。例如,通过使用嵌套路由,可以按以下方式改进上述示例的最后几行:

RouterFunction<ServerResponse> route = route()
   .path("/person", builder -> builder 
       .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
       .GET(accept(APPLICATION_JSON), handler::listPeople)
       .POST(handler::createPerson))
   .build();

虽然基于路径的嵌套是最常见的,但您可以通过构建器上的 nest 方法在任何类型的谓词上进行嵌套。上述示例中仍然存在一些重复的 Accept 标头谓词形式。可以通过将 nest 方法与 accept 结合使用来进一步改进:

RouterFunction<ServerResponse> route = route()
   .path("/person", b1 -> b1
       .nest(accept(APPLICATION_JSON), b2 -> b2
           .GET("/{id}", handler::getPerson)
           .GET(handler::listPeople))
       .POST(handler::createPerson))
   .build();

# 41.4、提供资源服务

WebFlux.fn 提供了内置的资源服务支持。

除了下面描述的功能之外,借助 RouterFunctions#resource(java.util.function.Function) (opens new window) 还可以实现更灵活的资源处理。

# a、重定向到资源

可以将匹配指定谓词的请求重定向到资源。例如,这在处理单页应用程序中的重定向时很有用。

ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
   .resource(spaPredicate, index)
   .build();
# b、从根位置提供资源服务

也可以将匹配给定模式的请求路由到相对于给定根位置的资源。

Resource location = new FileUrlResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);

# 41.5、运行服务器

如何在 HTTP 服务器中运行路由函数呢?一个简单的选择是使用以下方法之一将路由函数转换为 HttpHandler:

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后可以根据 HttpHandler 中的服务器特定说明,将返回的 HttpHandler 与多个服务器适配器一起使用。

一个更典型的选择(Spring Boot 也使用这种方式)是通过 WebFlux 配置 以基于 DispatcherHandler 的设置运行。WebFlux Java 配置使用 Spring 配置来声明处理请求所需的组件,它声明了以下基础设施组件来支持函数式端点:

  • RouterFunctionMapping:在 Spring 配置中检测一个或多个 RouterFunction<?> Bean,对它们进行 排序,通过 RouterFunction.andOther 将它们组合起来,并将请求路由到最终组合的 RouterFunction。
  • HandlerFunctionAdapter:一个简单的适配器,允许 DispatcherHandler 调用映射到请求的 HandlerFunction。
  • ServerResponseResultHandler:通过调用 ServerResponse 的 writeTo 方法来处理 HandlerFunction 调用的结果。

上述组件使函数式端点能够融入 DispatcherHandler 的请求处理生命周期,并且(如果声明了的话)还可能与带注解的控制器并行运行。这也是 Spring Boot WebFlux 启动器启用函数式端点的方式。

以下示例展示了一个 WebFlux Java 配置(有关如何运行它,请参阅 DispatcherHandler):

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        //...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        //...
    }

    //...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // 配置消息转换...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 配置 CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // 配置 HTML 渲染的视图解析...
    }
}

# 41.6、过滤处理函数

可以使用路由函数构建器上的 before、after 或 filter 方法来过滤处理函数。使用注解时,可以通过 @ControllerAdvice、ServletFilter 或两者来实现类似的功能。过滤器将应用于构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不会应用于 "顶级" 路由。例如,考虑以下示例:

RouterFunction<ServerResponse> route = route()
   .path("/person", b1 -> b1
       .nest(accept(APPLICATION_JSON), b2 -> b2
           .GET("/{id}", handler::getPerson)
           .GET(handler::listPeople)
           .before(request -> ServerRequest.from(request) 
               .header("X-RequestHeader", "Value")
               .build()))
       .POST(handler::createPerson))
    // (2) 记录响应的 after 过滤器应用于所有路由,包括嵌套路由
   .after((request, response) -> logResponse(response)) 
   .build();

路由构建器上的 filter 方法接受一个 HandlerFilterFunction:这是一个接收 ServerRequest 和 HandlerFunction 并返回 ServerResponse 的函数。处理函数参数表示链中的下一个元素。这通常是被路由到的处理函数,但如果应用了多个过滤器,它也可以是另一个过滤器。

现在可以向路由添加一个简单的安全过滤器,假设我们有一个 SecurityManager 可以确定是否允许访问特定路径。以下示例展示了如何实现:

SecurityManager securityManager =...;

RouterFunction<ServerResponse> route = route()
   .path("/person", b1 -> b1
       .nest(accept(APPLICATION_JSON), b2 -> b2
           .GET("/{id}", handler::getPerson)
           .GET(handler::listPeople))
       .POST(handler::createPerson))
   .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        } else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
   .build();

上述示例表明,调用 next.handle(ServerRequest) 是可选的。只有当允许访问时,才会让处理函数运行。

除了在路由函数构建器上使用 filter 方法外,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用于现有的路由函数。

函数式端点的 CORS 支持通过专门的 CorsWebFilter 提供。

# 42、链接

本节描述了 Spring 框架中准备 URI 的各种选项。

# 42.1、UriComponents

适用框架:Spring MVC 和 Spring WebFlux

UriComponentsBuilder 有助于使用带有变量的 URI 模板构建 URI,如下例所示:

UriComponents uriComponents = UriComponentsBuilder
       .fromUriString("https://example.com/hotels/{hotel}") // (1) Static factory method with a URI template.
       .queryParam("q", "{q}") // (2) Add or replace URI components.
       .encode() // (3) Request to have the URI template and URI variables encoded.
       .build(); // (4) Build a `UriComponents`.

URI uri = uriComponents.expand("Westin", "123").toUri(); // (5) Expand variables and obtain the `URI`.
  1. 使用 URI 模板的静态工厂方法。
  2. 添加或替换 URI 组件。
  3. 请求对 URI 模板和 URI 变量进行编码。
  4. 构建 UriComponents。
  5. 展开变量并获取 URI。

上述示例可以使用 buildAndExpand 合并为一个链式调用并简化,如下例所示:

URI uri = UriComponentsBuilder
       .fromUriString("https://example.com/hotels/{hotel}")
       .queryParam("q", "{q}")
       .encode()
       .buildAndExpand("Westin", "123")
       .toUri();

您可以直接生成 URI(这意味着会进行编码)来进一步简化,如下例所示:

URI uri = UriComponentsBuilder
       .fromUriString("https://example.com/hotels/{hotel}")
       .queryParam("q", "{q}")
       .build("Westin", "123");

使用完整的 URI 模板还可以进一步简化,如下例所示:

URI uri = UriComponentsBuilder
       .fromUriString("https://example.com/hotels/{hotel}?q={q}")
       .build("Westin", "123");

# 42.2、UriBuilder

适用框架:Spring MVC 和 Spring WebFlux

UriComponentsBuilder 实现了 UriBuilder。您可以使用 UriBuilderFactory 创建 UriBuilder。UriBuilderFactory 和 UriBuilder 一起提供了一种可插拔机制,用于基于共享配置(如基础 URL、编码偏好和其他细节)从 URI 模板构建 URI。

您可以使用 UriBuilderFactory 配置 RestTemplate 和 WebClient 以自定义 URI 的准备过程。DefaultUriBuilderFactory 是 UriBuilderFactory 的默认实现,它内部使用 UriComponentsBuilder 并公开共享配置选项。

以下示例展示了如何配置 RestTemplate:

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

以下示例配置了 WebClient:

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

此外,您也可以直接使用 DefaultUriBuilderFactory。它与使用 UriComponentsBuilder 类似,但它是一个实际的实例,用于保存配置和偏好,如下例所示:

String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
       .queryParam("q", "{q}")
       .build("Westin", "123");

# 42.3、解析

适用框架:Spring MVC 和 Spring WebFlux

UriComponentsBuilder 支持两种 URI 解析器类型:

  1. RFC 解析器:这种解析器类型期望 URI 字符串符合 RFC 3986 语法,并将不符合语法的情况视为非法。
  2. WhatWG 解析器:此解析器基于 WhatWG URL 现行标准 (opens new window) 中的 URL 解析算法 (opens new window)。它可以对各种意外输入进行宽松处理。浏览器实现此功能是为了宽松处理用户输入的 URL。有关更多详细信息,请参阅 URL 现行标准和 URL 解析测试用例 (opens new window)。

默认情况下,RestClient、WebClient 和 RestTemplate 使用 RFC 解析器类型,并期望应用程序提供符合 RFC 语法的 URL 模板。要更改此设置,您可以自定义任何客户端上的 UriBuilderFactory。

应用程序和框架可以进一步依靠 UriComponentsBuilder 来解析用户提供的 URL,以便检查并可能验证 URI 组件,如 scheme、主机、端口、路径和查询。这些组件可以选择使用 WhatWG 解析器类型,以便更宽松地处理 URL,并在重定向到输入 URL 或将其包含在对浏览器的响应中时,与浏览器解析 URI 的方式保持一致。

# 42.4、编码

适用框架:Spring MVC 和 Spring WebFlux

UriComponentsBuilder 在两个级别上提供编码选项:

  • UriComponentsBuilder#encode() (opens new window): 先对 URI 模板进行预编码,然后在展开时严格对 URI 变量进行编码。
  • UriComponents#encode() (opens new window): 在 URI 变量展开后对 URI 组件进行编码。

这两种选项都会用转义八进制替换非 ASCII 和非法字符。然而,第一种选项还会替换 URI 变量中具有保留含义的字符。

提示:考虑 ;,它在路径中是合法的,但具有保留含义。第一种选项会在 URI 变量中将 ; 替换为 %3B,但不会在 URI 模板中替换。相比之下,第二种选项永远不会替换 ;,因为它在路径中是合法字符。

在大多数情况下,第一种选项可能会得到预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而第二种选项在 URI 变量有意包含保留字符时很有用。当根本不展开 URI 变量时,第二种选项也很有用,因为它也会对任何看起来像是 URI 变量的内容进行编码。

以下示例使用第一种选项:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
       .queryParam("q", "{q}")
       .encode()
       .buildAndExpand("New York", "foo+bar")
       .toUri();

// 结果是 "/hotel%20list/New%20York?q=foo%2Bbar"

您可以直接生成 URI 来简化上述示例(这意味着会进行编码),如下例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
       .queryParam("q", "{q}")
       .build("New York", "foo+bar");

使用完整的 URI 模板还可以进一步简化,如下例所示:

URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
       .build("New York", "foo+bar");

WebClient 和 RestTemplate 通过 UriBuilderFactory 策略在内部展开和编码 URI 模板。两者都可以配置自定义策略,如下例所示:

String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// 自定义 RestTemplate
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// 自定义 WebClient
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

DefaultUriBuilderFactory 实现内部使用 UriComponentsBuilder 来展开和编码 URI 模板。作为一个工厂,它基于以下编码模式之一,提供了一个统一的配置编码方式的地方:

  • TEMPLATE_AND_VALUES: 使用 UriComponentsBuilder#encode()(对应前面列表中的第一个选项)对 URI 模板进行预编码,并在展开时严格对 URI 变量进行编码。
  • VALUES_ONLY: 不对 URI 模板进行编码,而是在将 URI 变量展开到模板之前,通过 UriUtils#encodeUriVariables 对其应用严格的编码。
  • URI_COMPONENT: 使用 UriComponents#encode()(对应前面列表中的第二个选项)在 URI 变量展开后对 URI 组件值进行编码。
  • NONE: 不应用任何编码。

由于历史原因和向后兼容性,RestTemplate 被设置为 EncodingMode.URI_COMPONENT。WebClient 依赖于 DefaultUriBuilderFactory 中的默认值,该值在 5.0.x 版本中为 EncodingMode.URI_COMPONENT,在 5.1 版本中改为 EncodingMode.TEMPLATE_AND_VALUES。

# 43、CORS

Spring WebFlux 允许你处理 CORS(跨域资源共享)。本节将介绍具体的处理方式。

# 43.1、简介

出于安全考虑,浏览器禁止 AJAX 调用当前来源之外的资源。例如,你可能在一个标签页中打开了银行账户页面,而在另一个标签页中打开了恶意网站 evil.com。来自 evil.com 的脚本不应能够使用你的凭据向银行 API 发起 AJAX 请求,比如从你的账户中取款!

跨域资源共享(CORS)是一项 W3C 规范 (opens new window),大多数浏览器 (opens new window) 都实现了该规范,它允许你指定哪些类型的跨域请求是被授权的,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的变通方法。

# 43.2、处理过程

CORS 规范将请求分为预检请求、简单请求和实际请求。若想了解 CORS 的工作原理,你可以阅读 这篇文章 (opens new window) 及其他众多相关文章,或者查看规范以获取更多详细信息。

Spring WebFlux 的 HandlerMapping 实现提供了对 CORS 的内置支持。在将请求成功映射到处理程序后,HandlerMapping 会检查给定请求和处理程序的 CORS 配置,并采取进一步的操作。预检请求会被直接处理,而简单请求和实际 CORS 请求会被拦截、验证,并设置所需的 CORS 响应头。

为了启用跨域请求(即请求中包含 Origin 头且与请求的主机不同),你需要明确声明一些 CORS 配置。如果未找到匹配的 CORS 配置,预检请求将被拒绝。简单请求和实际 CORS 请求的响应中不会添加 CORS 头,因此浏览器会拒绝这些请求。

每个 HandlerMapping 都可以通过基于 URL 模式的 CorsConfiguration 映射进行 单独配置 (opens new window)。在大多数情况下,应用程序会使用 WebFlux Java 配置来声明这些映射,这会将一个全局映射传递给所有 HandlerMapping 实现。

你可以将 HandlerMapping 级别的全局 CORS 配置与更精细的处理程序级 CORS 配置相结合。例如,带注解的控制器可以使用类级或方法级的 @CrossOrigin 注解(其他处理程序可以实现 CorsConfigurationSource)。

全局配置和局部配置的组合规则通常是累加的,例如,所有全局和所有局部的来源。对于那些只能接受单个值的属性,如 allowCredentials 和 maxAge,局部配置会覆盖全局配置的值。更多详细信息请参阅 CorsConfiguration#combine(CorsConfiguration) (opens new window)。

提示:若想从源代码中了解更多信息或进行高级定制,请参考以下内容:

  • CorsConfiguration
  • CorsProcessor 和 DefaultCorsProcessor
  • AbstractHandlerMapping

# 43.3、带凭证的请求

在带凭证的请求中使用 CORS 需要启用 allowedCredentials。需要注意的是,此选项会与配置的域名建立高度信任关系,同时通过暴露诸如 cookie 和 CSRF 令牌等敏感的用户特定信息,增加了 Web 应用程序的攻击面。

启用凭证还会影响对配置的 * CORS 通配符的处理方式:

  • allowOrigins 中不允许使用通配符,但可以使用 allowOriginPatterns 属性来匹配动态的来源集。
  • 当在 allowedHeaders 或 allowedMethods 中设置时,Access-Control-Allow-Headers 和 Access-Control-Allow-Methods 响应头会通过复制 CORS 预检请求中指定的相关头和方法来处理。
  • 当在 exposedHeaders 中设置时,Access-Control-Expose-Headers 响应头将设置为配置的头列表或通配符。虽然 CORS 规范在 Access-Control-Allow-Credentials 设置为 true 时不允许使用通配符,但大多数浏览器支持它,并且在 CORS 处理期间并非所有响应头都可用,因此,无论 allowCredentials 属性的值如何,指定通配符时都会将其用作头值。

警告:虽然这种通配符配置很方便,但建议尽可能配置有限的值集,以提供更高的安全性。

# 43.4、@CrossOrigin

@CrossOrigin (opens new window) 注解可在带注解的控制器方法上启用跨域请求,如下例所示:

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        //...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        //...
    }
}

默认情况下,@CrossOrigin 允许以下内容:

  • 所有来源。
  • 所有头。
  • 控制器方法映射到的所有 HTTP 方法。

allowCredentials 默认未启用,因为这会建立一个信任级别,暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),应仅在适当的情况下使用。启用此选项时,allowOrigins 必须设置为一个或多个特定的域名(不能是特殊值 *),或者可以使用 allowOriginPatterns 属性来匹配动态的来源集。

maxAge 设置为 30 分钟。

@CrossOrigin 也支持在类级别使用,并由所有方法继承。以下示例指定了特定的域名,并将 maxAge 设置为 1 小时:

@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        //...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        //...
    }
}

你可以在类级别和方法级别同时使用 @CrossOrigin,如下例所示:

@CrossOrigin(maxAge = 3600) // (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com") // (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        //...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        //...
    }
}
  1. 在类级别使用 @CrossOrigin。
  2. 在方法级别使用 @CrossOrigin。

# 43.5、全局配置

除了精细的控制器方法级配置外,你可能还需要定义一些全局的 CORS 配置。你可以在任何 HandlerMapping 上单独设置基于 URL 的 CorsConfiguration 映射。然而,大多数应用程序会使用 WebFlux Java 配置来实现这一点。

默认情况下,全局配置启用以下内容:

  • 所有来源。
  • 所有头。
  • GET、HEAD 和 POST 方法。

allowedCredentials 默认未启用,因为这会建立一个信任级别,暴露敏感的用户特定信息(如 cookie 和 CSRF 令牌),应仅在适当的情况下使用。启用此选项时,allowOrigins 必须设置为一个或多个特定的域名(不能是特殊值 *),或者可以使用 allowOriginPatterns 属性来匹配动态的来源集。

maxAge 设置为 30 分钟。

要在 WebFlux Java 配置中启用 CORS,你可以使用 CorsRegistry 回调,如下例所示:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
           .allowedOrigins("https://domain2.com")
           .allowedMethods("PUT", "DELETE")
           .allowedHeaders("header1", "header2", "header3")
           .exposedHeaders("header1", "header2")
           .allowCredentials(true).maxAge(3600);

        // 添加更多映射...
    }
}

# 43.6、WebFilter

你可以通过内置的 CorsWebFilter (opens new window) 来应用 CORS 支持,它非常适合 函数式端点。

注意:如果你尝试将 CorsFilter 与 Spring Security 一起使用,请记住 Spring Security 对 CORS 有 内置支持 (opens new window)。

要配置此过滤器,你可以声明一个 CorsWebFilter bean,并将 CorsConfigurationSource 传递给其构造函数,如下例所示:

@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // 可能的操作...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}

# 44、错误响应

REST 服务的一个常见需求是在错误响应的正文中包含详细信息。Spring 框架支持 “HTTP API 问题详情” 规范,即 RFC 9457 (opens new window)。

以下是该支持的主要抽象概念:

  • ProblemDetail:用于表示 RFC 9457 问题详情,是一个简单的容器,可用于存放规范中定义的标准字段和非标准字段。
  • ErrorResponse:这是一个合同(契约),用于暴露 HTTP 错误响应的详细信息,包括 HTTP 状态、响应头以及 RFC 9457 格式的响应体。它允许异常封装并暴露自身如何映射到 HTTP 响应的详细信息,所有 Spring WebFlux 异常都实现了该接口。
  • ErrorResponseException:这是一个基本的 ErrorResponse 实现,其他类可以方便地将其作为基类使用。
  • ResponseEntityExceptionHandler:这是一个便捷的基类,适用于 @ControllerAdvice。它可以处理所有 Spring WebFlux 异常和任何 ErrorResponseException,并渲染带有响应体的错误响应。

# 44.1、渲染

你可以从任何 @ExceptionHandler 或 @RequestMapping 方法中返回 ProblemDetail 或 ErrorResponse,以渲染一个符合 RFC 9457 的响应。具体处理过程如下:

  • ProblemDetail 的 status 属性决定 HTTP 状态。
  • 如果 ProblemDetail 的 instance 属性尚未设置,则会从当前 URL 路径中获取并设置。
  • 对于内容协商,当渲染 ProblemDetail 时,Jackson 的 HttpMessageConverter 优先选择 "application/problem+json" 而非 "application/json"。如果没有找到兼容的媒体类型,也会回退到使用它。

要为 Spring WebFlux 异常和任何 ErrorResponseException 启用 RFC 9457 响应,可以扩展 ResponseEntityExceptionHandler 并在 Spring 配置中将其声明为 @ControllerAdvice。该处理器有一个 @ExceptionHandler 方法,用于处理任何 ErrorResponse 异常,其中包括所有内置的 Web 异常。你还可以添加更多的异常处理方法,并使用受保护的方法将任何异常映射到 ProblemDetail。

你可以通过 WebFlux 配置 使用 WebFluxConfigurer 注册 ErrorResponse 拦截器。利用它拦截任何 RFC 9457 响应并执行相应操作。

# 44.2、非标准字段

你可以通过以下两种方式之一为 RFC 9457 响应添加非标准字段:

方法一,将非标准字段插入 ProblemDetail 的 "properties" Map 中。使用 Jackson 库时,Spring 框架会注册 ProblemDetailJacksonMixin,确保该 "properties" Map 被展开,并作为顶级 JSON 属性在响应中渲染。同样,在反序列化时,任何未知属性都会插入到这个 Map 中。

你也可以扩展 ProblemDetail 以添加专用的非标准属性。ProblemDetail 中的拷贝构造函数使子类能够轻松地从现有的 ProblemDetail 创建。例如,可以在 ResponseEntityExceptionHandler 这样的 @ControllerAdvice 中集中处理,将异常的 ProblemDetail 重新创建为包含额外非标准字段的子类。

# 44.3、自定义和国际化

自定义和国际化错误响应详情是常见的需求。自定义 Spring WebFlux 异常的问题详情也是一种良好实践,这样可以避免暴露实现细节。本节将介绍相关支持信息。

ErrorResponse 会为 “类型(type)”、“标题(title)” 和 “详情(detail)” 提供消息代码,并为 “详情” 字段提供消息代码参数。ResponseEntityExceptionHandler 通过 MessageSource 解析这些内容,并相应地更新 ProblemDetail 的对应字段。

默认的消息代码策略遵循以下模式:

problemDetail.[type|title|detail].[完全限定的异常类名]

一个 ErrorResponse 可能会暴露多个消息代码,通常是在默认消息代码后面添加后缀。下表列出了 Spring WebFlux 异常的消息代码和参数:

异常 消息代码 消息代码参数
HandlerMethodValidationException (默认) {0} 列出所有验证错误。每个错误的消息代码和参数也会通过 MessageSource 解析。
MethodNotAllowedException (默认) {0} 当前的 HTTP 方法,{1} 支持的 HTTP 方法列表
MissingRequestValueException (默认) {0} 值的标签(例如,“请求头”、“cookie 值” 等),{1} 值的名称
NotAcceptableStatusException (默认) {0} 支持的媒体类型列表
NotAcceptableStatusException (默认) + ".parseError"
ServerErrorException (默认) {0} 传递给类构造函数的失败原因
UnsupportedMediaTypeStatusException (默认) {0} 不支持的媒体类型,{1} 支持的媒体类型列表
UnsupportedMediaTypeStatusException (默认) + ".parseError"
UnsatisfiedRequestParameterException (默认) {0} 参数条件列表
WebExchangeBindException (默认) {0} 全局错误列表,{1} 字段错误列表。每个错误的消息代码和参数也会通过 MessageSource 解析。

注意:与其他异常不同,WebExchangeBindException 和 HandlerMethodValidationException 的消息参数基于 MessageSourceResolvable 错误列表,这些错误也可以通过 MessageSource 资源包进行自定义。更多详细信息请参阅 自定义验证错误。

# 44.4、客户端处理

客户端应用程序在使用 WebClient 时可以捕获 WebClientResponseException,在使用 RestTemplate 时可以捕获 RestClientResponseException,并使用它们的 getResponseBodyAs 方法将错误响应体解码为任何目标类型,如 ProblemDetail 或 ProblemDetail 的子类。

# 45、Security

请查看 Servlet 栈中的对应内容。

Spring Security (opens new window) 项目为保护 Web 应用程序免受恶意攻击提供支持。你可以查看 Spring Security 参考文档,其中包括:

  • WebFlux 安全 (opens new window)
  • WebFlux 测试支持 (opens new window)
  • CSRF 保护 (opens new window)
  • 安全响应头 (opens new window)

# 46、缓存

请查看 Servlet 栈中的对应内容。

HTTP 缓存可以显著提升 Web 应用程序的性能。HTTP 缓存主要围绕 Cache-Control 响应头以及后续的条件请求头(如 Last-Modified 和 ETag)展开。Cache-Control 用于告知私有缓存(如浏览器)和公共缓存(如代理)如何缓存和复用响应内容。如果内容未发生变化,ETag 头可用于发起条件请求,该请求可能会返回 304(未修改)状态码且不包含响应体。ETag 可以看作是 Last-Modified 头更高级的替代方案。

本节将介绍 Spring WebFlux 中可用的与 HTTP 缓存相关的选项。

# 46.1、CacheControl

请查看 Servlet 栈中的对应内容。

CacheControl (opens new window) 支持配置与 Cache-Control 头相关的设置,并且在以下几个场景中作为参数使用:

  • 控制器
  • 静态资源

虽然 RFC 7234 (opens new window) 描述了 Cache-Control 响应头的所有可能指令,但 CacheControl 类型采用了以用例为导向的方法,专注于常见场景,示例如下:

// 缓存一小时 - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// 禁止缓存 - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// 在公共和私有缓存中缓存十天,公共缓存不应转换响应
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();

# 46.2、控制器

请查看 Servlet 栈中的对应内容。

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为在将资源的 lastModified 或 ETag 值与条件请求头进行比较之前,需要先对其进行计算。控制器可以将 ETag 和 Cache-Control 设置添加到 ResponseEntity 中,示例如下:

@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
           .ok()
           .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
           .eTag(version) // 也可以使用 lastModified
           .body(book);
}

如果与条件请求头的比较表明内容未发生变化,上述示例将发送一个带有空响应体的 304(未修改)响应。否则,ETag 和 Cache-Control 头将被添加到响应中。

你也可以在控制器中对条件请求头进行检查,示例如下:

@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

    long eTag =...; // (1) 应用程序特定的计算

    if (exchange.checkNotModified(eTag)) {
        return null; // (2) 响应已设置为 304(未修改),无需进一步处理
    }

    model.addAttribute(...); // (3) 继续处理请求
    return "myViewName";
}
  1. 应用程序特定的计算。
  2. 响应已设置为 304(未修改),无需进一步处理。
  3. 继续处理请求。

针对 eTag 值、lastModified 值或两者,有三种检查条件请求的变体。对于条件 GET 和 HEAD 请求,你可以将响应设置为 304(未修改)。对于条件 POST、PUT 和 DELETE 请求,你可以将响应设置为 412(预条件失败)以防止并发修改。

# 46.3、静态资源

请查看 Servlet 栈中的对应内容。

为了达到最佳性能,你应该为静态资源提供 Cache-Control 和条件响应头。请参阅 静态资源配置 部分。

# 47、视图技术

Spring WebFlux 中的视图渲染是可插拔的。你决定使用 Thymeleaf、FreeMarker 还是其他视图技术,主要取决于配置的更改。本章将介绍与 Spring WebFlux 集成的视图技术。

如需了解更多视图渲染的背景信息,请参阅视图解析。

警告:Spring WebFlux 应用程序的视图位于应用程序的内部信任边界内。视图可以访问应用程序上下文中的 Bean,因此,我们不建议在模板可由外部源编辑的应用程序中使用 Spring WebFlux 模板支持,因为这可能会带来安全隐患。

# 47.1、Thymeleaf

Thymeleaf 是一款现代的服务器端 Java 模板引擎,它强调使用自然的 HTML 模板,这些模板可以通过双击在浏览器中预览。这对于独立进行 UI 模板工作(例如由设计师进行)非常有帮助,无需运行服务器。Thymeleaf 提供了丰富的功能,并且得到了积极的开发和维护。如需更全面的介绍,请访问 Thymeleaf (opens new window) 项目主页。

Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目负责管理。配置涉及一些 Bean 声明,例如 SpringResourceTemplateResolver、SpringWebFluxTemplateEngine 和 ThymeleafReactiveViewResolver。更多详细信息,请参阅 Thymeleaf+Spring (opens new window) 和 WebFlux 集成 公告 (opens new window)。

# 47.2、FreeMarker

Apache FreeMarker (opens new window) 是一个模板引擎,可用于生成从 HTML 到电子邮件等任何类型的文本输出。Spring 框架内置了支持 Spring WebFlux 使用 FreeMarker 模板的集成。

# a、视图配置

以下示例展示了如何将 FreeMarker 配置为视图技术:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // 配置 FreeMarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
        return configurer;
    }
}

你的模板需要存储在 FreeMarkerConfigurer 指定的目录中,如上述示例所示。根据上述配置,如果你的控制器返回视图名称 welcome,解析器将查找 classpath:/templates/freemarker/welcome.ftl 模板。

# b、配置

你可以通过设置 FreeMarkerConfigurer Bean 上的相应属性,将 FreeMarker 的 “Settings” 和 “SharedVariables” 直接传递给 FreeMarker 的 Configuration 对象(由 Spring 管理)。freemarkerSettings 属性需要一个 java.util.Properties 对象,而 freemarkerVariables 属性需要一个 java.util.Map。以下示例展示了如何使用 FreeMarkerConfigurer:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    //...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("xml_escape", new XmlEscape());

        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        configurer.setFreemarkerVariables(variables);
        return configurer;
    }
}

有关应用于 Configuration 对象的设置和变量的详细信息,请参阅 FreeMarker 文档。

# c、表单处理

Spring 提供了一个用于 JSP 的标签库,其中包含 <spring:bind/> 元素。该元素主要用于让表单显示表单支持对象中的值,并显示来自 Web 或业务层 Validator 的验证失败结果。Spring 在 FreeMarker 中也提供了相同功能的支持,并且还有用于生成表单输入元素本身的便利宏。

# 绑定宏

spring-webflux.jar 文件中为 FreeMarker 维护了一组标准的宏,因此对于配置适当的应用程序来说,这些宏始终可用。

Spring 模板库中定义的一些宏被视为内部(私有)宏,但宏定义中不存在这样的作用域,这使得所有宏对调用代码和用户模板都是可见的。以下部分仅关注你需要在模板中直接调用的宏。如果你想直接查看宏代码,文件名为 spring.ftl,位于 org.springframework.web.reactive.result.view.freemarker 包中。

有关绑定支持的更多详细信息,请参阅 Spring MVC 的 简单绑定。

# 表单宏

有关 Spring 为 FreeMarker 模板提供的表单宏支持的详细信息,请参阅 Spring MVC 文档的以下部分:

  • 输入宏
  • 输入字段
  • 选择字段
  • HTML 转义

# 47.3、脚本视图

Spring 框架内置了支持 Spring WebFlux 与任何可运行在 JSR - 223 (opens new window) Java 脚本引擎之上的模板库的集成。下表展示了我们在不同脚本引擎上测试过的模板库:

脚本库 脚本引擎
Handlebars (opens new window) Nashorn (opens new window)
Mustache (opens new window) Nashorn (opens new window)
React (opens new window) Nashorn (opens new window)
EJS (opens new window) Nashorn (opens new window)
ERB (opens new window) JRuby (opens new window)
字符串模板 (opens new window) Jython (opens new window)
Kotlin 脚本模板 (opens new window) Kotlin (opens new window)

提示:集成任何其他脚本引擎的基本规则是它必须实现 ScriptEngine 和 Invocable 接口。

# a、要求

你需要在类路径中包含脚本引擎,具体细节因脚本引擎而异:

  • Nashorn (opens new window) JavaScript 引擎随 Java 8 及更高版本提供。强烈建议使用最新的更新版本。
  • 若要支持 Ruby,应将 JRuby (opens new window) 作为依赖添加。
  • 若要支持 Python,应将 Jython (opens new window) 作为依赖添加。
  • 若要支持 Kotlin 脚本,应添加 org.jetbrains.kotlin:kotlin-script-util 依赖,并在 META - INF/services/javax.script.ScriptEngineFactory 文件中添加 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 这一行。更多详细信息请参阅此示例 (opens new window)。

你还需要有脚本模板库。对于 JavaScript,一种方法是通过 WebJars (opens new window) 来实现。

# b、脚本模板

你可以声明一个 ScriptTemplateConfigurer Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用哪个函数来渲染模板等等。以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}

调用 render 函数时会传入以下参数:

  • String template:模板内容
  • Map model:视图模型
  • RenderingContext renderingContext:RenderingContext (opens new window),可访问应用程序上下文、区域设置、模板加载器和 URL(从 5.0 版本开始)

Mustache.render() 与这个签名原生兼容,因此你可以直接调用它。

如果你的模板技术需要一些自定义设置,你可以提供一个实现自定义渲染函数的脚本。例如,Handlebars (opens new window) 在使用模板之前需要对其进行编译,并且需要一个 polyfill (opens new window) 来模拟服务器端脚本引擎中不可用的一些浏览器功能。以下示例展示了如何设置自定义渲染函数:

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}

注意:当使用非线程安全的脚本引擎和未针对并发设计的模板库(如在 Nashorn 上运行的 Handlebars 或 React)时,必须将 sharedEngine 属性设置为 false。在这种情况下,由于[此 bug](https://bugs.openjdk.java.net/browse/JDK - 8076099),需要 Java SE 8 update 60,但无论如何,通常建议使用最新的 Java SE 补丁版本。

polyfill.js 仅定义了 Handlebars 正常运行所需的 window 对象,如下所示:

var window = {};

以下基本的 render.js 实现会在使用模板之前对其进行编译。一个生产级的实现还应该存储和复用缓存的模板或预编译的模板。这可以在脚本端完成,以及进行任何你需要的自定义操作(例如管理模板引擎配置)。以下示例展示了如何编译模板:

function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}

有关更多配置示例,请查看 Spring 框架的单元测试,Java (opens new window) 和 资源 (opens new window)。

# 47.4、片段

HTMX (opens new window) 和 Hotwire Turbo (opens new window) 强调基于 HTML 进行数据传输的方法,客户端接收的是 HTML 格式的服务器更新,而不是 JSON。这使得单页应用(SPA)无需编写大量甚至任何 JavaScript 就能实现相应功能。如需详细了解,请访问它们各自的网站。

在 Spring WebFlux 中,视图渲染通常涉及指定一个视图和一个模型。然而,在基于 HTML 进行数据传输的场景中,一个常见的功能是发送多个 HTML 片段,浏览器可以使用这些片段更新页面的不同部分。为此,控制器方法可以返回 Collection<Fragment>。例如:

@GetMapping
List<Fragment> handle() {
    return List.of(Fragment.create("posts"), Fragment.create("comments"));
}

也可以通过返回专用类型 FragmentsRendering 来实现相同的功能:

@GetMapping
FragmentsRendering handle() {
    return FragmentsRendering.with("posts").fragment("comments").build();
}

每个片段可以有独立的模型,并且该模型会继承请求的共享模型中的属性。

HTMX 和 Hotwire Turbo 支持通过 SSE(服务器发送事件)进行流式更新。控制器可以使用 Flux<Fragment>、或者通过 ReactiveAdapterRegistry 适配为 Reactive Streams Publisher 的任何其他响应式生产者来创建 FragmentsRendering。也可以直接返回 Flux<Fragment>,而不使用 FragmentsRendering 包装器。

# 47.5、和 XML

出于内容协商的目的,根据客户端请求的内容类型,在使用 HTML 模板渲染模型或渲染为其他格式(如 JSON 或 XML)之间进行切换是很有用的。为了支持这一点,Spring WebFlux 提供了 HttpMessageWriterView,你可以使用它来插入 spring - web 中任何可用的 编解码器,如 Jackson2JsonEncoder、Jackson2SmileEncoder 或 Jaxb2XmlEncoder。

与其他视图技术不同,HttpMessageWriterView 不需要 ViewResolver,而是作为默认视图进行配置。你可以配置一个或多个这样的默认视图,包装不同的 HttpMessageWriter 实例或 Encoder 实例。运行时将使用与请求的内容类型匹配的视图。

在大多数情况下,模型包含多个属性。为了确定要序列化哪个属性,你可以配置 HttpMessageWriterView,指定用于渲染的模型属性名称。如果模型中只有一个属性,则使用该属性。

# 48、配置

WebFlux 的 Java 配置声明了使用带注解的控制器或功能端点处理请求所需的组件,并提供了一个 API 来定制配置。这意味着你无需了解 Java 配置创建的底层 Bean。不过,如果你想了解它们,可以在 WebFluxConfigurationSupport 中查看,或者在特殊 Bean 类型中了解更多相关信息。

对于配置 API 中没有提供的更高级的定制,你可以通过高级配置模式来完全控制配置。

# 48.1、启用 WebFlux 配置

你可以在 Java 配置中使用 @EnableWebFlux 注解,如下例所示:

@Configuration
@EnableWebFlux
public class WebConfig {
}

注意:使用 Spring Boot 时,你可以使用 WebFluxConfigurer 类型的 @Configuration 类,但不使用 @EnableWebFlux,以保留 Spring Boot WebFlux 的定制。更多详细信息请参阅 WebFlux 配置 API 部分 和 Spring Boot 文档 (opens new window)。

上述示例注册了一些 Spring WebFlux 基础设施 Bean,并根据类路径上可用的依赖项进行适配,如 JSON、XML 等。

# 48.2、配置 API

在 Java 配置中,你可以实现 WebFluxConfigurer 接口,如下例所示:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    // 实现配置方法...
}

# 48.3、转换与格式化

默认情况下,会安装各种数字和日期类型的格式化器,并支持通过 @NumberFormat、@DurationFormat 和 @DateTimeFormat 对字段和参数进行定制。

要在 Java 配置中注册自定义格式化器和转换器,请使用以下代码:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        //...
    }

}

默认情况下,Spring WebFlux 在解析和格式化日期值时会考虑请求的 Locale。这适用于日期以字符串形式通过“input”表单字段表示的表单。不过,对于“date”和“time”表单字段,浏览器会使用 HTML 规范中定义的固定格式。对于这种情况,可以按如下方式定制日期和时间格式化:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

注意:有关何时使用 FormatterRegistrar 实现的更多信息,请参阅 FormatterRegistrar SPI 和 FormattingConversionServiceFactoryBean。

# 48.4、验证

默认情况下,如果类路径上存在Bean 验证(例如 Hibernate Validator),LocalValidatorFactoryBean 会被注册为全局验证器,用于 @Controller 方法参数上的 @Valid 和 @Validated。

在 Java 配置中,你可以自定义全局 Validator 实例,如下例所示:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator() {
        //...
    }

}

请注意,你也可以在本地注册 Validator 实现,如下例所示:

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

提示:如果你需要在某个地方注入 LocalValidatorFactoryBean,可以创建一个 Bean 并使用 @Primary 进行标记,以避免与 MVC 配置中声明的 Bean 发生冲突。

# 48.5、内容类型解析器

你可以配置 Spring WebFlux 如何从请求中确定 @Controller 实例的请求媒体类型。默认情况下,只会检查 Accept 标头,但你也可以启用基于查询参数的策略。

以下示例展示了如何自定义请求内容类型的解析:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        //...
    }
}

# 48.6、消息编解码器

以下示例展示了如何自定义请求和响应体的读写方式:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024);
    }
}

ServerCodecConfigurer 提供了一组默认的读取器和写入器。你可以使用它来添加更多的读取器和写入器、自定义默认的读取器和写入器,或者完全替换默认的读取器和写入器。

对于 Jackson JSON 和 XML,考虑使用 Jackson2ObjectMapperBuilder (opens new window),它使用以下属性自定义 Jackson 的默认属性:

  • DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES (opens new window):禁用。
  • MapperFeature.DEFAULT_VIEW_INCLUSION (opens new window):禁用。

如果在类路径中检测到以下知名模块,它还会自动注册:

  • jackson-datatype-jsr310 (opens new window):支持 Java 8 日期和时间 API 类型。
  • jackson-datatype-jdk8 (opens new window):支持其他 Java 8 类型,如 Optional。
  • jackson-module-kotlin (opens new window):支持 Kotlin 类和数据类。

# 48.7、视图解析器

以下示例展示了如何配置视图解析:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        //...
    }
}

ViewResolverRegistry 为 Spring 框架集成的视图技术提供了快捷方式。以下示例使用 FreeMarker(这还需要配置底层的 FreeMarker 视图技术):

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // 配置 Freemarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}

你也可以插入任何 ViewResolver 实现,如下例所示:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        ViewResolver resolver =... ;
        registry.viewResolver(resolver);
    }
}

为了支持内容协商并通过视图解析渲染其他格式(除了 HTML),你可以根据 HttpMessageWriterView 实现配置一个或多个默认视图,该实现接受 spring-web 中任何可用的编解码器。以下示例展示了如何实现:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
        registry.defaultViews(new HttpMessageWriterView(encoder));
    }

    //...
}

有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术。

# 48.8、静态资源

此选项提供了一种方便的方式,可从基于 Resource (opens new window) 的位置列表中提供静态资源。

在下例中,对于以 /resources 开头的请求,将使用相对路径在类路径上的 /static 中查找并提供静态资源。为确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求,资源将被设置为一年后过期。还会评估 Last-Modified 标头,如果存在,则返回 304 状态码。示例如下:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
               .addResourceLocations("/public", "classpath:/static/")
               .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

}

另请参阅静态资源的 HTTP 缓存支持。

资源处理器还支持一系列 ResourceResolver (opens new window) 实现和 ResourceTransformer (opens new window) 实现,可用于创建处理优化资源的工具链。

你可以使用 VersionResourceResolver 基于从内容计算得出的 MD5 哈希、固定的应用程序版本或其他信息为资源 URL 提供版本。ContentVersionStrategy(MD5 哈希)是个不错的选择,但也有一些明显的例外情况(例如与模块加载器一起使用的 JavaScript 资源)。

以下示例展示了如何在 Java 配置中使用 VersionResourceResolver:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
               .addResourceLocations("/public/")
               .resourceChain(true)
               .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}

你可以使用 ResourceUrlProvider 来重写 URL 并应用完整的解析器和转换器链(例如插入版本)。WebFlux 配置提供了一个 ResourceUrlProvider,以便可以将其注入到其他组件中。

与 Spring MVC 不同,目前在 WebFlux 中,没有办法透明地重写静态资源 URL,因为没有视图技术可以利用非阻塞的解析器和转换器链。在仅提供本地资源时,解决办法是直接使用 ResourceUrlProvider(例如通过自定义元素)并阻塞。

请注意,当同时使用 EncodedResourceResolver(例如 Gzip、Brotli 编码)和 VersionedResourceResolver 时,必须按此顺序注册它们,以确保始终根据未编码的文件可靠地计算基于内容的版本。

对于 WebJars (opens new window),像 /webjars/jquery/1.2.0/jquery.min.js 这样的带版本的 URL 是推荐且最有效的使用方式。相关的资源位置在 Spring Boot 中是默认配置的(也可以通过 ResourceHandlerRegistry 手动配置),并且不需要添加 org.webjars:webjars-locator-core 依赖项。

像 /webjars/jquery/jquery.min.js 这样的无版本 URL 可以通过 WebJarsResourceResolver 得到支持,当类路径上存在 org.webjars:webjars-locator-core 库时,该解析器会自动注册,但代价是需要进行类路径扫描,这可能会减慢应用程序的启动速度。该解析器可以重写 URL 以包含 jar 的版本,还可以匹配没有版本的传入 URL,例如从 /webjars/jquery/jquery.min.js 到 /webjars/jquery/1.2.0/jquery.min.js。

提示:基于 ResourceHandlerRegistry 的 Java 配置提供了更多细粒度控制的选项,例如最后修改时间行为和优化的资源解析。

# 48.9、路径匹配

你可以自定义与路径匹配相关的选项。有关各个选项的详细信息,请参阅 PathMatchConfigurer (opens new window) 的 Javadoc。以下示例展示了如何使用 PathMatchConfigurer:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.config.PathMatchConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatching(PathMatchConfigurer configurer) {
        configurer.addPathPrefix(
                "/api", HandlerTypePredicate.forAnnotation(RestController.class));
    }
}

提示:

  • Spring WebFlux 依赖于请求路径的解析表示形式 RequestPath 来访问解码后的路径段值,并去除分号内容(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,你无需指定是否对请求路径进行解码,也无需指定是否为路径匹配目的去除分号内容。
  • Spring WebFlux 也不支持后缀模式匹配,而在 Spring MVC 中,我们也建议不再依赖它。

# 48.10、阻塞执行

WebFlux 的 Java 配置允许你在 WebFlux 中自定义阻塞执行。

你可以通过提供一个 AsyncTaskExecutor(如 VirtualThreadTaskExecutor (opens new window)),让阻塞的控制器方法在单独的线程上调用,如下所示:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
        AsyncTaskExecutor executor =...
        configurer.setExecutor(executor);
    }
}

默认情况下,返回类型未被配置的 ReactiveAdapterRegistry 识别的控制器方法被视为阻塞方法,但你可以通过 BlockingExecutionConfigurer 设置自定义的控制器方法谓词。

# 48.11、WebSocketService

WebFlux 的 Java 配置声明了一个 WebSocketHandlerAdapter Bean,它为调用 WebSocket 处理程序提供支持。这意味着,要处理 WebSocket 握手请求,剩下要做的就是通过 SimpleUrlHandlerMapping 将一个 WebSocketHandler 映射到一个 URL。

在某些情况下,可能需要使用提供的 WebSocketService 创建 WebSocketHandlerAdapter Bean,以便配置 WebSocket 服务器属性。例如:

@Configuration
public class WebConfig implements WebFluxConfigurer {

    @Override
    public WebSocketService getWebSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}

# 48.12、高级配置模式

@EnableWebFlux 导入了 DelegatingWebFluxConfiguration,它具有以下功能:

  • 为 WebFlux 应用程序提供默认的 Spring 配置。
  • 检测并委托给 WebFluxConfigurer 实现以自定义该配置。

对于高级模式,你可以移除 @EnableWebFlux,并直接继承 DelegatingWebFluxConfiguration,而不是实现 WebFluxConfigurer,如下例所示:

@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    //...
}

你可以保留 WebConfig 中现有的方法,但现在也可以重写基类中的 Bean 声明,并且类路径上仍然可以有任意数量的其他 WebMvcConfigurer 实现。

# 49、HTTP/2

Reactor Netty、Tomcat、Jetty 和 Undertow 均支持 HTTP/2。不过,服务器配置方面存在一些需要考虑的因素。有关更多详细信息,请参阅 HTTP/2 维基页面 (opens new window)。

# 二、WebClient

Spring WebFlux 包含一个用于执行 HTTP 请求的客户端。WebClient 有一个基于 Reactor 的函数式流式 API,请参阅响应式库,它支持异步逻辑的声明式组合,而无需处理线程或并发问题。它是完全无阻塞的,支持流式传输,并且依赖于与服务器端对请求和响应内容进行编码和解码相同的编解码器。

WebClient 需要一个 HTTP 客户端库来执行请求。它对以下几种库提供了内置支持:

  • Reactor Netty (opens new window)
  • JDK HttpClient (opens new window)
  • Jetty Reactive HttpClient (opens new window)
  • Apache HttpComponents (opens new window)
  • 其他库可以通过 ClientHttpConnector 进行集成。

# 1、章节总结

  • 配置
  • retrieve()
  • 交换
  • 请求体
  • 过滤器
  • 属性
  • 上下文
  • 同步使用
  • 测试

# 2、配置

创建 WebClient 最简单的方法是使用其中一个静态工厂方法:

  • WebClient.create()
  • WebClient.create(String baseUrl)

你也可以使用 WebClient.builder() 搭配更多选项:

  • uriBuilderFactory:自定义的 UriBuilderFactory,用作基础 URL。
  • defaultUriVariables:展开 URI 模板时使用的默认值。
  • defaultHeader:每个请求的头部信息。
  • defaultCookie:每个请求的 Cookie 信息。
  • defaultRequest:用于自定义每个请求的 Consumer。
  • filter:每个请求的客户端过滤器。
  • exchangeStrategies:HTTP 消息读取器/写入器的自定义配置。
  • clientConnector:HTTP 客户端库的设置。
  • observationRegistry:用于启用可观测性支持的注册表。
  • observationConvention:用于为记录的观测提取元数据的可选自定义约定。

例如:

WebClient client = WebClient.builder()
   .codecs(configurer ->... )
   .build();

一旦构建完成,WebClient 就是不可变的。不过,你可以克隆它并构建一个修改后的副本,如下所示:

WebClient client1 = WebClient.builder()
   .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
   .filter(filterC).filter(filterD).build();

// client1 包含过滤器 filterA 和 filterB
// client2 包含过滤器 filterA、filterB、filterC 和 filterD

# 3、最大内存大小(MaxInMemorySize)

编解码器对内存中缓冲数据设置了限制,以避免应用程序出现内存问题。默认情况下,这些限制设置为 256KB。如果这个限制不够,你会收到以下错误:

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

要更改默认编解码器的限制,请使用以下代码:

WebClient webClient = WebClient.builder()
   .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
   .build();

# 4、Netty

要自定义 Reactor Netty 设置,可提供一个预配置的 HttpClient:

HttpClient httpClient = HttpClient.create().secure(sslSpec ->...);

WebClient webClient = WebClient.builder()
   .clientConnector(new ReactorClientHttpConnector(httpClient))
   .build();

# 4.1、资源

默认情况下,HttpClient 会使用 reactor.netty.http.HttpResources 中管理的全局 Reactor Netty 资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源更适合事件循环并发。在这种模式下,全局资源会在进程退出时才会关闭。

如果服务器随进程关闭,通常不需要显式关闭。但是,如果服务器可以在进程内启动或停止(例如,以 WAR 形式部署的 Spring MVC 应用程序),你可以声明一个 ReactorResourceFactory 类型的 Spring 管理的 bean,并将 globalResources 设置为 true(默认值),以确保在 Spring ApplicationContext 关闭时关闭 Reactor Netty 全局资源,如下所示:

@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}

你也可以选择不使用全局 Reactor Netty 资源。不过,在这种模式下,你需要确保所有 Reactor Netty 客户端和服务器实例都使用共享资源,如下所示:

@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false); // 1. 创建独立于全局资源的资源
    return factory;
}

@Bean
public WebClient webClient() {
    Function<HttpClient, HttpClient> mapper = client -> {
        // 进一步的自定义...
    };

    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper); // 2. 使用包含资源工厂的 ReactorClientHttpConnector 构造函数

    return WebClient.builder().clientConnector(connector).build(); // 3. 将连接器插入到 WebClient.Builder 中
}

# 4.2、超时设置

配置连接超时:

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
   .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

WebClient webClient = WebClient.builder()
   .clientConnector(new ReactorClientHttpConnector(httpClient))
   .build();

配置读写超时:

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
   .doOnConnected(conn -> conn
       .addHandlerLast(new ReadTimeoutHandler(10))
       .addHandlerLast(new WriteTimeoutHandler(10)));

// 创建 WebClient...

为所有请求配置响应超时:

HttpClient httpClient = HttpClient.create()
   .responseTimeout(Duration.ofSeconds(2));

// 创建 WebClient...

为特定请求配置响应超时:

WebClient.create().get()
   .uri("https://example.org/path")
   .httpRequest(httpRequest -> {
        HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
        reactorRequest.responseTimeout(Duration.ofSeconds(2));
    })
   .retrieve()
   .bodyToMono(String.class);

# 5、HttpClient

以下示例展示了如何自定义 JDK HttpClient:

HttpClient httpClient = HttpClient.newBuilder()
   .followRedirects(Redirect.NORMAL)
   .connectTimeout(Duration.ofSeconds(20))
   .build();

ClientHttpConnector connector =
        new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());

WebClient webClient = WebClient.builder().clientConnector(connector).build();

# 6、Jetty

以下示例展示了如何自定义 Jetty HttpClient 设置:

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
   .clientConnector(new JettyClientHttpConnector(httpClient))
   .build();

默认情况下,HttpClient 会创建自己的资源(Executor、ByteBufferPool、Scheduler),这些资源会在进程退出或调用 stop() 方法之前一直保持活跃。

你可以在 Jetty 客户端(和服务器)的多个实例之间共享资源,并通过声明一个 JettyResourceFactory 类型的 Spring 管理的 bean,确保在 Spring ApplicationContext 关闭时关闭这些资源,如下所示:

@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {
    HttpClient httpClient = new HttpClient();
    // 进一步的自定义...

    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory()); // 1. 使用包含资源工厂的 JettyClientHttpConnector 构造函数

    return WebClient.builder().clientConnector(connector).build(); // 2. 将连接器插入到 WebClient.Builder 中
}

# 7、HttpComponents

以下示例展示了如何自定义 Apache HttpComponents HttpClient 设置:

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();

ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

WebClient webClient = WebClient.builder().clientConnector(connector).build();

# 8、retrieve()

retrieve() 方法可用于声明如何提取响应。例如:

WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
       .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
       .retrieve()
       .toEntity(Person.class);

或者仅获取响应体:

WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
       .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
       .retrieve()
       .bodyToMono(Person.class);

若要获取解码对象的流,则可以这样做:

Flux<Quote> result = client.get()
       .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
       .retrieve()
       .bodyToFlux(Quote.class);

默认情况下,4xx 或 5xx 的响应会导致抛出 WebClientResponseException,针对特定的 HTTP 状态码还有对应的子类异常。若要自定义错误响应的处理方式,可以使用 onStatus 处理器,示例如下:

Mono<Person> result = client.get()
       .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
       .retrieve()
       .onStatus(HttpStatusCode::is4xxClientError, response ->...)
       .onStatus(HttpStatusCode::is5xxServerError, response ->...)
       .bodyToMono(Person.class);

# 9、交换

exchangeToMono() 和 exchangeToFlux() 方法(在 Kotlin 中是 awaitExchange { } 和 exchangeToFlow { })适用于需要更多控制的更高级场景,例如根据响应状态以不同方式解析响应:

以下是 Java 示例代码:

Mono<Person> entityMono = client.get()
       .uri("/persons/1")
       .accept(MediaType.APPLICATION_JSON)
       .exchangeToMono(response -> {
            if (response.statusCode().equals(HttpStatus.OK)) {
                return response.bodyToMono(Person.class);
            } else {
                // 转换为错误
                return response.createError();
            }
        });

使用上述方法时,在返回的 Mono 或 Flux 操作完成后,会检查响应体。如果响应体未被消费,会释放它以防止内存和连接泄漏。因此,不能在后续操作中进一步解析响应。如果需要解析响应,需要在提供的函数中声明如何解析。

# 10、请求体

请求体可以从 ReactiveAdapterRegistry 处理的任何异步类型进行编码,比如 Mono,如下例所示:

Mono<Person> personMono =... ;

Mono<Void> result = client.post()
       .uri("/persons/{id}", id)
       .contentType(MediaType.APPLICATION_JSON)
       .body(personMono, Person.class)
       .retrieve()
       .bodyToMono(Void.class);

你还可以对对象流进行编码,如下例所示:

Flux<Person> personFlux =... ;

Mono<Void> result = client.post()
       .uri("/persons/{id}", id)
       .contentType(MediaType.APPLICATION_STREAM_JSON)
       .body(personFlux, Person.class)
       .retrieve()
       .bodyToMono(Void.class);

或者,如果你有实际的值,可以使用 bodyValue 快捷方法,如下例所示:

Person person =... ;

Mono<Void> result = client.post()
       .uri("/persons/{id}", id)
       .contentType(MediaType.APPLICATION_JSON)
       .bodyValue(person)
       .retrieve()
       .bodyToMono(Void.class);

# 10.1、表单数据

要发送表单数据,可以提供一个 MultiValueMap<String, String> 作为请求体。请注意,FormHttpMessageWriter 会自动将内容类型设置为 application/x-www-form-urlencoded。以下示例展示了如何使用 MultiValueMap<String, String>:

MultiValueMap<String, String> formData =... ;

Mono<Void> result = client.post()
       .uri("/path", id)
       .bodyValue(formData)
       .retrieve()
       .bodyToMono(Void.class);

你还可以通过 BodyInserters 直接提供表单数据,如下例所示:

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
       .uri("/path", id)
       .body(fromFormData("k1", "v1").with("k2", "v2"))
       .retrieve()
       .bodyToMono(Void.class);

# 10.2、多部分数据

要发送多部分数据,需要提供一个 MultiValueMap<String,?>,其值可以是表示部件内容的 Object 实例,也可以是表示部件内容和头部信息的 HttpEntity 实例。MultipartBodyBuilder 提供了一个方便的 API 来准备多部分请求。以下示例展示了如何创建一个 MultiValueMap<String,?>:

MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // 来自服务器请求的部件

MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,不必为每个部件指定 Content-Type。内容类型会根据用于序列化它的 HttpMessageWriter 自动确定,或者在 Resource 的情况下,根据文件扩展名确定。如有必要,可以通过 part 方法的重载版本为每个部件明确指定要使用的 MediaType。

一旦准备好 MultiValueMap,将其传递给 WebClient 的最简单方法是通过 body 方法,如下例所示:

MultipartBodyBuilder builder =...;

Mono<Void> result = client.post()
       .uri("/path", id)
       .body(builder.build())
       .retrieve()
       .bodyToMono(Void.class);

如果 MultiValueMap 中至少包含一个非 String 值(也可能表示常规表单数据,即 application/x-www-form-urlencoded),则无需将 Content-Type 设置为 multipart/form-data。使用 MultipartBodyBuilder 时总是如此,因为它会确保使用 HttpEntity 进行包装。

作为 MultipartBodyBuilder 的替代方案,还可以通过内置的 BodyInserters 以内联方式提供多部分内容,如下例所示:

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
       .uri("/path", id)
       .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
       .retrieve()
       .bodyToMono(Void.class);
# a、PartEvent

要顺序流式传输多部分数据,可以通过 PartEvent 对象提供多部分内容。

  • 表单字段可以通过 FormPartEvent::create 创建。
  • 文件上传可以通过 FilePartEvent::create 创建。

可以通过 Flux::concat 将这些方法返回的流连接起来,并为 WebClient 创建一个请求。

例如,以下示例将 POST 一个包含表单字段和文件的多部分表单:

Resource resource =...
Mono<String> result = webClient
   .post()
   .uri("https://example.com")
   .body(Flux.concat(
            FormPartEvent.create("field", "field value"),
            FilePartEvent.create("file", resource)
    ), PartEvent.class)
   .retrieve()
   .bodyToMono(String.class);

在服务器端,通过 @RequestBody 或 ServerRequest::bodyToFlux(PartEvent.class) 接收的 PartEvent 对象可以通过 WebClient 转发到另一个服务。

# 11、过滤器

你可以通过 WebClient.Builder 注册一个客户端过滤器(ExchangeFilterFunction),以便拦截和修改请求,如下例所示:

WebClient client = WebClient.builder()
   .filter((request, next) -> {

        ClientRequest filtered = ClientRequest.from(request)
          .header("foo", "bar")
          .build();

        return next.exchange(filtered);
    })
   .build();

这可用于处理跨领域问题,例如身份验证。以下示例通过静态工厂方法使用过滤器进行基本身份验证:

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = WebClient.builder()
   .filter(basicAuthentication("user", "password"))
   .build();

通过改变现有 WebClient 实例,可以添加或移除过滤器,从而生成一个新的 WebClient 实例,且不会影响原来的实例。例如:

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = webClient.mutate()
   .filters(filterList -> {
        filterList.add(0, basicAuthentication("user", "password"));
    })
   .build();

WebClient 是一个围绕过滤器链和 ExchangeFunction 的轻量级外观。它提供了一个工作流来发起请求、在高级对象之间进行编码和解码,并有助于确保响应内容始终被消费。当过滤器以某种方式处理响应时,必须格外注意始终消费其内容,或者将其向下游传播到 WebClient 以确保同样的处理。以下是一个处理 UNAUTHORIZED 状态码的过滤器,同时确保释放任何响应内容(无论是否预期):

public ExchangeFilterFunction renewTokenFilter() {
    return (request, next) -> next.exchange(request).flatMap(response -> {
        if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
            return response.releaseBody()
              .then(renewToken())
              .flatMap(token -> {
                    ClientRequest newRequest = ClientRequest.from(request).build();
                    return next.exchange(newRequest);
                });
        } else {
            return Mono.just(response);
        }
    });
}

下面的示例展示了如何使用 ExchangeFilterFunction 接口创建一个自定义过滤器类,该类通过缓冲来帮助为 PUT 和 POST 的 multipart/form-data 请求计算 Content-Length 头部:

public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {

    @Override
    public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
        if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
            && (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
            return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
                request.body().insert(new BufferingDecorator(outputMessage), context)).build()
            );
        } else {
            return next.exchange(request);
        }
    }

    private static final class BufferingDecorator extends ClientHttpRequestDecorator {

        private BufferingDecorator(ClientHttpRequest delegate) {
            super(delegate);
        }

        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            return DataBufferUtils.join(body).flatMap(buffer -> {
                getHeaders().setContentLength(buffer.readableByteCount());
                return super.writeWith(Mono.just(buffer));
            });
        }
    }
}

# 12、属性

你可以为请求添加属性。如果你想通过过滤器链传递信息,并影响给定请求的过滤器行为,这会很方便。例如:

WebClient client = WebClient.builder()
   .filter((request, next) -> {
        Optional<Object> usr = request.attribute("myAttribute");
        //...
    })
   .build();

client.get().uri("https://example.org/")
   .attribute("myAttribute", "...")
   .retrieve()
   .bodyToMono(Void.class);

请注意,你可以在 WebClient.Builder 级别全局配置一个 defaultRequest 回调,从而将属性插入到所有请求中。在 Spring MVC 应用程序中,这可用于根据 ThreadLocal 数据填充请求属性。

# 13、上下文

属性提供了一种将信息传递给过滤器链的便捷方式,但它们仅影响当前请求。如果你想传递信息,使其传播到嵌套的额外请求(例如,通过flatMap),或者在之后执行的请求(例如,通过concatMap)中,那么你需要使用 Reactor 的上下文(Context)。

为了使 Reactor 的上下文应用于所有操作,需要在响应式链的末尾进行填充。例如:

WebClient client = WebClient.builder()
   .filter((request, next) ->
        Mono.deferContextual(contextView -> {
            String value = contextView.get("foo");
            //...
        }))
   .build();

client.get().uri("https://example.org/")
   .retrieve()
   .bodyToMono(String.class)
   .flatMap(body -> {
        // 执行嵌套请求(上下文会自动传播)...
    })
   .contextWrite(context -> context.put("foo",...));

# 14、同步使用

WebClient 可以通过在最后阻塞以获取结果的方式以同步风格使用:

Person person = client.get().uri("/person/{id}", i).retrieve()
   .bodyToMono(Person.class)
   .block();

List<Person> persons = client.get().uri("/persons").retrieve()
   .bodyToFlux(Person.class)
   .collectList()
   .block();

但是,如果需要进行多次调用,更高效的做法是避免对每个响应单独进行阻塞,而是等待组合后的结果:

Mono<Person> personMono = client.get().uri("/person/{id}", personId)
   .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
   .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
    Map<String, Object> map = new LinkedHashMap<>();
    map.put("person", person);
    map.put("hobbies", hobbies);
    return map;
})
   .block();

以上仅仅是一个示例。还有许多其他模式和操作符可用于构建一个响应式管道,该管道可以进行许多远程调用,可能其中一些是嵌套的、相互依赖的,并且直到最后才会阻塞。

注意: 在 Spring MVC 或 Spring WebFlux 控制器中,使用 Flux 或 Mono 时,你永远不需要进行阻塞。只需从控制器方法返回响应式类型即可。同样的原则也适用于 Kotlin 协程和 Spring WebFlux,只需在控制器方法中使用挂起函数或返回 Flow 即可。

# 15、测试

要测试使用 WebClient 的代码,你可以使用模拟 Web 服务器,例如 OkHttp MockWebServer (opens new window)。若想查看其使用示例,请查阅 Spring 框架测试套件中的 WebClientIntegrationTests (opens new window),或者 OkHttp 代码库中的 static-server (opens new window) 示例。

# 三、HTTP接口客户端

Spring框架允许你将HTTP服务定义为一个包含HTTP交换方法的Java接口。然后,你可以生成一个实现该接口并执行交换的代理。这有助于简化HTTP远程访问,并提供额外的灵活性,让你选择同步或响应式等API风格。

详情请参阅REST端点。

# 四、WebSockets

文档的这一部分介绍了对响应式Web消息传递处理机制的支持方法。

# 1、WebSocket简介

WebSocket协议(RFC 6455 (opens new window))为客户端与服务器之间通过单个TCP连接建立全双工双向通信通道提供了标准化手段。它是一种与HTTP不同的TCP协议,但设计用于在HTTP之上工作,使用80和443端口,并允许复用现有防火墙规则。

WebSocket交互始于一个使用HTTP Upgrade 头的HTTP请求,目的是将协议升级(或者说切换)到WebSocket协议。以下是一个示例:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket # (1): Upgrade头
Connection: Upgrade # (2): 使用Upgrade连接
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

支持WebSocket的服务器不会返回常见的200状态码,而是返回类似以下的输出:

HTTP/1.1 101 Switching Protocols # (1): 协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

成功握手后,用于HTTP升级请求的底层TCP套接字保持打开状态,客户端和服务器可继续通过该通道收发消息。

关于WebSocket工作原理的完整介绍,已超出本文档范围;如有需要,请参阅RFC 6455、HTML5的WebSocket章节,或网络上其他的相关介绍和教程。

需要注意的是,如果WebSocket服务器部署在Web服务器(如Nginx)之后运行,可能需要对Web服务器进行配置,以便将WebSocket升级请求转发到WebSocket服务器。同样,如果应用程序运行在云环境中,请查阅云服务提供商文档,了解有关WebSocket支持的具体配置说明。

# 1.1、HTTP与WebSocket的对比

虽然WebSocket设计为与HTTP兼容,并通过HTTP请求启动,但这两个协议导致的是截然不同架构和应用编程模型。

在HTTP和REST中,应用程序被建模为众多URL。客户端通过请求 - 响应式的方式访问这些URL与应用进行交互,而服务器则基于HTTP URL、方法和头信息将请求路由到适当的处理器。

与此形成鲜明对比的是,WebSocket通常只有一个用于初始连接的URL,随后所有应用消息都通过此同一TCP连接进行传输;这种特性指向一种完全不同的、基于异步和事件驱动的消息传递架构。

此外,WebSocket只是一个底层传输协议,不像HTTP那样对消息内容指定任何语义;这意味着除非客户端和服务器对消息的语义达成共识,否则无法对消息进行路由或处理。为了解决这一问题,WebSocket客户端和服务器可以通过HTTP握手请求中的Sec-WebSocket-Protocol头协商使用更高级别的消息传递协议(例如STOMP);若未协商使用特定协议,则需要制定自己的消息规范。

# 1.2、何时使用WebSocket

WebSocket可使网页更加动态和交互化,但在许多情况下,结合使用AJAX和HTTP流式传输或长轮询,也能提供一个简单而有效的解决方案。

例如,新闻、邮件和社交动态需要动态更新,但每隔几分钟更新一次可能就足够了;而协作、游戏和金融应用则需要接近实时的数据同步。

不过,延迟本身并非是决定使用WebSocket的唯一因素,如果消息量相对较低(例如监控网络故障),使用HTTP流式传输或轮询也可以提供有效的解决方案;但当同时存在低延迟、高频率和高容量的需求时,则应首选WebSocket。

此外,在互联网环境中,因无法控制限制性质的代理服务器,可能会影响WebSocket的交互:可能是因为代理未配置转发Upgrade头,或者他们由于长时间处于空闲的连接而将其关闭。因此,在防火墙内的内部应用中使用WebSocket会比直接面向公众的应用更为直接。

# 2、API

Spring框架提供了一个WebSocket API,可用于编写处理WebSocket消息的客户端和服务器端应用程序。

# 2.1、服务器

要创建WebSocket服务器,首先要创建一个WebSocketHandler。下面是一个示例:

import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        //...
    }
}

之后,将其映射到一个URL上:

@Configuration
class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());
        int order = -1; // 在注解控制器之前

        return new SimpleUrlHandlerMapping(map, order);
    }
}

如果你使用了WebFlux配置,则无需进行其他操作;如果没有使用WebFlux配置,则需要声明一个WebSocketHandlerAdapter,如下所示:

@Configuration
class WebConfig {

    //...

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

# 2.2、WebSocketHandler

WebSocketHandler 的 handle 方法接受一个 WebSocketSession 参数,并返回 Mono<Void>,以指示会话的应用处理何时完成。会话通过两个流进行处理,一个用于入站消息,另一个用于出站消息。

以下是处理这两个流的方法:

WebSocketSession方法 描述
Flux<WebSocketMessage> receive() 提供对入站消息流的访问,并在连接关闭时完成。
Mono<Void> send(Publisher<WebSocketMessage>) 接收出站消息的源,写入消息,并返回一个 Mono<Void>,该对象在源完成且写入操作完成时完成。

WebSocketHandler 必须将入站和出站流组合成一个统一的流,并返回一个 Mono<Void>,反映该流的完成状态。根据应用需求,统一流完成的条件如下:

  • 入站或出站消息流完成。
  • 入站流完成(即连接关闭),而出站流是无限的。
  • 通过 WebSocketSession 的 close 方法在选定的时间点完成。

当入站和出站消息流组合在一起时,不需要检查连接是否打开,因为Reactive Streams会发出结束信号。入站流接收完成或错误信号,出站流接收取消信号。

处理程序最基本的实现是处理入站流,以下是实现例子:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive() // (1): 访问入站消息流
              .doOnNext(message -> {
                    //...处理每个消息
                })
              .concatMap(message -> {
                    //... 使用消息内容执行嵌套异步操作
                })
              .then(); // (4): 返回一个Mono<Void>,在接收完成时完成
    }
}
  • (1): 访问入站消息流。
  • (2): 处理每个消息。
  • (3): 使用消息内容执行嵌套的异步操作。
  • (4): 返回一个 Mono<Void>,在接收完成时完成。

提示:对于嵌套的异步操作,在使用池化数据缓冲区的底层服务器(如Netty)上,可能需要调用 message.retain(),否则在读取数据之前数据缓冲区可能会被释放。更多背景信息,请参阅数据缓冲区和编解码器。

以下实现将入站和出站流结合在一起:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Flux<WebSocketMessage> output = session.receive() // (1): 处理入站消息流
              .doOnNext(message -> {
                    //...
                })
              .concatMap(message -> {
                    //...
                })
              .map(value -> session.textMessage("Echo " + value)); // (2): 创建出站消息,生成组合流

        return session.send(output); // (3): 返回一个Mono<Void>,在持续接收时不完成
    }
}
  • (1): 处理入站消息流。
  • (2): 创建出站消息,生成组合流。
  • (3): 返回一个 Mono<Void>,在持续接收时不完成。

入站和出站流可以是独立的,仅为了完成而结合在一起,如下例所示:

class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Mono<Void> input = session.receive() // (1): 处理入站消息流
              .doOnNext(message -> {
                    //...
                })
              .concatMap(message -> {
                    //...
                })
              .then();

        Flux<String> source =... ;
        Mono<Void> output = session.send(source.map(session::textMessage)); // (2): 发送出站消息

        return Mono.zip(input, output).then(); // (3): 组合流,并返回一个Mono<Void>,在任一流结束时完成
    }
}
  • (1): 处理入站消息流。
  • (2): 发送出站消息。
  • (3): 组合流,并返回一个 Mono<Void>,在任一流结束时完成。

# 2.3、DataBuffer

DataBuffer 是WebFlux中字节缓冲区的一种表现形式,在参考文档的Spring Core部分的“数据缓冲区和编解码器”章节有更详细的介绍。需要明白的关键要点是,在某些服务器(如Netty)上,字节缓冲区是池化的并且使用引用计数,在使用后必须释放以避免内存泄漏。

当在Netty上运行时,如果应用程序希望保留输入数据缓冲区,则需要使用 DataBufferUtils.retain(dataBuffer) 来确保这些缓冲区不会被释放,后续在使用完这些缓冲区时,还需调用 DataBufferUtils.release(dataBuffer) 方法释放它们。

# 2.4、握手

WebSocketHandlerAdapter 将任务委托给 WebSocketService。默认情况下,它是 HandshakeWebSocketService 的一个实例,该实例会对WebSocket请求进行基本检查,然后使用服务器对应的RequestUpgradeStrategy。目前,对Reactor Netty、Tomcat、Jetty和Undertow有内置支持。

HandshakeWebSocketService 公开了一个 sessionAttributePredicate 属性,允许设置一个 Predicate<String>,用于从 WebSession 中提取属性,并将其插入到 WebSocketSession 的属性中。

# 2.5、服务器配置

每个服务器的 RequestUpgradeStrategy 都公开了特定于底层WebSocket服务器引擎的配置选项。当使用WebFlux Java配置时,可以在WebFlux配置的相应部分自定义这些属性。如果不使用WebFlux配置,可以使用以下方法:

@Configuration
class WebConfig {

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter(webSocketService());
    }

    @Bean
    public WebSocketService webSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}

请查看你所使用服务器的升级策略,以了解可用的选项。目前,只有Tomcat和Jetty公开了此类选项。

# 2.6、CORS

配置CORS并限制对WebSocket端点访问的最简单方法是让 WebSocketHandler 实现 CorsConfigurationSource 接口,并返回一个包含允许的来源、头信息和其他详细信息的 CorsConfiguration 对象。如果不能这样做,也可以在 SimpleUrlHandler 上设置 corsConfigurations 属性,通过URL模式指定CORS设置。如果两者都指定,则会使用 CorsConfiguration 的 combine 方法将它们合并。

# 2.7、客户端

Spring WebFlux 提供了 WebSocketClient 抽象,实现包括 Reactor Netty、Tomcat、Jetty、Undertow 和标准 Java(即 JSR-356)。

注意:Tomcat客户端实际上是标准Java客户端的扩展,在 WebSocketSession 处理中具有一些额外功能,以利用Tomcat特定的API来为背压暂停接收消息。

要启动WebSocket会话,可以创建客户端实例并使用其 execute 方法:

WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
              .doOnNext(System.out::println)
              .then());

一些客户端(如Jetty)实现了 Lifecycle 接口,在使用前需要启动和停止操作。所有客户端都有与底层WebSocket客户端配置相关的构造函数选项。

# 五、测试

spring-test 模块提供了 ServerHttpRequest、ServerHttpResponse 和 ServerWebExchange 的模拟实现。关于模拟对象的讨论,请参阅 Spring Web Reactive)。

WebTestClient) 基于这些模拟请求和响应对象构建,支持在不使用 HTTP 服务器的情况下测试 WebFlux 应用程序。你也可以使用 WebTestClient 进行端到端的集成测试。

# 1、RSocket

本节介绍了 Spring Framework 对 RSocket 协议的支持。

# 1.1、概述

RSocket 是一种应用层协议,用于在 TCP、WebSocket 和其他字节流传输协议上进行多路复用和双工通信,支持以下几种交互模型:

  • Request-Response(请求 - 响应)—— 发送一条消息并接收一条返回消息。
  • Request-Stream(请求 - 流)—— 发送一条消息并接收一系列返回消息。
  • Channel(通道)—— 双向发送消息流。
  • Fire-and-Forget(即发即弃)—— 发送单向消息。

一旦建立了初始连接,"客户端"和"服务器"的区别就不再存在,因为双方变得对等,并且每一方都可以发起上述交互。这就是为什么在协议中将参与方称为"请求者"和"响应者",而上述交互被称为"请求流"或简称为"请求"。

RSocket 协议的关键特性和优势如下:

  • 跨网络边界的 Reactive Streams 语义:对于 Request-Stream 和 Channel 这类流式请求,背压信号会在请求者和响应者之间传递,使请求者能够从源头上减缓响应者的处理速度,从而减少对网络层拥塞控制的依赖,以及在网络层或任何层次进行缓冲的需求。
  • 请求限流:此功能名为“租约”(Leasing),源于 LEASE 帧,每一端都可以发送该帧,以限制另一端在给定时间内允许的请求总数。租约会定期更新。
  • 会话恢复:该功能是为应对连接中断而设计的,需要维护一些状态。状态管理对应用程序是透明的,并且与背压机制配合良好,背压机制可以在可能的情况下停止生产者,从而减少所需的状态量。
  • 大消息的分段和重组。
  • 心跳机制。

RSocket 有多种语言的实现 (opens new window)。Java 库 (opens new window)基于 Project Reactor (opens new window) 和用于传输的 Reactor Netty (opens new window) 构建。这意味着应用程序中 Reactive Streams 发布者的信号可以通过 RSocket 透明地在网络中传播。

# a、协议

RSocket 的一个优点是,它在网络传输层有明确的行为定义,并且有易于阅读的规范 (opens new window)以及一些协议扩展 (opens new window)。因此,建议阅读该规范,而不依赖于语言实现和更高级别的框架 API。本节提供了一个简要概述,以建立一些上下文。

连接过程

最初,客户端通过 TCP 或 WebSocket 等底层流式传输协议连接到服务器,并向服务器发送一个 SETUP 帧来设置连接参数。

服务器可能会拒绝 SETUP 帧,但通常在客户端发送、服务器接收之后,双方就可以开始发送请求。不过,如果 SETUP 帧指示使用了租约语义来限制请求数量,那么双方都必须等待对方的 LEASE 帧允许后才能发送请求。

发送请求

一旦建立连接,双方都可以通过 REQUEST_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL 或 REQUEST_FNF 帧发起请求。每个帧都携带请求者发送给响应者的一条消息。

响应者随后可以返回包含响应消息的 PAYLOAD 帧。对于 REQUEST_CHANNEL 交互,请求者也可以发送包含更多请求消息的 PAYLOAD 帧。

当请求涉及消息流(如 Request-Stream 和 Channel)时,响应者必须遵循请求者的需求信号。需求以消息数量表示。初始需求在 REQUEST_STREAM 和 REQUEST_CHANNEL 帧中指定,后续需求通过 REQUEST_N 帧发出信号。

每一方还可以通过 METADATA_PUSH 帧发送元数据通知,这些通知不针对任何单个请求,而是与整个连接相关。

消息格式

RSocket 消息包含数据和元数据。元数据可用于发送路由、安全令牌等信息。数据和元数据可以有不同的格式。每种类型的 MIME 类型在 SETUP 帧中声明,并适用于给定连接上的所有请求。

虽然所有消息都可以有元数据,但像路由这样的元数据通常是每个请求独有的,因此通常只包含在请求的第一条消息中,即使用 REQUEST_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL 或 REQUEST_FNF 帧发送的消息。

协议扩展定义了应用程序中常用的元数据格式:

  • 复合元数据 (opens new window):多个独立格式化的元数据条目。
  • 路由 (opens new window):请求的路由。
# b、实现

RSocket 的 Java 实现 (opens new window)基于 Project Reactor (opens new window)。TCP 和 WebSocket 的传输层基于 Reactor Netty (opens new window) 构建。作为一个 Reactive Streams 库,Reactor 简化了协议的实现工作。对于应用程序来说,使用带有声明式操作符和透明背压支持的 Flux 和 Mono 是很自然的选择。

RSocket Java 中的 API 有意设计得尽可能简洁和基础。它专注于协议特性,将应用程序编程模型(例如,RPC 代码生成与其他模型)作为更高级别的独立关注点。

主要接口 io.rsocket.RSocket (opens new window) 用 Mono 表示单条消息的承诺,用 Flux 表示消息流,用 io.rsocket.Payload 表示可访问字节缓冲区中的数据和元数据的实际消息,来对四种请求交互类型进行建模。RSocket 接口的使用是对称的。对于请求,应用程序会得到一个 RSocket 实例来执行请求;对于响应,应用程序实现 RSocket 接口来处理请求。

这并不是一个全面的介绍。在大多数情况下,Spring 应用程序不必直接使用其 API。然而,了解或独立于 Spring 测试 RSocket 可能很重要。RSocket Java 仓库包含了许多 示例应用程序 (opens new window),展示了其 API 和协议特性。

# c、支持

spring-messaging 模块包含以下内容:

  • RSocketRequester (opens new window):通过 io.rsocket.RSocket 执行请求的流式 API,支持数据和元数据的编码/解码。
  • 注解响应者 (opens new window):使用 @MessageMapping 和 @RSocketExchange 注解的处理方法来进行响应。
  • RSocket 接口 (opens new window):将 RSocket 服务声明为带有 @RSocketExchange 方法的 Java 接口,可作为请求者或响应者使用。

spring-web 模块包含 Encoder 和 Decoder 实现,如 Jackson CBOR/JSON 和 Protobuf,这些是 RSocket 应用程序可能需要的。它还包含 PathPatternParser,可用于高效的路由匹配。

Spring Boot 2.2 支持通过 TCP 或 WebSocket 启动 RSocket 服务器,包括在 WebFlux 服务器中通过 WebSocket 暴露 RSocket 的选项。还有对客户端的支持,以及对 RSocketRequester.Builder 和 RSocketStrategies 的自动配置。有关更多详细信息,请参阅 Spring Boot 参考文档中的 RSocket 部分 (opens new window)。

Spring Security 5.2 提供了 RSocket 支持。

Spring Integration 5.2 提供了入站和出站网关,用于与 RSocket 客户端和服务器进行交互。有关更多详细信息,请参阅 Spring Integration 参考手册。

Spring Cloud Gateway 支持 RSocket 连接。

# 1.2、RSocketRequester

RSocketRequester 提供了一个流式 API 来执行 RSocket 请求,接受和返回对象作为数据和元数据,而不是底层的数据缓冲区。它可以对称使用,既可以从客户端发起请求,也可以从服务器发起请求。

# a、客户端请求者

在客户端获取 RSocketRequester 需要连接到服务器,这涉及发送一个包含连接设置的 RSocket SETUP 帧。RSocketRequester 提供了一个构建器,用于准备一个 io.rsocket.core.RSocketConnector,包括 SETUP 帧的连接设置。

以下是使用默认设置进行连接的最基本方式:

RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);

URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);

上述代码不会立即建立连接。当发起请求时,会透明地建立并使用共享连接。

# 连接设置

RSocketRequester.Builder 提供了以下方法来自定义初始 SETUP 帧:

  • dataMimeType(MimeType):设置连接数据的 MIME 类型。
  • metadataMimeType(MimeType):设置连接元数据的 MIME 类型。
  • setupData(Object):包含在 SETUP 帧中的数据。
  • setupRoute(String, Object…):包含在 SETUP 帧元数据中的路由。
  • setupMetadata(Object, MimeType):包含在 SETUP 帧中的其他元数据。

对于数据,默认的 MIME 类型从第一个配置的 Decoder 派生而来。对于元数据,默认的 MIME 类型是复合元数据 (opens new window),它允许每个请求包含多个元数据值和 MIME 类型对。通常,两者都不需要更改。

SETUP 帧中的数据和元数据是可选的。在服务器端,可以使用 @ConnectMapping (opens new window) 方法来处理连接的开始和 SETUP 帧的内容。元数据可用于连接级别的安全。

# 策略

RSocketRequester.Builder 接受 RSocketStrategies 来配置请求者。你需要使用它来提供编码器和解码器,用于数据和元数据值的序列化和反序列化。默认情况下,仅注册了 spring-core 中用于 String、byte[] 和 ByteBuffer 的基本编解码器。添加 spring-web 模块后,可以注册更多编解码器,示例如下:

RSocketStrategies strategies = RSocketStrategies.builder()
   .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
   .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
   .build();

RSocketRequester requester = RSocketRequester.builder()
   .rsocketStrategies(strategies)
   .tcp("localhost", 7000);

RSocketStrategies 设计为可重用。在某些情况下,例如同一应用程序中的客户端和服务器,最好在 Spring 配置中声明它。

# 客户端响应者

RSocketRequester.Builder 可用于配置对服务器请求的响应者。

你可以使用基于与服务器相同基础设施的注解处理器进行客户端响应,但需要以编程方式进行注册,示例如下:

RSocketStrategies strategies = RSocketStrategies.builder()
   .routeMatcher(new PathPatternRouteMatcher())  // (1)
   .build();

SocketAcceptor responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); // (2)

RSocketRequester requester = RSocketRequester.builder()
   .rsocketConnector(connector -> connector.acceptor(responder)) // (3)
   .tcp("localhost", 7000);
  1. 如果存在 spring-web 模块,使用 PathPatternRouteMatcher 进行高效的路由匹配。
  2. 从带有 @MessageMapping 和/或 @ConnectMapping 方法的类创建响应者。
  3. 注册响应者。

注意,上述代码只是一个为客户端响应者进行编程式注册而设计的快捷方式。对于客户端响应者位于 Spring 配置中的替代场景,你仍然可以将 RSocketMessageHandler 声明为 Spring bean,然后按如下方式应用:

ApplicationContext context =... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

RSocketRequester requester = RSocketRequester.builder()
   .rsocketConnector(connector -> connector.acceptor(handler.responder()))
   .tcp("localhost", 7000);

对于上述情况,你可能还需要在 RSocketMessageHandler 中使用 setHandlerPredicate 来切换到不同的检测客户端响应者的策略,例如,基于自定义注解(如 @RSocketClientResponder),而不是默认的 @Controller。在同一应用程序中同时存在客户端和服务器,或多个客户端的场景中,这是必要的。

有关编程模型的更多信息,请参见 注解响应者 (opens new window)。

# 高级用法

RSocketRequesterBuilder 提供了一个回调,用于暴露底层的 io.rsocket.core.RSocketConnector,以便进行更多的配置选项,如保持活动间隔、会话恢复、拦截器等。你可以按如下方式配置这些选项:

RSocketRequester requester = RSocketRequester.builder()
   .rsocketConnector(connector -> {
        //...
    })
   .tcp("localhost", 7000);
# b、服务器请求者

从服务器向已连接的客户端发送请求,关键在于从服务器获取与该客户端连接的请求者。

在 注解响应者 (opens new window) 中,@ConnectMapping 和 @MessageMapping 方法支持 RSocketRequester 参数。使用此参数可以访问与客户端连接的请求者。请记住,@ConnectMapping 方法本质上是 SETUP 帧的处理器,在开始发送请求之前必须处理该帧。因此,最开始的请求必须与处理过程分离。例如:

@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
    requester.route("status").data("5")
       .retrieveFlux(StatusReport.class)
       .subscribe(bar -> { // (1)
            //...
        });
    return... // (2)
}
  1. 异步启动请求,与处理过程分离。
  2. 执行处理并返回完成 Mono<Void>。
# c、请求

一旦你获得了客户端 (opens new window)或服务器 (opens new window)请求者,就可以按如下方式发起请求:

ViewBox viewBox =... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") // (1)
       .data(viewBox) // (2)
       .retrieveFlux(AirportLocation.class); // (3)
  1. 指定一个路由,包含在请求消息的元数据中。
  2. 为请求消息提供数据。
  3. 声明预期的响应类型。

交互类型由输入和输出的基数隐式决定。上述示例是一个 Request-Stream,因为发送了一个值并接收了一个值流。在大多数情况下,只要输入和输出的选择与 RSocket 交互类型以及响应者期望的输入和输出类型相匹配,你就不必考虑这个问题。唯一无效的组合示例是多对一。

data(Object) 方法还接受任何 Reactive Streams Publisher,包括 Flux 和 Mono,以及在 ReactiveAdapterRegistry 中注册的任何其他值生产者。对于产生相同类型值的多值 Publisher(如 Flux),考虑使用重载的 data 方法,以避免对每个元素进行类型检查和 Encoder 查找:

data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);

data(Object) 步骤是可选的。对于不发送数据的请求,可以跳过该步骤:

Mono<AirportLocation> location = requester.route("find.radar.EWR")
   .retrieveMono(AirportLocation.class);

如果使用复合元数据 (opens new window)(默认情况),并且注册的 Encoder 支持这些值,则可以添加额外的元数据值。例如:

String securityToken =... ;
ViewBox viewBox =... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");

Flux<AirportLocation> locations = requester.route("locate.radars.within")
       .metadata(securityToken, mimeType)
       .data(viewBox)
       .retrieveFlux(AirportLocation.class);

对于 Fire-and-Forget 交互,使用返回 Mono<Void> 的 send() 方法。请注意,Mono 仅表示消息已成功发送,而不表示已被处理。

对于 Metadata-Push 交互,使用返回 Mono<Void> 的 sendMetadata() 方法。

# 1.3、注解响应者

RSocket 响应者可以实现为 @MessageMapping 和 @ConnectMapping 方法。@MessageMapping 方法处理单个请求,而 @ConnectMapping 方法处理连接级事件(设置和元数据推送)。注解响应者支持对称使用,既可以从服务器端进行响应,也可以从客户端进行响应。

# a、服务器响应者

要在服务器端使用注解响应者,需要在 Spring 配置中添加 RSocketMessageHandler,以检测带有 @MessageMapping 和 @ConnectMapping 方法的 @Controller bean:

@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}

然后,通过 Java RSocket API 启动一个 RSocket 服务器,并插入 RSocketMessageHandler 作为响应者,示例如下:

ApplicationContext context =... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
    RSocketServer.create(handler.responder())
       .bind(TcpServerTransport.create("localhost", 7000))
       .block();

RSocketMessageHandler 默认支持复合 (opens new window)和路由 (opens new window)元数据。如果需要切换到不同的 MIME 类型或注册其他元数据 MIME 类型,可以设置其 MetadataExtractor (opens new window)。

你需要设置支持元数据和数据格式所需的 Encoder 和 Decoder 实例。你可能需要 spring-web 模块来获取编解码器实现。

默认情况下,使用 SimpleRouteMatcher 通过 AntPathMatcher 进行路由匹配。我们建议使用 spring-web 中的 PathPatternRouteMatcher 进行高效的路由匹配。RSocket 路由可以是分层的,但不是 URL 路径。两个路由匹配器默认都配置为使用 "." 作为分隔符,并且不像 HTTP URL 那样进行 URL 解码。

RSocketMessageHandler 可以通过 RSocketStrategies 进行配置,如果需要在同一进程中的客户端和服务器之间共享配置,这可能会很有用:

@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
           .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
           .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
           .routeMatcher(new PathPatternRouteMatcher())
           .build();
    }
}
# b、客户端响应者

客户端的注解响应者需要在 RSocketRequester.Builder 中进行配置。详细信息请参见 客户端响应者 (opens new window)。

# c、@MessageMapping

在完成服务器 (opens new window)或客户端 (opens new window)响应者配置后,可以按如下方式使用 @MessageMapping 方法:

@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        //...
    }
}

上述 @MessageMapping 方法响应具有路由 "locate.radars.within" 的 Request-Stream 交互。它支持灵活的方法签名,可以选择使用以下方法参数:

方法参数 描述
@Payload 请求的有效负载。可以是 Mono 或 Flux 等异步类型的具体值。注意:注解的使用是可选的。不是简单类型且不是其他受支持参数的方法参数,将被视为预期的有效负载。
RSocketRequester 用于向远程端发送请求的请求者。
@DestinationVariable 根据映射模式中的变量从路由中提取的值,例如 @MessageMapping("find.radar.{id}")。
@Header 如 MetadataExtractor (opens new window) 中所述,为提取而注册的元数据值。
@Headers Map<String, Object> 如 MetadataExtractor (opens new window) 中所述,为提取而注册的所有元数据值。

返回值预计是一个或多个对象,将被序列化为响应有效负载。可以是 Mono 或 Flux 等异步类型、具体值,或者是 void 或 Mono<Void> 等无值异步类型。

@MessageMapping 方法支持的 RSocket 交互类型由输入(即 @Payload 参数)和输出的基数决定,其中基数的含义如下:

基数 描述
1 显式值或 Mono<T> 等单值异步类型。
多 Flux<T> 等多值异步类型。
0 对于输入,这意味着方法没有 @Payload 参数。对于输出,这是 void 或 Mono<Void> 等无值异步类型。

下表显示了所有输入和输出基数组合以及相应的交互类型:

输入基数 输出基数 交互类型
0, 1 0 Fire-and-Forget, Request-Response
0, 1 1 Request-Response
0, 1 多 Request-Stream
多 0, 1, 多 Request-Channel
# d、@RSocketExchange

作为 @MessageMapping 的替代方案,你还可以使用 @RSocketExchange 方法处理请求。这类方法在 RSocket 接口 (opens new window) 上声明,可以通过 RSocketServiceProxyFactory 作为请求者使用,也可以由响应者实现。

例如,作为响应者处理请求:

public interface RadarsService {

    @RSocketExchange("locate.radars.within")
    Flux<AirportLocation> radars(MapRequest request);
}

@Controller
public class RadarsController implements RadarsService {

    public Flux<AirportLocation> radars(MapRequest request) {
        //...
    }
}

@RSocketExhange 和 @MessageMapping 之间存在一些差异,因为前者需要适用于请求者和响应者的使用。例如,虽然 @MessageMapping 可以声明处理任意数量的路由,并且每个路由可以是一个模式,但 @RSocketExchange 必须使用单个具体路由声明。在支持的方法参数方面也存在一些与元数据相关的细微差异,请参见 @MessageMapping (opens new window) 和 RSocket 接口 (opens new window) 了解支持的参数列表。

@RSocketExchange 可以在类型级别使用,为给定的 RSocket 服务接口的所有路由指定一个公共前缀。

# e、@ConnectMapping

@ConnectMapping 处理 RSocket 连接开始时的 SETUP 帧,以及后续通过 METADATA_PUSH 帧发送的任何元数据推送通知,即 io.rsocket.RSocket 中的 metadataPush(Payload)。

@ConnectMapping 方法支持与 @MessageMapping (opens new window) 相同的参数,但基于 SETUP 和 METADATA_PUSH 帧的元数据和数据。@ConnectMapping 可以有一个模式,用于将处理限定为元数据中具有特定路由的连接;如果未声明模式,则所有连接都匹配。

@ConnectMapping 方法不能返回数据,必须声明为 void 或 Mono<Void> 作为返回值。如果处理新连接时返回错误,则该连接将被拒绝。处理过程中不得阻塞以向连接的 RSocketRequester 发送请求。详细信息请参见 服务器请求者 (opens new window)。

# 1.4、MetadataExtractor

响应者必须解释元数据。复合元数据 (opens new window) 允许使用独立格式化的元数据值(例如,用于路由、安全、跟踪),每个值都有自己的 MIME 类型。应用程序需要一种方法来配置要支持的元数据 MIME 类型,并一种方法来访问提取的值。

MetadataExtractor 是一个接口,用于将序列化的元数据转换为解码后的名值对,然后可以像访问头信息一样通过名称访问这些对,例如在注解处理方法中通过 @Header 注解访问。

DefaultMetadataExtractor 可以提供 Decoder 实例来解码元数据。它内置支持 "message/x.rsocket.routing.v0" (opens new window),会将其解码为 String 并存储在 "route" 键下。对于任何其他 MIME 类型,你需要提供一个 Decoder 并按如下方式注册该 MIME 类型:

DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");

复合元数据在组合独立的元数据值方面效果很好。然而,请求者可能不支持复合元数据,或者可能选择不使用它。为此,DefaultMetadataExtractor 可能需要自定义逻辑来将解码后的值映射到输出映射。以下是一个使用 JSON 作为元数据的示例:

DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
    MimeType.valueOf("application/vnd.myapp.metadata+json"),
    new ParameterizedTypeReference<Map<String,String>>() {},
    (jsonMap, outputMap) -> {
        outputMap.putAll(jsonMap);
    });

在通过 RSocketStrategies 配置 MetadataExtractor 时,你可以让 RSocketStrategies.Builder 使用配置的解码器创建提取器,并简单地使用回调来定制注册,如下所示:

RSocketStrategies strategies = RSocketStrategies.builder()
   .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        //...
    })
   .build();

# 1.5、接口

Spring Framework 允许你将 RSocket 服务定义为带有 @RSocketExchange 方法的 Java 接口。你可以将这样的接口传递给 RSocketServiceProxyFactory 来创建一个代理,该代理通过 RSocketRequester (opens new window) 执行请求。你也可以将该接口实现为处理请求的响应者。

首先,创建带有 @RSocketExchange 方法的接口:

interface RadarService {

    @RSocketExchange("radars")
    Flux<AirportLocation> getRadars(@Payload MapRequest request);

    // 更多 RSocket 交换方法...

}

现在,你可以创建一个代理,在调用方法时执行请求:

RSocketRequester requester =... ;
RSocketServiceProxyFactory factory = RSocketServiceProxyFactory.builder(requester).build();

RadarService service = factory.createClient(RadarService.class);

你也可以实现该接口,作为响应者处理请求。请参阅 注解响应者 (opens new window)。

# a、方法参数

带有注解的 RSocket 交换方法支持灵活的方法签名,包含以下方法参数:

方法参数 描述
@DestinationVariable 添加一个路由变量,与 @RSocketExchange 注解中的路由一起传递给 RSocketRequester,以展开路由中的模板占位符。该变量可以是 String 或任何 Object,然后通过 toString() 进行格式化。
@Payload 设置请求的输入有效负载。可以是具体值,也可以是任何能够通过 ReactiveAdapterRegistry 适配为 Reactive Streams Publisher 的值生产者。除非 required 属性设置为 false,或者参数根据 MethodParameter#isOptional (opens new window) 被标记为可选,否则必须提供有效负载。
Object,后跟 MimeType 输入有效负载中元数据条目的值。可以是任何 Object,只要下一个参数是元数据条目的 MimeType。该值可以是具体值,也可以是任何能够通过 ReactiveAdapterRegistry 适配为 Reactive Streams Publisher 的单值生产者。
MimeType 元数据条目的 MimeType。前一个方法参数预期是元数据值。
# b、返回值

带有注解的 RSocket 交换方法支持返回具体值或任何能够通过 ReactiveAdapterRegistry 适配为 Reactive Streams Publisher 的值生产者。

默认情况下,具有同步(阻塞)方法签名的 RSocket 服务方法的行为取决于底层 RSocket ClientTransport 的响应超时设置以及 RSocket 保持活动设置。RSocketServiceProxyFactory.Builder 确实提供了一个 blockTimeout 选项,也允许你配置阻塞等待响应的最长时间,但我们建议在 RSocket 级别配置超时值以获得更多控制。

# 六、响应式库

spring-webflux 依赖于 reactor-core,并在内部使用它来组合异步逻辑并提供响应式流支持。通常情况下,WebFlux API会返回 Flux 或 Mono(因为它们在内部被使用),并且能宽松地接受任何响应式流 Publisher 实现作为输入。如果提供了一个 Publisher,它只能被视为一个语义未知(0 到 N)的流。然而,如果语义是已知的,你应该使用 Flux 或 Mono.from(Publisher) 对其进行包装,而不是直接传递原始的 Publisher。使用 Flux 还是 Mono 很重要,因为这有助于表达数据的数量关系,例如,预期是单个异步值还是多个异步值,这对做出决策(例如,在编码或解码 HTTP 消息时)至关重要。

对于带注解的控制器,WebFlux 会自动适配应用程序选择的响应式库。这借助于 ReactiveAdapterRegistry (opens new window) 来完成,它为响应式库和其他异步类型提供了可插拔的支持。该注册表内置了对 RxJava 3、Kotlin 协程和 SmallRye Mutiny 的支持,但你也可以注册其他类型。

祝你变得更强!

编辑 (opens new window)
上次更新: 2025/03/03
Spring中的Web访问:WebSocket支持
Spring中的集成测试与单元测试

← Spring中的Web访问:WebSocket支持 Spring中的集成测试与单元测试→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式