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 呢?
这是一个很自然会问到的问题,但这样的提问方式会造成一种不合理的二分法。实际上,两者可以协同工作,以扩展可用选项的范围。这两者的设计旨在保持连续性和一致性,它们可以并存使用,而且双方的反馈都对彼此有益。下图展示了两者之间的关系、它们的共同点以及各自独特的支持功能:
我们建议你考虑以下具体要点:
- 如果你现有的 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) {
//...
}
}
- 类级别的 URI 映射。
- 方法级别的 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) {
//...
}
- 检查
myParam
是否等于myValue
。
你也可以对请求头条件使用相同的方式,如下例所示:
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // (1)
public void findPet(@PathVariable String petId) {
//...
}
- 检查
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)
}
}
- 注入目标处理程序和控制器的处理程序映射。
- 准备请求映射元数据。
- 获取处理方法。
- 添加注册信息。
# 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";
}
//...
}
- 使用
@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 {
//...
}
- 使用
@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();
//...
}
}
- 使用
@SessionAttributes
注解。 - 使用
SessionStatus
变量。
# k、@SessionAttribute
如果你需要访问全局管理的现有会话属性(即在控制器外部管理,例如通过过滤器),且这些属性可能存在也可能不存在,你可以在方法参数上使用 @SessionAttribute
注解,如下例所示:
@GetMapping("/")
public String handle(@SessionAttribute User user) { // 1. 使用 `@SessionAttribute`
//...
}
- 使用
@SessionAttribute
。
对于需要添加或移除会话属性的用例,可以考虑将 WebSession
注入到控制器方法中。
对于在控制器工作流中将会话用作模型属性的临时存储场景,可以考虑使用 SessionAttributes
,详见 @SessionAttributes
。
# l、@RequestAttribute
和@SessionAttribute
类似,你可以使用@RequestAttribute
注解来访问此前创建好的请求属性(例如,由WebFilter
创建),如下例所示:
@GetMapping("/")
public String handle(@RequestAttribute Client client) { // 1. 使用 `@RequestAttribute`
//...
}
- 使用
@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)
//...
}
- 使用
@RequestPart
获取元数据。 - 使用
@RequestPart
获取文件。
要对原始部分内容进行反序列化(例如转换为 JSON,类似于 @RequestBody
),你可以声明一个具体的目标 Object
,而不是 Part
,如下示例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { // (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)
//...
}
- 使用
@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; // 完成或错误信号
}
}));
}
- 使用
@RequestBody
。 - 某个特定部分的最后一个
PartEvent
的isLast()
会被设置为true
,并且后面可能会跟随属于后续部分的额外事件。这使得isLast
属性适合作为Flux::windowUntil
操作符的谓词,以便将所有部分的事件分割成每个都属于单个部分的窗口。 Flux::switchOnFirst
操作符允许你判断正在处理的是表单字段还是文件上传。- 处理表单字段。
- 处理文件上传。
- 必须完全消耗、中继或释放主体内容,以避免内存泄漏。
接收到的部分事件也可以通过 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));
}
//...
}
- 使用
@InitBinder
注解。
另外,当通过共享的 FormattingConversionService
使用基于 Formatter
的设置时,可以采用相同的方法来注册特定于控制器的 Formatter
实例,示例如下:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); // 1. 添加自定义格式化器(这里是 DateFormatter)
}
//...
}
- 添加自定义格式化器(这里是
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 验证。验证可以在两个级别上应用:
- 方法参数级验证:如果方法参数使用 Jakarta 的
@Valid
或 Spring 的@Validated
注解,并且后面没有紧接Errors
或BindingResult
参数,而且不需要进行方法级验证(后续讨论),则 @ModelAttribute、@RequestBody 和 @RequestPart 参数解析器会单独验证方法参数。这种情况下抛出的异常是WebExchangeBindException
。 - 方法级验证:当
@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`.
- 使用 URI 模板的静态工厂方法。
- 添加或替换 URI 组件。
- 请求对 URI 模板和 URI 变量进行编码。
- 构建
UriComponents
。 - 展开变量并获取
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 解析器类型:
- RFC 解析器:这种解析器类型期望 URI 字符串符合 RFC 3986 语法,并将不符合语法的情况视为非法。
- 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) {
//...
}
}
- 在类级别使用
@CrossOrigin
。 - 在方法级别使用
@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";
}
- 应用程序特定的计算。
- 响应已设置为 304(未修改),无需进一步处理。
- 继续处理请求。
针对 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 脚本引擎之上的模板库的集成。下表展示了我们在不同脚本引擎上测试过的模板库:
提示:集成任何其他脚本引擎的基本规则是它必须实现 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);
- 如果存在
spring-web
模块,使用PathPatternRouteMatcher
进行高效的路由匹配。 - 从带有
@MessageMapping
和/或@ConnectMapping
方法的类创建响应者。 - 注册响应者。
注意,上述代码只是一个为客户端响应者进行编程式注册而设计的快捷方式。对于客户端响应者位于 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)
}
- 异步启动请求,与处理过程分离。
- 执行处理并返回完成
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)
- 指定一个路由,包含在请求消息的元数据中。
- 为请求消息提供数据。
- 声明预期的响应类型。
交互类型由输入和输出的基数隐式决定。上述示例是一个 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 的支持,但你也可以注册其他类型。
祝你变得更强!