Web 响应式
1. Spring WebFlux
Spring 框架中最初包含的 Web 框架 Spring Web MVC 是专为 Servlet API 和 Servlet 容器而设计的。响应式栈的 Web 框架 Spring WebFlux 则是在 5.0 版本之后加入的。它完全是非阻塞的,支持Reactive Streams背压机制,并可在 Netty、Undertow 以及 Servlet 3.1+ 容器等服务器上运行。
这两个 Web 框架与其源模块的名称相对应(spring-webmvc 和 spring-webflux),并在 Spring Framework 中并存。每个模块都是可选的。应用程序可以使用其中一个模块,或者在某些情况下同时使用两者——例如,使用响应式的 WebClient 的 Spring MVC 控制器。
1.1. 概述
为什么创建了 Spring WebFlux?
部分原因在于需要一个非阻塞的 Web 技术栈,以便使用少量线程处理并发,并以更少的硬件资源实现扩展。Servlet 3.1 确实提供了用于非阻塞 I/O 的 API。然而,使用该 API 会使开发者偏离 Servlet API 的其余部分,因为这些部分的契约是同步的(Filter、Servlet)或阻塞的(getParameter、getPart)。这促使人们开发一种新的通用 API,作为适用于任何非阻塞运行时环境的基础。这一点尤为重要,因为像 Netty 这样的服务器在异步、非阻塞领域已经非常成熟。
答案的另一部分是函数式编程。正如 Java 5 中引入注解(annotations)带来了新的机遇(例如带注解的 REST 控制器或单元测试),Java 8 中引入 Lambda 表达式也为 Java 中的函数式 API 创造了机会。
这对于非阻塞应用程序和延续式(continuation-style)API 是一大利好(这类 API 由 CompletableFuture 和 ReactiveX 推广开来),它们允许以声明式的方式组合异步逻辑。在编程模型层面,Java 8 使 Spring WebFlux 能够在提供带注解的控制器的同时,也支持函数式 Web 端点。
1.1.1. 定义“响应式”
我们提到了“非阻塞”和“函数式”,但“响应式”到底是什么意思呢?
“响应式”(reactive)一词指的是围绕对变化作出反应而构建的编程模型——例如网络组件对 I/O 事件作出反应,UI 控制器对鼠标事件作出反应,等等。 从这个意义上说,非阻塞就是响应式的,因为此时我们不再处于阻塞状态,而是转变为在操作完成或数据可用时对通知作出反应的模式。
Spring 团队还将“响应式”与另一种重要机制联系在一起,即非阻塞背压(non-blocking back pressure)。在同步的命令式代码中,阻塞调用作为一种天然的背压形式,强制调用方等待。而在非阻塞代码中,控制事件速率就变得至关重要,以防止快速的生产者压垮其接收端。
Reactive Streams 是一个 小型规范 (也在 Java 9 中被采纳), 它定义了具有背压机制的异步组件之间的交互方式。 例如,一个数据仓库(作为 发布者(Publisher)) 可以生成数据,而 HTTP 服务器(作为 订阅者(Subscriber)) 则可以将这些数据写入响应中。Reactive Streams 的主要目的是让 订阅者能够控制发布者生成数据的速度快慢。
|
常见问题:如果发布者无法减速怎么办? Reactive Streams 的目的仅是建立机制和边界。 如果发布者无法减速,它必须决定是缓冲、丢弃还是失败。 |
1.1.2. 响应式 API
Reactive Streams 在互操作性方面起着重要作用。它对库和基础设施组件很有价值,但作为应用程序 API 却不太实用,因为其层级过低。应用程序需要一个更高级、更丰富的函数式 API 来组合异步逻辑——类似于 Java 8 的 Stream API,但不仅限于集合。
这正是响应式库所扮演的角色。
Reactor 是 Spring WebFlux 的首选响应式库。它提供了
Mono 和
Flux API 类型,
用于处理 0..1(Mono)和 0..N(Flux)的数据序列,并拥有丰富的操作符集合,这些操作符与
ReactiveX 的 操作符词汇表 保持一致。
Reactor 是一个响应式流(Reactive Streams)库,因此其所有操作符都支持非阻塞背压。
Reactor 专注于服务端 Java 开发,并与 Spring 紧密协作开发。
WebFlux 需要 Reactor 作为核心依赖,但通过 Reactive Streams 可与其他响应式库互操作。通常情况下,WebFlux API 接受一个普通的 Publisher 作为输入,在内部将其适配为 Reactor 类型并加以使用,然后返回 Flux 或 Mono 作为输出。因此,您可以传入任意 Publisher 作为输入,并对输出应用操作,但若要在其他响应式库中使用该输出,则需要对其进行适配。在可行的情况下(例如,带注解的控制器),WebFlux 能够透明地适配 RxJava 或其他响应式库的使用。更多详细信息,请参阅响应式库。
| 除了响应式(Reactive)API 之外,WebFlux 还可以与 Kotlin 中的协程(Coroutines) API 一起使用,从而提供一种更具命令式风格的编程方式。 以下 Kotlin 代码示例将使用协程 API 提供。 |
1.1.3. 编程模型
spring-web 模块包含作为 Spring WebFlux 基础的响应式核心,包括 HTTP 抽象、针对受支持服务器的响应式流 适配器、编解码器,以及一个核心的 WebHandler API,该 API 与 Servlet API 类似,但采用非阻塞契约。
在此基础上,Spring WebFlux 提供了两种编程模型供选择:
1.1.4. 适用性
Spring MVC 还是 WebFlux?
这是一个很自然的问题,但它却建立了一个不合理的二分法。实际上,两者协同工作,共同拓展了可用选项的范围。二者在设计上相互连贯、一致,可并行使用,并且来自任一方的反馈都能惠及双方。下图展示了二者之间的关系、它们的共同之处,以及各自独有的支持功能:
我们建议您考虑以下具体要点:
-
如果你有一个运行良好的 Spring MVC 应用程序,就没有必要进行更改。 命令式编程是编写、理解和调试代码最简单的方式。 你可以最大限度地选择各种库,因为从历史上看,大多数库都是阻塞式的。
-
如果你已经在寻找一个非阻塞的 Web 技术栈,Spring WebFlux 不仅能提供与其他同类框架相同的执行模型优势,还提供了多种服务器选择(Netty、Tomcat、Jetty、Undertow 和 Servlet 3.1+ 容器)、多种编程模型选择(注解式控制器和函数式 Web 端点),以及多种响应式库选择(Reactor、RxJava 或其他)。
-
如果你对一个轻量级、函数式的 Web 框架感兴趣,并希望在 Java 8 Lambda 表达式或 Kotlin 中使用,可以选择 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 上的并发机制)及其影响。
1.1.5. 服务器
Spring WebFlux 支持在 Tomcat、Jetty、Servlet 3.1+ 容器上运行,也支持在非 Servlet 运行环境(如 Netty 和 Undertow)上运行。所有服务器都被适配到一个底层的通用 API,以便跨服务器支持更高层次的编程模型。
Spring WebFlux 本身不提供启动或停止服务器的内置支持。但是,通过 Spring 配置和组装应用程序、WebFlux 基础设施,并用几行代码运行它,是非常容易的。
Spring Boot 提供了一个 WebFlux Starter,可自动完成这些步骤。默认情况下,该 Starter 使用 Netty,但只需更改您的 Maven 或 Gradle 依赖项,即可轻松切换到 Tomcat、Jetty 或 Undertow。Spring Boot 默认选择 Netty,是因为它在异步、非阻塞领域应用更为广泛,并且允许客户端和服务器共享资源。
Tomcat 和 Jetty 均可用于 Spring MVC 和 WebFlux。但请注意,它们的使用方式截然不同。Spring MVC 依赖于 Servlet 阻塞式 I/O,并允许应用程序在需要时直接使用 Servlet API。而 Spring WebFlux 则依赖于 Servlet 3.1 的非阻塞 I/O,并通过一个底层适配器来使用 Servlet API,该 API 不会暴露给开发者直接使用。
对于 Undertow,Spring WebFlux 直接使用 Undertow 的 API,而不通过 Servlet API。
1.1.6. 性能
性能具有多种特性和含义。响应式和非阻塞通常并不会让应用程序运行得更快。在某些情况下,它们可能会提升性能(例如,使用 WebClient 并行执行远程调用)。总体而言,采用非阻塞方式实现功能需要更多的工作量,并可能略微增加所需的处理时间。
响应式和非阻塞方式的主要预期优势在于,能够以少量且固定数量的线程以及更少的内存实现扩展。这使得应用程序在负载下更具弹性,因为它们能以更可预测的方式进行扩展。然而,要体现这些优势,系统中必须存在一定的延迟(包括较慢且不可预测的网络 I/O 混合情况)。正是在这种场景下,响应式技术栈开始展现出其优势,而且这种差异可能非常显著。
1.1.7. 并发模型
Spring MVC 和 Spring WebFlux 都支持注解式控制器,但在并发模型以及对阻塞和线程的默认假设方面存在一个关键区别。
在 Spring MVC(以及一般的 Servlet 应用程序)中,假定应用程序可以阻塞当前线程(例如,用于远程调用)。因此,Servlet 容器使用一个较大的线程池,以吸收请求处理过程中可能出现的阻塞。
在 Spring WebFlux(以及一般的非阻塞服务器)中,假定应用程序不会发生阻塞。因此,非阻塞服务器使用一个小型、固定大小的线程池(事件循环工作线程)来处理请求。
| “可扩展性”和“少量线程”听起来可能相互矛盾,但绝不阻塞当前线程(而是依赖回调)意味着你不需要额外的线程,因为根本不存在需要被吸收的阻塞调用。 |
如果你确实需要使用阻塞式库怎么办?Reactor 和 RxJava 都提供了 publishOn 操作符,以便在不同的线程上继续处理。这意味着存在一个简单的变通方案。但请记住,阻塞式 API 并不适合这种并发模型。
在 Reactor 和 RxJava 中,您通过操作符来声明逻辑。在运行时,会形成一个响应式管道,数据在其中按顺序、分阶段进行处理。这样做的一个关键优势在于,它使应用程序无需再保护可变状态,因为该管道中的应用代码永远不会被并发调用。
在运行 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 提供了称为调度器(schedulers)的线程池抽象,可与
publishOn操作符配合使用,以将处理切换到不同的线程池。 这些调度器具有暗示特定并发策略的名称——例如,“parallel”(用于 CPU 密集型任务,使用数量有限的线程)或“elastic”(用于 I/O 密集型任务,使用大量线程)。如果你看到此类线程,说明某些代码正在使用特定的线程池Scheduler策略。 -
数据访问库和其他第三方依赖项也可以创建并使用它们自己的线程。
1.2. 响应式核心
spring-web 模块包含以下对响应式 Web 应用程序的基础支持:
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:用于处理 HTTP 请求的基础契约,支持非阻塞 I/O 和响应式流(Reactive Streams)背压机制,并提供适配器以兼容 Reactor Netty、Undertow、Tomcat、Jetty 以及任意 Servlet 3.1+ 容器。
-
WebHandlerAPI:更高级别的通用 Web API,用于请求处理,基于此构建了注解控制器和函数式端点等具体编程模型。
-
-
在客户端方面,有一个基本的
ClientHttpConnector契约,用于通过非阻塞 I/O 和响应式流(Reactive Streams)背压机制执行 HTTP 请求,同时还提供了针对 Reactor Netty 和响应式的 Jetty HttpClient 的适配器。 应用程序中使用的高级 WebClient 正是构建于这一基本契约之上。 -
对于客户端和服务器,用于HTTP请求和响应内容的序列化与反序列化的编解码器。
1.2.1. HttpHandler
HttpHandler 是一个简单的契约,仅包含一个用于处理请求和响应的方法。它被有意设计得极为精简,其主要且唯一的目的就是对不同的 HTTP 服务器 API 提供一个最小化的抽象。
下表描述了所支持的服务器 API:
| 服务器名称 | 所使用的服务器 API | 响应式流支持 |
|---|---|---|
Netty |
Netty API |
|
Undertow |
Undertow API |
spring-web:Undertow 到响应式流(Reactive Streams)的桥接 |
Tomcat |
Servlet 3.1 非阻塞 I/O;Tomcat API 用于读写 ByteBuffer 与 byte[] |
spring-web:Servlet 3.1 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
Jetty |
Servlet 3.1 非阻塞 I/O;Jetty API 使用 ByteBuffer 而非 byte[] 进行写入 |
spring-web:Servlet 3.1 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
Servlet 3.1 容器 |
Servlet 3.1 非阻塞 I/O |
spring-web:Servlet 3.1 非阻塞 I/O 到响应式流(Reactive Streams)的桥接 |
下表描述了服务器依赖项(另请参阅支持的版本):
| 服务器名称 | 组 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 一起使用:
Reactor Netty
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
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();
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
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();
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
Servlet 3.1+ 容器
若要作为 WAR 部署到任何 Servlet 3.1+ 容器,您可以扩展并包含
AbstractReactiveWebInitializer
到 WAR 中。该类使用 ServletHttpHandlerAdapter 包装一个 HttpHandler,并将其注册为 Servlet。
1.2.2. WebHandlerAPI
org.springframework.web.server 包基于 HttpHandler 契约构建,提供了一个通用的 Web API,用于通过由多个 WebExceptionHandler、多个 WebFilter 以及单个 WebHandler 组件组成的链来处理请求。该链可以通过 WebHttpHandlerBuilder 进行组装,只需指向一个 Spring ApplicationContext(其中的组件会被 自动检测),和/或通过向构建器注册组件来完成。
虽然 HttpHandler 的目标很简单,即抽象化不同 HTTP 服务器的使用,但
WebHandler API 旨在提供更广泛的功能集,这些功能通常用于 Web 应用程序中,例如:
-
带有属性的用户会话。
-
请求属性。
-
为请求解析出的
Locale或Principal。 -
对已解析并缓存的表单数据的访问。
-
多部分数据的抽象。
-
以及更多……
特殊 Bean 类型
下表列出了 WebHttpHandlerBuilder 可以在 Spring ApplicationContext 中自动检测到的组件,或者可以直接向其注册的组件:
| Bean 名称 | Bean 类型 | 计数 | 描述 |
|---|---|---|---|
<any> |
|
0..N |
为来自 |
<any> |
|
0..N |
将拦截风格的逻辑应用于过滤器链其余部分以及目标 |
|
|
1 |
请求的处理器。 |
|
|
0..1 |
通过 |
|
|
0..1 |
用于访问 |
|
|
0..1 |
通过 |
|
|
0..1 |
用于处理转发类型(forwarded type)的头部信息,可以提取并移除这些头部,也可以仅移除它们。 默认情况下不使用。 |
表单数据
ServerWebExchange 提供了以下方法用于访问表单数据:
Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>
DefaultServerWebExchange 使用配置好的 HttpMessageReader 将表单数据(application/x-www-form-urlencoded)解析为一个 MultiValueMap。默认情况下,FormHttpMessageReader 由 ServerCodecConfigurer bean 进行配置(参见Web Handler API)。
多部分数据
ServerWebExchange 提供了以下方法用于访问多部分(multipart)数据:
Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>
DefaultServerWebExchange 使用已配置的
HttpMessageReader<MultiValueMap<String, Part>> 将 multipart/form-data 内容
解析为一个 MultiValueMap。目前,
Synchronoss NIO Multipart 是唯一受支持的
第三方库,也是我们所知的唯一可用于非阻塞解析
multipart 请求的库。它通过 ServerCodecConfigurer bean 启用
(参见 Web Handler API)。
要以流式方式解析 multipart 数据,您可以改用从 Flux<Part> 返回的 HttpMessageReader<Part>。例如,在带注解的控制器中,使用 @RequestPart 意味着通过名称对各个部分进行类似 Map 的访问,因此需要完整地解析 multipart 数据。相比之下,您可以使用 @RequestBody 将内容解码为 Flux<Part>,而无需将其收集到 MultiValueMap 中。
转发的请求头
当请求经过代理(例如负载均衡器)时,主机、端口和协议方案可能会发生变化。这从客户端的角度来看,使得创建指向正确主机、端口和协议方案的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 头部,
代理可以使用该头部提供有关原始请求的信息。此外,还存在其他
非标准的头部,包括 X-Forwarded-Host、X-Forwarded-Port、
X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderTransformer 是一个组件,它根据转发头(forwarded headers)修改请求的主机、端口和协议(scheme),然后移除这些头信息。如果你将其声明为名为 forwardedHeaderTransformer 的 Bean,它将被自动检测并使用。
转发头(forwarded headers)存在安全方面的考虑,因为应用程序无法判断这些头是由代理按预期添加的,还是由恶意客户端伪造的。因此,在信任边界处的代理应配置为移除来自外部的不可信转发流量。你也可以将 ForwardedHeaderTransformer 配置为 removeOnly=true,在这种情况下,它会移除这些头但不会使用它们。
在 5.1 版本中,ForwardedHeaderFilter 已被弃用,并由 ForwardedHeaderTransformer 取代,以便在交换(exchange)创建之前更早地处理转发头(forwarded headers)。如果仍然配置了该过滤器,它将从过滤器列表中移除,并改用 ForwardedHeaderTransformer。 |
1.2.3. 过滤器
在 WebHandler API 中,您可以使用 WebFilter 在过滤器链和目标 WebHandler 的其余处理流程之前和之后应用拦截式逻辑。当使用 WebFlux 配置 时,注册 WebFilter 非常简单,只需将其声明为 Spring Bean,并(可选地)通过在 Bean 声明上使用 @Order 或实现 Ordered 来指定优先级。
CORS(跨域资源共享)
Spring WebFlux 通过控制器上的注解提供了细粒度的 CORS 配置支持。然而,当你将其与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,该过滤器必须排在 Spring Security 过滤器链之前。
有关更多详细信息,请参阅CORS部分和webflux-cors.html。
1.2.4. 异常
在 WebHandler API 中,您可以使用 WebExceptionHandler 来处理来自 WebFilter 实例链和目标 WebHandler 的异常。当使用 WebFlux 配置 时,注册 WebExceptionHandler 非常简单,只需将其声明为 Spring Bean,并通过在 Bean 声明上使用 @Order 或实现 Ordered 来(可选地)表达优先级即可。
下表描述了可用的 WebExceptionHandler 实现:
| 异常处理器 | 描述 |
|---|---|
|
提供对类型为
|
|
此处理器在WebFlux 配置中声明。 |
1.2.5. 编解码器
spring-web 和 spring-core 模块通过带有响应式流(Reactive Streams)背压机制的非阻塞 I/O,提供了将字节内容与高层对象之间进行序列化和反序列化的支持。以下内容描述了该支持:
-
HttpMessageReader和HttpMessageWriter是用于编码和解码 HTTP 消息内容的契约。 -
可以使用
Encoder包装一个EncoderHttpMessageWriter,以使其适用于 Web 应用程序;而Decoder则可以使用DecoderHttpMessageReader进行包装。 -
DataBuffer抽象了不同的字节缓冲区表示形式(例如 NettyByteBuf、java.nio.ByteBuffer等),是所有编解码器操作的基础。有关此主题的更多详细信息,请参阅“Spring Core”部分中的 数据缓冲区与编解码器。
spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource 和
String 的编码器和解码器实现。spring-web 模块则提供了 Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers 等编码器和解码器,以及仅用于 Web 的 HTTP 消息读取器和写入器实现,用于处理表单数据、多部分(multipart)内容、服务器发送事件(server-sent events)等。
ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和自定义应用程序中使用的编解码器。请参阅HTTP 消息编解码器的配置部分。
Jackson JSON
当 Jackson 库存在时,JSON 和二进制 JSON(Smile)均受支持。
Jackson2Decoder 的工作方式如下:
-
Jackson 的异步、非阻塞解析器用于将字节块流聚合为多个
TokenBuffer,每个1代表一个 JSON 对象。 -
每个
TokenBuffer都会被传递给 Jackson 的ObjectMapper,以创建一个更高层次的对象。 -
当解码为单值发布者(例如
Mono)时,只有一个TokenBuffer。 -
在解码为多值发布者(例如
Flux)时,一旦接收到足够构成一个完整对象的字节,每个TokenBuffer就会立即传递给ObjectMapper。输入内容可以是一个 JSON 数组,或者如果内容类型为https://en.wikipedia.org/wiki/JSON_streaming,则可以是行分隔的 JSON。
Jackson2Encoder 的工作方式如下:
-
对于单值发布者(例如
Mono),只需通过ObjectMapper将其序列化即可。 -
对于具有
application/json类型的多值发布者,默认使用Flux#collectToList()收集所有值,然后对生成的集合进行序列化。 -
对于具有流式媒体类型(例如
application/stream+json或application/stream+x-jackson-smile)的多值发布者,请使用换行分隔的 JSON 格式对每个值分别进行编码、写入和刷新。 -
对于 SSE,每次事件都会调用
Jackson2Encoder,并且会刷新输出以确保及时传递。
|
默认情况下, |
表单数据
FormHttpMessageReader 和 FormHttpMessageWriter 支持解码和编码
application/x-www-form-urlencoded 内容。
在服务器端,当需要从多个位置访问表单内容时,
ServerWebExchange 提供了一个专用的 getFormData() 方法,该方法通过 FormHttpMessageReader 解析内容,
然后缓存结果以便重复访问。
请参阅 表单数据,位于 WebHandler API 部分。
一旦使用了 getFormData(),就无法再从请求体中读取原始的原始内容。因此,应用程序应始终通过 ServerWebExchange 来访问缓存的表单数据,而不是直接读取原始请求体。
文件上传
MultipartHttpMessageReader 和 MultipartHttpMessageWriter 支持对 "multipart/form-data" 内容进行解码和编码。其中,MultipartHttpMessageReader 会委托另一个 HttpMessageReader 来实际解析内容为 Flux<Part>,然后仅将这些部分收集到一个 MultiValueMap 中。目前,实际的解析工作使用的是 Synchronoss NIO Multipart。
在服务器端,当需要从多个位置访问多部分表单内容时,ServerWebExchange 提供了专用的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以供重复访问。
请参阅 WebHandler API 章节中的 多部分数据。
一旦使用了 getMultipartData(),就无法再从请求体中读取原始的原始内容。因此,应用程序必须始终一致地使用 getMultipartData() 来实现对各部分(parts)的重复、类似 Map 的访问;否则,应依赖 SynchronossPartHttpMessageReader 对 Flux<Part> 进行一次性访问。
限制
可以为那些对部分或全部输入流进行缓冲的 Decoder 和 HttpMessageReader 实现配置一个内存中缓冲字节数的最大限制。
在某些情况下,缓冲的发生是因为输入被聚合并表示为单个对象——例如,带有 @RequestBody byte[] 的控制器方法、x-www-form-urlencoded 数据等。
在流式处理场景中,当对输入流进行拆分时(例如,分隔符分隔的文本、JSON 对象流等),也会发生缓冲。
对于这些流式处理的情况,该限制适用于流中单个对象所关联的字节数。
要配置缓冲区大小,您可以检查给定的 Decoder 或 HttpMessageReader 是否公开了 maxInMemorySize 属性,如果公开了该属性,其 Javadoc 中将包含有关默认值的详细信息。在服务器端,ServerCodecConfigurer 提供了一个统一的位置来设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,所有编解码器的限制可以在 WebClient.Builder 中进行修改。
对于多部分(Multipart)解析,maxInMemorySize 属性用于限制非文件部分的大小。对于文件部分,该属性决定了将该部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个额外的 maxDiskUsagePerPart 属性,用于限制每个部分所占用的磁盘空间。此外,还有一个 maxParts 属性,用于限制多部分请求中各部分的总数量。
在 WebFlux 中配置上述三个属性时,你需要向 MultipartHttpMessageReader 提供一个预先配置好的 ServerCodecConfigurer 实例。
流式处理
在向 HTTP 响应进行流式传输时(例如 text/event-stream、
application/stream+json),定期发送数据非常重要,以便能够更早而非更晚地可靠检测到客户端断开连接。这种发送可以是一个仅包含注释的空 SSE 事件,或者任何其他实际上可作为心跳机制的“无操作”(no-op)数据。
DataBuffer
DataBuffer 是 WebFlux 中字节缓冲区的表示形式。本参考文档的 Spring Core 部分在数据缓冲区与编解码器一节中对此有更详细的介绍。需要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是池化的,并且采用引用计数机制,因此在使用完毕后必须释放,以避免内存泄漏。
WebFlux 应用程序通常无需关注此类问题,除非它们直接消费或生成数据缓冲区,而不是依赖编解码器(codecs)将数据转换为高层对象或从高层对象转换而来,或者除非它们选择创建自定义编解码器。对于这些情况,请参阅数据缓冲区与编解码器中的相关信息,特别是使用 DataBuffer一节。
1.2.6. 日志记录
DEBUG 级别的日志记录在 Spring WebFlux 中设计得简洁、精炼且对人类友好。它聚焦于那些反复有用的关键信息,而非仅在调试特定问题时才有用的其他信息。
TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,也不应像“消防水带”一样输出大量日志),但可用于调试任何问题。此外,某些日志消息在 TRACE 级别下可能会比在 DEBUG 级别下显示更详细的细节。
良好的日志记录源于使用日志的经验。如果您发现任何不符合所述目标的内容,请告知我们。
日志 ID
在 WebFlux 中,单个请求可能会在多个线程上执行,因此线程 ID 对于关联属于特定请求的日志消息并无帮助。这就是为什么 WebFlux 的日志消息默认会加上一个请求专属的 ID 作为前缀。
在服务器端,日志 ID 存储在 ServerWebExchange 属性中
(LOG_ID_ATTRIBUTE),
而基于该 ID 的完整格式化前缀可从
ServerWebExchange#getLogPrefix() 获取。在 WebClient 端,日志 ID 存储在
ClientRequest 属性中
(LOG_ID_ATTRIBUTE)
,而完整的格式化前缀可从 ClientRequest#logPrefix() 获取。
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息。因此,表单参数和请求头默认会被屏蔽,您必须显式启用才能完整记录它们。
以下示例展示了如何对服务器端请求进行此操作:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}
以下示例展示了如何对客户端请求进行此操作:
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()
自定义编解码器
应用程序可以注册自定义编解码器,以支持额外的媒体类型,或实现默认编解码器不支持的特定行为。
以下示例展示了如何对客户端请求进行此操作:
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
1.3. DispatcherHandler
Spring WebFlux 与 Spring MVC 类似,围绕前端控制器模式进行设计,其中由一个中心 WebHandler(即 DispatcherHandler)提供通用的请求处理算法,而实际工作则由可配置的委托组件来完成。
该模型具有良好的灵活性,能够支持多种工作流程。
DispatcherHandler 从 Spring 配置中发现其所需的委托组件。
它本身也被设计为一个 Spring Bean,并实现了 ApplicationContextAware,
以便访问其运行所在的上下文。如果 DispatcherHandler 被声明为名为 webHandler 的 Bean,
那么它反过来会被 WebHttpHandlerBuilder 发现,
后者会组装一条请求处理链,正如 WebHandler API 中所述。
WebFlux 应用程序中的 Spring 配置通常包含:
-
DispatcherHandler,其 bean 名称为webHandler -
WebFilter和WebExceptionHandlerBean -
其他
该配置被提供给 WebHttpHandlerBuilder 以构建处理链,如下例所示:
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
生成的 HttpHandler 已准备好与服务器适配器一起使用。
1.3.1. 特殊 Bean 类型
DispatcherHandler 将请求委托给特殊的 Bean 进行处理,并渲染相应的响应。这里的“特殊 Bean”指的是由 Spring 管理的、实现了 WebFlux 框架契约的 Object 实例。这些 Bean 通常带有内置的契约,但你可以自定义它们的属性、对其进行扩展或替换。
下表列出了由 DispatcherHandler 检测到的特殊 Bean。请注意,
还有一些其他 Bean 在更低的层级被检测到(参见 Web Handler API 中的
特殊 Bean 类型)。
| Bean 类型 | 说明 |
|---|---|
|
将请求映射到一个处理器。该映射基于某些条件,其具体细节因 主要的 |
|
帮助 |
|
处理处理器调用的结果并完成响应。 参见结果处理。 |
1.3.2. WebFlux 配置
应用程序可以声明处理请求所需的基础设施 Bean(列在
Web Handler API 和
DispatcherHandler 下)。
然而,在大多数情况下,WebFlux 配置 是最佳的起点。它会声明所需的 Bean,并提供更高级别的配置回调 API 以便进行自定义。
| Spring Boot 依赖 WebFlux 配置来设置 Spring WebFlux,并且还提供了许多额外的便捷选项。 |
1.3.3. 处理
DispatcherHandler 按如下方式处理请求:
-
每个
HandlerMapping都会被要求查找一个匹配的处理器,第一个匹配的结果将被使用。 -
如果找到了处理器,它将通过一个合适的
HandlerAdapter执行,该适配器会将执行的返回值以HandlerResult的形式暴露出来。 -
HandlerResult会被传递给相应的HandlerResultHandler,以通过直接写入响应或使用视图进行渲染来完成处理。
1.3.4. 结果处理
通过 HandlerAdapter 调用处理器所返回的值,会连同一些额外的上下文信息一起被包装为一个 HandlerResult,并传递给第一个声明支持该结果类型的 HandlerResultHandler。下表列出了可用的 HandlerResultHandler 实现,它们均在WebFlux 配置中声明:
| 结果处理器类型 | 返回值 | 默认顺序 |
|---|---|---|
|
|
0 |
|
|
0 |
|
处理来自 |
100 |
|
另请参阅 视图解析。 |
|
1.3.5. 异常
从 HandlerResult 返回的 HandlerAdapter 可以基于某种处理器特定的机制公开一个用于错误处理的函数。在以下情况下会调用此错误函数:
-
处理器(例如,
@Controller)调用失败。 -
通过
HandlerResultHandler处理处理器返回值时失败。
只要在处理程序返回的响应式类型生成任何数据项之前发生错误信号,错误函数就可以更改响应(例如,将其更改为错误状态)。
这是 @ExceptionHandler 类中 @Controller 方法的实现方式。
相比之下,Spring MVC 中对相同功能的支持是基于 HandlerExceptionResolver 构建的。
这通常不会产生影响。但请注意,在 WebFlux 中,您无法使用
@ControllerAdvice 来处理在选择处理器之前发生的异常。
1.3.6. 视图解析
视图解析使您能够使用 HTML 模板和模型向浏览器渲染内容,而无需绑定到特定的视图技术。在 Spring WebFlux 中,视图解析通过一个专用的 HandlerResultHandler 来支持,该处理器使用 ViewResolver 实例将一个字符串(代表逻辑视图名称)映射到一个 View 实例。View 随后被用于渲染响应。
处理
传入 HandlerResult 的 ViewResolutionResultHandler 包含处理器的返回值以及在请求处理过程中添加了属性的模型。该返回值将按以下其中一种方式进行处理:
-
String、CharSequence:一个逻辑视图名称,将通过已配置的View实现列表解析为一个ViewResolver。 -
void:根据请求路径(去除开头和结尾的斜杠)选择一个默认视图名称,并将其解析为一个View。当未提供视图名称(例如,返回了模型属性)或异步返回值为空(例如,Mono完成但无内容)时,也会发生同样的情况。 -
渲染:用于视图解析场景的 API。使用 IDE 中的代码补全功能探索可用选项。
-
Model、Map:要为请求添加到模型中的额外模型属性。 -
其他任意类型:任何其他返回值(除了由 BeanUtils#isSimpleProperty 判定的简单类型外)都将被视为模型属性,并添加到模型中。属性名称将根据类名按照约定自动生成,除非处理方法上存在
@ModelAttribute注解。
模型可以包含异步响应式类型(例如来自 Reactor 或 RxJava 的类型)。在渲染之前,AbstractView 会将此类模型属性解析为具体值并更新模型。单值响应式类型会被解析为一个单一值或无值(如果为空),而多值响应式类型(例如 Flux<T>)则会被收集并解析为 List<T>。
配置视图解析非常简单,只需在您的 Spring 配置中添加一个 ViewResolutionResultHandler Bean 即可。WebFlux 配置 提供了专门用于视图解析的配置 API。
有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术。
正在重定向
视图名称中的特殊 redirect: 前缀可让你执行重定向。UrlBasedViewResolver(及其子类)会将此识别为需要执行重定向的指令。视图名称的其余部分即为重定向 URL。
其最终效果与控制器返回 RedirectView 或
Rendering.redirectTo("abc").build() 相同,但现在控制器本身可以使用逻辑视图名称进行操作。像
redirect:/some/resource 这样的视图名称是相对于当前应用程序的,而像
redirect:https://example.com/arbitrary/path 这样的视图名称则会重定向到一个绝对 URL。
内容协商
ViewResolutionResultHandler 支持内容协商。它将请求的媒体类型与每个选定的 View 所支持的媒体类型进行比较。第一个支持所请求媒体类型的 View 将被使用。
为了支持 JSON 和 XML 等媒体类型,Spring WebFlux 提供了 HttpMessageWriterView,这是一种特殊的 View,它通过 HttpMessageWriter 进行渲染。通常,你会通过 WebFlux 配置 将这些视图配置为默认视图。如果默认视图与请求的媒体类型匹配,它们将始终被选中并使用。
1.4. 带注解的控制器
Spring WebFlux 提供了一种基于注解的编程模型,其中 @Controller 和
@RestController 组件使用注解来表达请求映射、请求输入、
异常处理等功能。带注解的控制器具有灵活的方法签名,
无需继承基类,也无需实现特定接口。
以下列表展示了一个基本示例:
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
@RestController
class HelloController {
@GetMapping("/hello")
fun handle() = "Hello WebFlux"
}
在前面的示例中,该方法返回一个String,用于写入响应体。
1.4.1. @Controller
你可以使用标准的 Spring Bean 定义来定义控制器 Bean。
@Controller 刻板注解(stereotype)支持自动检测,并与 Spring 对在类路径中检测 @Component 类并自动注册其 Bean 定义的通用支持保持一致。它还作为被注解类的刻板注解,表明该类作为 Web 组件的角色。
要启用对此类 @Controller bean 的自动检测,您可以将组件扫描添加到您的 Java 配置中,如下例所示:
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {
// ...
}
| 1 | 扫描 org.example.web 包。 |
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {
// ...
}
| 1 | 扫描 org.example.web 包。 |
@RestController 是一个组合注解,其本身通过元注解方式同时标注了 @Controller 和 @ResponseBody,表示该控制器的所有方法都继承了类级别的 @ResponseBody 注解,因此会直接将内容写入响应体,而不是通过视图解析并使用 HTML 模板进行渲染。
1.4.2. 请求映射
@RequestMapping 注解用于将请求映射到控制器方法。它提供了多种属性,可用于根据 URL、HTTP 方法、请求参数、请求头和媒体类型进行匹配。你可以在类级别使用该注解以定义共享的映射,也可以在方法级别使用以进一步细化到特定的端点映射。
此外,还有针对特定 HTTP 方法的 @RequestMapping 快捷变体:
-
@GetMapping -
@PostMapping -
@PutMapping -
@DeleteMapping -
@PatchMapping
上述注解是自定义注解,之所以提供这些注解,是因为可以说大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用默认情况下匹配所有 HTTP 方法的@RequestMapping。同时,在类级别上仍然需要使用@RequestMapping来表达共享的映射。
以下示例使用了类型级别和方法级别的映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式
你可以使用通配符模式(glob patterns)和通配符来映射请求:
| 模式 | 描述 | 例举 |
|---|---|---|
|
匹配一个字符 |
|
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径结束 |
|
|
匹配一个路径段,并将其捕获为名为“name”的变量 |
|
|
将正则表达式 |
|
|
匹配零个或多个路径段,直到路径结束,并将其捕获为名为“path”的变量 |
|
可以使用 @PathVariable 访问捕获的 URI 变量,如下例所示:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
您可以在类级别和方法级别声明 URI 变量,如下例所示:
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {
@GetMapping("/pets/{petId}") (2)
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
| 1 | 类级别的 URI 映射。 |
| 2 | 方法级别的 URI 映射。 |
@Controller
@RequestMapping("/owners/{ownerId}") (1)
class OwnerController {
@GetMapping("/pets/{petId}") (2)
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
| 1 | 类级别的 URI 映射。 |
| 2 | 方法级别的 URI 映射。 |
URI 变量会自动转换为适当的类型,否则会抛出 TypeMismatchException。默认支持简单类型(int、long、Date 等),您也可以注册对其他任何数据类型的支持。
请参阅 类型转换 和 DataBinder。
URI 变量可以显式命名(例如,@PathVariable("customId")),但如果变量名称相同,并且您在编译代码时包含调试信息,或者在 Java 8 中使用 -parameters 编译器标志,则可以省略这一细节。
语法 {*varName} 声明一个 URI 变量,用于匹配零个或多个剩余的路径段。例如,/resources/{*path} 会匹配 /resources/ 目录下的所有文件,且 "path" 变量会捕获完整的相对路径。
语法 {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) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式还可以包含嵌入的 ${…} 占位符,这些占位符在启动时通过 PropertyPlaceHolderConfigurer 从本地、系统、环境以及其他属性源中进行解析。例如,您可以利用此功能根据某些外部配置对基础 URL 进行参数化。
Spring WebFlux 使用 PathPattern 和 PathPatternParser 来提供 URI 路径匹配支持。
这两个类都位于 spring-web 模块中,专为在 Web 应用程序中处理 HTTP URL 路径而设计,适用于运行时需要匹配大量 URI 路径模式的场景。 |
Spring WebFlux 不支持后缀模式匹配 —— 与 Spring MVC 不同,在 Spring MVC 中,像 /person 这样的映射也会匹配 /person.*。如果需要基于 URL 的内容协商,我们建议使用查询参数,这种方式更简单、更明确,并且不易受到基于 URL 路径的攻击。
模式对比
当多个模式匹配同一个 URL 时,必须对它们进行比较以找出最佳匹配。这是通过 PathPattern.SPECIFICITY_COMPARATOR 实现的,它会寻找更加具体的模式。
对于每个模式,都会根据 URI 变量和通配符的数量计算一个得分,其中 URI 变量的得分低于通配符。总得分较低的模式胜出。如果两个模式得分相同,则选择较长的那个。
通配模式(例如,**、{*varName})不参与评分,并始终排在最后。如果两个模式都是通配模式,则选择较长的那个。
可消费的媒体类型
您可以根据请求的 Content-Type 来缩小请求映射的范围,如下例所示:
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
// ...
}
!text/plain 属性也支持否定表达式——例如,text/plain 表示除 2 之外的任何内容类型。
你可以在类级别声明一个共享的 consumes 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 consumes 属性会覆盖而非扩展类级别的声明。
MediaType 提供了常用媒体类型的常量——例如,
APPLICATION_JSON_VALUE 和 APPLICATION_XML_VALUE。 |
可生成的媒体类型
您可以根据 Accept 请求头以及控制器方法所生成的内容类型列表来缩小请求映射的范围,如下例所示:
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
// ...
}
媒体类型可以指定字符集。支持否定表达式——例如,!text/plain 表示除 text/plain 之外的任何内容类型。
你可以在类级别声明一个共享的 produces 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。
MediaType 提供了常用媒体类型的常量——例如
APPLICATION_JSON_VALUE、APPLICATION_XML_VALUE。 |
参数和请求头
您可以根据查询参数条件来缩小请求映射的范围。您可以测试某个查询参数是否存在(myParam)、是否不存在(!myParam),或者是否具有特定值(myParam=myValue)。以下示例测试的是一个带有特定值的参数:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
| 1 | 检查 myParam 是否等于 myValue。 |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
| 1 | 检查 myParam 是否等于 myValue。 |
你也可以将其与请求头条件一起使用,如下例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
| 1 | 检查 myHeader 是否等于 myValue。 |
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
| 1 | 检查 myHeader 是否等于 myValue。 |
HTTP 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,但在一般情况下并不需要这样做。
自定义注解
Spring WebFlux 支持使用组合注解进行请求映射。这些注解本身使用 @RequestMapping 进行元注解,并通过组合方式重新声明 @RequestMapping 的部分(或全部)属性,以实现更窄、更具体的目的。
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping 和 @PatchMapping 是组合注解的示例。提供这些注解是因为,可以说,大多数控制器方法应当映射到特定的 HTTP 方法,而不是使用默认匹配所有 HTTP 方法的 @RequestMapping。如果你需要组合注解的示例,可以查看这些注解的声明方式。
Spring WebFlux 还支持使用自定义请求匹配逻辑的自定义请求映射属性。这是一种更高级的选项,需要继承 RequestMappingHandlerMapping 类并重写 getCustomMethodCondition 方法,在该方法中你可以检查自定义属性并返回你自己的 RequestCondition。
显式注册
您可以以编程方式注册处理方法(Handler methods),这可用于动态注册或高级场景,例如在不同 URL 下注册同一处理程序的不同实例。以下示例展示了如何实现这一点:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
| 1 | 注入目标处理器和控制器的处理器映射。 |
| 2 | 准备请求映射元数据。 |
| 3 | 获取处理方法。 |
| 4 | 添加注册信息。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
| 1 | 注入目标处理器和控制器的处理器映射。 |
| 2 | 准备请求映射元数据。 |
| 3 | 获取处理方法。 |
| 4 | 添加注册信息。 |
1.4.3. 处理方法
@RequestMapping 处理方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。
方法参数
下表列出了支持的控制器方法参数。
对于需要阻塞 I/O(例如,读取请求体)才能解析的参数,支持响应式类型(Reactor、RxJava 或其他)。这一点已在“描述”列中标明。对于不需要阻塞的参数,则不应使用响应式类型。
JDK 1.8 的 java.util.Optional 可作为方法参数使用,与带有 required 属性的注解(例如 @RequestParam、@RequestHeader 等)结合使用时,等同于设置 required=false。
| 控制器方法参数 | 描述 |
|---|---|
|
访问完整的 |
|
访问 HTTP 请求或响应。 |
|
访问会话。除非添加了属性,否则不会强制启动新会话。支持响应式类型。 |
|
当前已认证的用户——如果已知,可能是某个特定的 |
|
请求的 HTTP 方法。 |
|
当前请求的区域设置,由可用的最具体的 |
|
由 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的键值对。请参阅矩阵变量。 |
|
用于访问 Servlet 请求参数。参数值将转换为声明的方法参数类型。参见 请注意,使用 |
|
用于访问请求头。请求头的值会被转换为声明的方法参数类型。请参阅 |
|
用于访问 Cookie。Cookie 值将转换为声明的方法参数类型。
请参阅 |
|
用于访问 HTTP 请求体。请求体内容会通过 |
|
用于访问请求头和请求体。请求体会通过 |
|
用于访问 |
|
用于访问在 HTML 控制器中使用的模型,该模型作为视图渲染的一部分暴露给模板。 |
|
用于访问模型中现有的属性(若不存在则实例化),并应用数据绑定和验证。另请参阅 请注意,使用 |
|
用于访问命令对象(即 |
|
用于标记表单处理完成,这将触发清理通过类级别 |
|
用于根据当前请求的主机、端口、协议和路径来准备一个相对 URL。 参见 URI 链接。 |
|
用于访问任何会话属性——这与由于类级别的 |
|
用于访问请求属性。详见 |
任何其他参数 |
如果一个方法参数未匹配上述任何情况,则默认情况下,若该参数是简单类型(由 BeanUtils#isSimpleProperty 判定),则解析为 |
返回值
下表列出了支持的控制器方法返回值。请注意,来自 Reactor、RxJava 或其他 等库的响应式类型通常适用于所有返回值。
| 控制器方法的返回值 | 描述 |
|---|---|
|
返回值通过 |
|
返回值指定完整的响应,包括 HTTP 头,主体通过 |
|
用于返回带有响应头但无响应体的响应。 |
|
一个视图名称,将通过 |
|
一个用于渲染的 |
|
要添加到隐式模型中的属性,视图名称将根据请求路径隐式确定。 |
|
一个要添加到模型中的属性,其视图名称将根据请求路径隐式确定。 请注意, |
|
用于模型和视图渲染场景的 API。 |
|
如果一个方法的返回类型为 如果以上情况均不成立, |
|
发送服务器发送事件(Server-Sent Events)。当仅需写入数据时,可以省略 |
任何其他返回值 |
如果返回值未匹配上述任何一种情况,则默认情况下,若其为 |
类型转换
某些表示基于字符串的请求输入的注解控制器方法参数(例如,@RequestParam、@RequestHeader、@PathVariable、@MatrixVariable 和 @CookieValue)
在参数声明类型不是 String 时,可能需要进行类型转换。
对于此类情况,类型转换会根据配置的转换器自动应用。
默认情况下,支持简单类型(如 int、long、Date 等)。类型转换可以通过 WebDataBinder 进行自定义(参见 DataBinder),或者通过向 FormattingConversionService 注册 Formatters 来实现(参见 Spring 字段格式化)。
矩阵变量
RFC 3986 讨论了路径段中的名称-值对。在 Spring WebFlux 中,我们根据 Tim Berners-Lee 的“一篇旧帖”将这些称为“矩阵变量”,但它们也可以被称为 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 /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// 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
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
您可以将矩阵变量定义为可选,并指定一个默认值,如下例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// 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]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
你可以使用 @RequestParam 注解将查询参数绑定到控制器中的方法参数上。以下代码片段展示了其用法:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
| 1 | 使用 @RequestParam。 |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
| 1 | 使用 @RequestParam。 |
Servlet API 中的“请求参数”(request parameter)概念将查询参数、表单数据和 multipart 数据混为一谈。然而在 WebFlux 中,每种数据类型都通过 ServerWebExchange 单独访问。虽然 @RequestParam 仅绑定到查询参数,但你可以使用数据绑定将查询参数、表单数据和 multipart 数据应用到命令对象上。 |
默认情况下,使用 @RequestParam 注解的方法参数是必需的,但你可以通过将 @RequestParam 的 required 标志设置为 false,或使用 java.util.Optional 包装器声明参数,来指定该方法参数是可选的。
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
当在 @RequestParam 注解声明于 Map<String, String> 或
MultiValueMap<String, String> 类型的参数上时,该 Map 将被填充所有查询参数。
请注意,使用 @RequestParam 是可选的——例如,用于设置其属性。默认情况下,任何简单值类型(由 BeanUtils#isSimpleProperty 判定)且未被其他参数解析器解析的参数,都会被当作已使用 @RequestParam 注解处理。
@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)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
| 1 | 获取 Accept-Encoging 请求头的值。 |
| 2 | 获取 Keep-Alive 请求头的值。 |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
| 1 | 获取 Accept-Encoging 请求头的值。 |
| 2 | 获取 Keep-Alive 请求头的值。 |
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
当在 @RequestHeader 注解用于 Map<String, String>、
MultiValueMap<String, String> 或 HttpHeaders 类型的参数时,该映射将被填充所有请求头的值。
内置支持将逗号分隔的字符串转换为字符串数组或集合,或其他类型转换系统已知的类型。例如,使用 @RequestHeader("Accept") 注解的方法参数可以是 String 类型,也可以是 String[] 或 List<String> 类型。 |
@CookieValue
你可以使用 @CookieValue 注解将 HTTP Cookie 的值绑定到控制器中的方法参数上。
以下示例展示了一个包含 Cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下代码示例演示了如何获取 Cookie 的值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
| 1 | 获取 Cookie 的值。 |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
| 1 | 获取 Cookie 的值。 |
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
@ModelAttribute
你可以使用 @ModelAttribute 注解标注方法参数,以访问模型中的属性;如果该属性不存在,则会自动实例化。模型属性还会被查询参数和表单字段的值覆盖,前提是这些参数或字段的名称与属性的字段名相匹配。这被称为数据绑定(data binding),它免去了你手动解析和转换各个查询参数及表单字段的麻烦。以下示例将一个 Pet 实例进行绑定:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
| 1 | 绑定一个 Pet 的实例。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
| 1 | 绑定一个 Pet 的实例。 |
前面示例中的 Pet 实例解析方式如下:
-
来自模型(如果已通过
Model添加)。 -
从 HTTP 会话通过
@SessionAttributes。 -
通过调用默认构造函数。
-
通过调用一个“主构造函数”来实现,该构造函数的参数与查询参数或表单字段相匹配。参数名称通过 JavaBeans 的
@ConstructorProperties注解确定,或者通过字节码中保留的运行时参数名称确定。
获取模型属性实例后,将应用数据绑定。WebExchangeDataBinder 类会将查询参数和表单字段的名称与目标 Object 上的字段名称进行匹配。在必要时应用类型转换后,匹配的字段将被填充。有关数据绑定(及验证)的更多信息,请参阅 验证。有关自定义数据绑定的更多信息,请参阅 DataBinder。
数据绑定可能会导致错误。默认情况下,会抛出一个 WebExchangeBindException 异常,但若要在控制器方法中检查此类错误,可以在 BindingResult 参数后立即添加一个 @ModelAttribute 参数,如下例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | 添加一个 BindingResult。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
| 1 | 添加一个 BindingResult。 |
你可以在数据绑定之后通过添加 javax.validation.Valid 注解或 Spring 的 @Validated 注解(另见
Bean Validation 和
Spring 验证)来自动应用验证。以下示例使用了 @Valid 注解:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | 在模型属性参数上使用 @Valid。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
| 1 | 在模型属性参数上使用 @Valid。 |
与 Spring MVC 不同,Spring WebFlux 在模型中支持响应式类型(reactive types)——例如,Mono<Account> 或 io.reactivex.Single<Account>。您可以声明一个带有或不带响应式类型包装器的 @ModelAttribute 参数,系统会相应地解析该参数,并在必要时解析为实际值。然而请注意,若要使用 BindingResult 参数,您必须像前面所示那样,在其之前声明不带响应式类型包装器的 @ModelAttribute 参数。或者,您也可以通过响应式类型来处理任何错误,如下例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
return petMono
.flatMap { pet ->
// ...
}
.onErrorResume{ ex ->
// ...
}
}
请注意,使用 @ModelAttribute 是可选的——例如,用于设置其属性。
默认情况下,任何不是简单值类型(由
BeanUtils#isSimpleProperty
判定)且未被其他任何参数解析器解析的参数,都会被视为带有 @ModelAttribute 注解。
@SessionAttributes
@SessionAttributes 用于在请求之间将模型属性存储在 WebSession 中。它是一个类级别的注解,用于声明特定控制器所使用的会话属性。通常,该注解列出模型属性的名称或类型,这些属性应透明地存储在会话中,以便后续请求访问。
考虑以下示例:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
| 1 | 使用 @SessionAttributes 注解。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
| 1 | 使用 @SessionAttributes 注解。 |
在第一次请求时,当一个名为 pet 的模型属性被添加到模型中时,
它会自动提升并保存到 WebSession 中。
该属性将一直保留在会话中,直到另一个控制器方法使用 SessionStatus 方法参数来清除存储,
如下例所示:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete();
// ...
}
}
}
| 1 | 使用 @SessionAttributes 注解。 |
| 2 | 使用 SessionStatus 变量。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete()
// ...
}
}
| 1 | 使用 @SessionAttributes 注解。 |
| 2 | 使用 SessionStatus 变量。 |
@SessionAttribute
如果你需要访问全局管理的、已存在的会话属性(即在控制器之外管理的会话属性——例如由过滤器管理),而这些属性可能存在也可能不存在,你可以在方法参数上使用 @SessionAttribute 注解,如下例所示:
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
| 1 | 使用 @SessionAttribute。 |
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
| 1 | 使用 @SessionAttribute。 |
对于需要添加或移除会话属性的使用场景,请考虑将 WebSession 注入到控制器方法中。
若需在控制器工作流中将会话中的模型属性进行临时存储,请考虑使用 SessionAttributes,详见
@SessionAttributes。
@RequestAttribute
与 @SessionAttribute 类似,你可以使用 @RequestAttribute 注解来访问之前已创建的请求属性(例如,由 WebFilter 创建的),如下例所示:
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
| 1 | 使用 @RequestAttribute。 |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
| 1 | 使用 @RequestAttribute。 |
多部分表单内容
如多部分数据(Multipart Data)中所述,ServerWebExchange 提供了对多部分内容的访问。在控制器中处理文件上传表单(例如来自浏览器的表单)的最佳方式是通过数据绑定到一个命令对象(command object),如下例所示:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
class MyForm(
val name: String,
val file: MultipartFile)
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
// ...
}
}
你也可以在 RESTful 服务场景中从非浏览器客户端提交 multipart 请求。以下示例使用了一个文件以及 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
... File Data ...
你可以使用 @RequestPart 访问各个部分,如下例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file) { (2)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
| 2 | 使用 @RequestPart 获取文件。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
| 2 | 使用 @RequestPart 获取文件。 |
要反序列化原始部分的内容(例如,将其转换为 JSON,类似于 @RequestBody),
您可以声明一个具体的目标准 Object,而不是 Part,如下例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
你可以将 @RequestPart 与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,从而触发标准的 Bean Validation 验证。验证错误会引发一个 WebExchangeBindException,导致返回 400(BAD_REQUEST)响应。该异常包含一个带有错误详情的 BindingResult,也可以通过在控制器方法中声明一个异步包装器类型的参数,然后使用与错误相关的操作符来处理该异常:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}
要将所有多部分(multipart)数据作为 MultiValueMap 访问,您可以使用 @RequestBody,如下例所示:
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
要以流式方式顺序访问 multipart 数据,您可以改用 @RequestBody 配合 Flux<Part>(在 Kotlin 中为 Flow<Part>),如下例所示:
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
@RequestBody
你可以使用 @RequestBody 注解,通过 HttpMessageReader 读取请求体并将其反序列化为一个 #webflux-codecs。
以下示例使用了一个 @RequestBody 参数:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
与 Spring MVC 不同,在 WebFlux 中,@RequestBody 方法参数支持响应式类型,并完全支持非阻塞读取以及(客户端到服务器端的)流式传输。
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
您可以使用HTTP 消息编解码器选项(位于WebFlux 配置中)来配置或自定义消息读取器。
你可以将 @RequestBody 与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,从而触发标准的 Bean Validation 验证。验证错误会引发一个 WebExchangeBindException 异常,导致返回 400(BAD_REQUEST)响应。该异常包含一个带有错误详情的 BindingResult,可以在控制器方法中通过声明一个异步包装器类型的参数,然后使用与错误处理相关的操作符来处理该异常:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
// use one of the onError* operators...
}
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
// ...
}
HttpEntity
HttpEntity 与使用 @RequestBody 大致相同,但基于一个暴露请求头和请求体的容器对象。以下示例使用了 HttpEntity:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
你可以在方法上使用 @ResponseBody 注解,通过 HttpMessageWriter 将返回值序列化到响应体中。以下示例展示了如何实现这一点:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody 也支持在类级别使用,此时它会被所有控制器方法继承。这正是 @RestController 的作用,它本质上只是一个元注解,同时标记了 @Controller 和 @ResponseBody。
您可以将 @ResponseBody 方法与 JSON 序列化视图结合使用。
详见Jackson JSON。
您可以使用WebFlux 配置中的HTTP 消息编解码器选项来配置或自定义消息的写入。
ResponseEntity
ResponseEntity 类似于 @ResponseBody,但包含状态和响应头。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux 支持使用单值响应式类型来异步生成ResponseEntity,以及使用单值或多值响应式类型作为响应体。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring WebFlux 内置支持
Jackson 的序列化视图(Serialization Views),
它允许仅渲染 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;
}
}
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser(): User {
return User("eric", "7!jd#h23")
}
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String
) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许指定一个视图类数组,但在每个控制器方法中只能指定其中一个。如果需要激活多个视图,请使用组合接口。 |
1.4.4. Model
你可以使用 @ModelAttribute 注解:
-
在
#webflux-ann-modelattrib-method-args方法的方法参数上, 用于从模型中创建或访问一个对象,并通过WebDataBinder将其绑定到请求。 -
作为
@Controller或@ControllerAdvice类中的方法级注解,用于在调用任何@RequestMapping方法之前初始化模型。 -
在
@RequestMapping方法上,用于将其返回值标记为模型属性。
本节讨论 @ModelAttribute 方法,即前述列表中的第二项。
一个控制器可以包含任意数量的 @ModelAttribute 方法。所有这些方法都会在同一个控制器中的 @RequestMapping 方法之前被调用。@ModelAttribute 方法还可以通过 @ControllerAdvice 在多个控制器之间共享。更多详细信息,请参阅控制器通知(Controller Advice)一节。
@ModelAttribute 方法具有灵活的方法签名。它们支持许多与 @RequestMapping 方法相同的参数(但不包括 @ModelAttribute 本身以及任何与请求体相关的内容)。
以下示例使用了一个 @ModelAttribute 方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
当未明确指定名称时,将根据类型选择一个默认名称,
如 Conventions 的 Javadoc 中所述。
您始终可以通过使用重载的 addAttribute 方法,
或通过在 @ModelAttribute 上设置 name 属性(针对返回值)来分配一个显式名称。 |
与 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) {
// ...
}
import org.springframework.ui.set
@ModelAttribute
fun addAccount(@RequestParam number: String) {
val accountMono: Mono<Account> = accountRepository.findAccount(number)
model["account"] = accountMono
}
@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
// ...
}
此外,任何具有响应式类型包装器的模型属性都会在视图渲染之前被解析为其实际值(并更新模型)。
你也可以在 @ModelAttribute 方法上将 @RequestMapping 用作方法级别的注解,此时 @RequestMapping 方法的返回值将被解释为模型属性。通常情况下,这并不是必需的,因为在 HTML 控制器中这是默认行为,除非返回值是一个 String,否则该字符串会被解释为视图名称。@ModelAttribute 还可用于自定义模型属性的名称,如下例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.4.5. DataBinder
@Controller 或 @ControllerAdvice 类可以包含 @InitBinder 方法,用于初始化 WebDataBinder 的实例。这些实例随后被用于:
-
将请求参数(即表单数据或查询参数)绑定到模型对象。
-
将基于
String的请求值(例如请求参数、路径变量、 请求头、Cookie 等)转换为控制器方法参数的目标类型。 -
在渲染 HTML 表单时,将模型对象的值格式化为
String值。
@InitBinder 方法可以注册控制器特定的 java.bean.PropertyEditor 或 Spring 的 Converter 和 Formatter 组件。此外,您还可以使用WebFlux Java 配置在全局共享的 Converter 中注册 Formatter 和 FormattingConversionService 类型。
@InitBinder 方法支持许多与 @RequestMapping 方法相同的参数,但不包括 @ModelAttribute(命令对象)参数。通常,它们会声明一个 WebDataBinder 参数用于注册,并具有 void 返回类型。
以下示例使用了 @InitBinder 注解:
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
| 1 | 使用 @InitBinder 注解。 |
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
或者,当通过共享的 Formatter 使用基于 FormattingConversionService 的配置时,您可以采用相同的方法,注册控制器特定的 Formatter 实例,如下例所示:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
| 1 | 添加一个自定义格式化器(在本例中为 DateFormatter)。 |
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
| 1 | 添加一个自定义格式化器(在本例中为 DateFormatter)。 |
模型设计
在 Web 应用程序的上下文中,数据绑定是指将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象的属性上。
只有遵循JavaBeans 命名规范的https://www.oracle.com/java/technologies/javase/javabeans-spec.html属性才会暴露用于数据绑定 —— 例如,对于public String getFirstName()属性,其对应的public void setFirstName(String)和firstName方法。
| 模型对象及其嵌套的对象图有时也被称为命令对象、表单支持对象或POJO(Plain Old Java Object,普通Java对象)。 |
默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以针对任意公共属性路径进行操作, 即使某些路径在特定用例中并不预期被访问。
例如,对于一个 HTTP 表单数据端点,恶意客户端可能会提供模型对象图中存在但未在浏览器所呈现的 HTML 表单中包含的属性值。这可能导致模型对象及其任意嵌套对象被设置意外更新的数据。
推荐的做法是使用一个专用的模型对象,该对象仅暴露与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单中,模型对象应声明最少的一组属性,如下列 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;
}
}
如果你不能或不想为每个数据绑定用例使用专用的模型对象,那么你必须限制允许用于数据绑定的属性。
理想情况下,你可以通过在setAllowedFields()上调用WebDataBinder方法来注册允许的字段模式,从而实现这一目标。
例如,要在您的应用程序中注册允许的字段模式,您可以在 @InitBinder 或 @Controller 组件中实现一个 @ControllerAdvice 方法,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以通过 setDisallowedFields() 及其子类中的 DataBinder 方法来注册禁止的字段模式。
但请注意,“允许列表”比“禁止列表”更安全。因此,应优先使用 setAllowedFields() 而非 setDisallowedFields()。
请注意,匹配允许的字段模式是区分大小写的;而匹配禁止的字段模式则是不区分大小写的。此外,即使某个字段同时匹配了允许列表中的某个模式,只要它匹配了禁止的模式,该字段也不会被接受。
|
在直接暴露您的领域模型用于数据绑定时,正确配置允许和禁止的字段模式至关重要。否则,将带来严重的安全风险。 此外,强烈建议您不要在数据绑定场景中使用来自领域模型的类型(例如 JPA 或 Hibernate 实体)作为模型对象。 |
1.4.6. 管理异常
@Controller 和 @ControllerAdvice 类可以包含 @ExceptionHandler 方法,用于处理控制器方法抛出的异常。以下示例包含这样一个异常处理方法:
@Controller
public class SimpleController {
// ...
@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
| 1 | 声明一个 @ExceptionHandler。 |
@Controller
class SimpleController {
// ...
@ExceptionHandler (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
| 1 | 声明一个 @ExceptionHandler。 |
该异常可以匹配正在传播的顶层异常(即直接抛出的IOException),也可以匹配顶层包装异常中的直接原因(例如,被包装在IOException内部的IllegalStateException)。
在匹配异常类型时,建议将目标异常声明为方法参数,如上例所示。或者,也可以通过注解声明来缩小要匹配的异常类型范围。我们通常建议在方法参数签名中尽可能具体,并在使用相应顺序(order)优先级的 @ControllerAdvice 中声明主要的根异常映射。
详见MVC 章节。
WebFlux 中的 @ExceptionHandler 方法支持与 @RequestMapping 方法相同的参数和返回值类型,但与请求体(request body)和 @ModelAttribute 相关的方法参数除外。 |
Spring WebFlux 对 @ExceptionHandler 方法的支持由用于 @RequestMapping 方法的 HandlerAdapter 提供。详见 DispatcherHandler 以获取更多详情。
REST API 异常
REST 服务的一个常见需求是在响应体中包含错误详细信息。Spring 框架不会自动这样做,因为响应体中错误详细信息的表示形式是特定于应用程序的。然而,@RestController 可以使用带有 @ExceptionHandler 返回值的 ResponseEntity 方法来设置响应的状态码和响应体。这类方法也可以在 @ControllerAdvice 类中声明,以实现全局应用。
请注意,Spring WebFlux 没有与 Spring MVC 中的 ResponseEntityExceptionHandler 等效的类,因为 WebFlux 仅抛出 ResponseStatusException(或其子类),而这些异常无需转换为 HTTP 状态码。 |
1.4.7. 控制器建议
通常,@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法仅在其所声明的 @Controller 类(或类层次结构)内生效。如果你想让这些方法具有更全局的作用范围(跨多个控制器),可以将它们声明在一个使用 @ControllerAdvice 或 @RestControllerAdvice 注解的类中。
@ControllerAdvice 使用了 @Component 注解,这意味着此类可以通过组件扫描注册为 Spring Bean。@RestControllerAdvice 是一个组合注解,同时标注了 @ControllerAdvice 和 @ResponseBody,本质上意味着 @ExceptionHandler 方法会通过消息转换直接渲染到响应体中(而不是通过视图解析或模板渲染)。
在启动时,用于处理 @RequestMapping 和 @ExceptionHandler 方法的基础设施类会检测带有 @ControllerAdvice 注解的 Spring Bean,并在运行时应用它们的方法。全局的 @ExceptionHandler 方法(来自 @ControllerAdvice)会在本地方法(来自 @Controller)之后被应用。相比之下,全局的 @ModelAttribute 和 @InitBinder 方法则会在本地方法之前被应用。
默认情况下,@ControllerAdvice 方法会应用于每个请求(即所有控制器),
但你可以通过在注解上使用属性来将其限定为控制器的一个子集,如下例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}
前一个示例中的选择器是在运行时评估的,如果大量使用,可能会对性能产生负面影响。请参阅
@ControllerAdvice
javadoc 以获取更多详细信息。
1.5. 函数式端点
Spring WebFlux 包含 WebFlux.fn,这是一种轻量级的函数式编程模型,使用函数来路由和处理请求,并且其契约设计为不可变的。 它是基于注解的编程模型的一种替代方案,但除此之外,它运行在相同的响应式核心(Reactive Core)基础之上。
1.5.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()
.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) {
// ...
}
}
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
suspend fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
suspend fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
suspend fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
| 1 | 使用协程(Coroutines)路由 DSL 创建路由器,也可以通过 router { } 使用响应式(Reactive)的替代方案。 |
运行 RouterFunction 的一种方式是将其转换为 HttpHandler,并通过内置的服务器适配器之一进行安装:
-
RouterFunctions.toHttpHandler(RouterFunction) -
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多数应用程序可以通过 WebFlux Java 配置运行,参见 运行服务器。
1.5.2. 处理函数
ServerRequest 和 ServerResponse 是不可变的接口,提供对 HTTP 请求和响应的 JDK 8 友好访问。
请求和响应都通过 Reactive Streams 对主体流提供背压支持。
请求主体由 Reactor 的 Flux 或 Mono 表示。
响应主体由任何 Reactive Streams 的 Publisher 表示,包括 Flux 和 Mono。
有关更多信息,请参阅 响应式库。
ServerRequest
ServerRequest 提供对 HTTP 方法、URI、请求头和查询参数的访问,而对请求体的访问则通过 body 方法提供。
以下示例将请求体提取为 Mono<String>:
Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()
以下示例将请求体提取为一个 Flux<Person>(在 Kotlin 中为 Flow<Person>),
其中 Person 对象从某种序列化格式(例如 JSON 或 XML)中解码得到:
Flux<Person> people = request.bodyToFlux(Person.class);
val people = request.bodyToFlow<Person>()
前面的示例是使用更通用的 ServerRequest.body(BodyExtractor) 方法的快捷方式,该方法接受 BodyExtractor 函数式策略接口。BodyExtractors 工具类提供了对多个实例的访问。例如,前面的示例也可以按如下方式编写:
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitFirst()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
以下示例展示了如何访问表单数据:
Mono<MultiValueMap<String, String> map = request.formData();
val map = request.awaitFormData()
以下示例展示了如何将多部分数据作为映射(Map)进行访问:
Mono<MultiValueMap<String, Part> map = request.multipartData();
val map = request.awaitMultipartData()
以下示例展示了如何以流式方式逐个访问多部分(multipart)数据:
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse
ServerResponse 提供对 HTTP 响应的访问,并且由于它是不可变的,你可以使用 build 方法来创建它。你可以使用构建器来设置响应状态、添加响应头,或者提供响应体。以下示例创建了一个包含 JSON 内容的 200(OK)响应:
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
以下示例展示了如何构建一个带有 Location 头部且无响应体的 201(CREATED)响应:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
根据所使用的编解码器(codec),可以传递提示参数(hint parameters)来自定义请求体的序列化或反序列化方式。例如,指定一个Jackson JSON 视图:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
处理器类
我们可以将处理函数编写为 lambda 表达式,如下例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
val helloWorld = HandlerFunction<ServerResponse> { 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;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
| 1 | listPeople 是一个处理函数,它将仓库中找到的所有 Person 对象以 JSON 格式返回。 |
| 2 | createPerson 是一个处理函数,用于将请求体中包含的新 Person 对象进行存储。
请注意,PersonRepository.savePerson(Person) 返回的是 Mono<Void>:一个空的 Mono,
当从请求中读取并存储完该人员信息后,会发出完成信号。因此,我们使用
build(Publisher<Void>) 方法,在接收到该完成信号时(即
Person 已被保存时)发送响应。 |
| 3 | getPerson 是一个处理函数,用于返回由路径变量 id 标识的单个人员。我们从仓库中检索该 Person 对象,如果找到,则创建一个 JSON 响应;如果未找到,则使用 switchIfEmpty(Mono<T>) 返回一个 404 Not Found 响应。 |
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
| 1 | listPeople 是一个处理函数,它将仓库中找到的所有 Person 对象以 JSON 格式返回。 |
| 2 | createPerson 是一个处理函数,用于将请求体中包含的新 Person 对象存储起来。
请注意,PersonRepository.savePerson(Person) 是一个无返回类型的挂起函数。 |
| 3 | getPerson 是一个处理函数,它返回由路径变量 id 标识的单个人员。我们从仓库中检索该 Person 并创建一个 JSON 响应(如果找到)。如果未找到,则返回 404 Not Found 响应。 |
验证
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对 400 响应抛出异常。 |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对 400 响应抛出异常。 |
处理器也可以通过创建并注入一个基于 Validator 的全局 LocalValidatorFactoryBean 实例,来使用标准的 Bean Validation API(JSR-303)。
参见 Spring 验证。
1.5.3. RouterFunction
路由函数(Router functions)用于将请求路由到相应的HandlerFunction。
通常,您不会自己编写路由函数,而是使用
RouterFunctions 工具类上的方法来创建路由函数。
RouterFunctions.route()(无参数)为您提供了一个流畅的构建器(fluent builder)来创建路由
函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 则提供了一种直接创建路由函数的方式。
通常建议使用 route() 构建器,因为它为典型的映射场景提供了便捷的快捷方式,而无需使用难以发现的静态导入。
例如,路由函数构建器提供了 GET(String, HandlerFunction) 方法来创建 GET 请求的映射;以及用于 POST 请求的 POST(String, HandlerFunction) 方法。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射请求时引入额外谓词(predicates)的方式。
对于每个 HTTP 方法,都提供了一个重载变体,该变体接受一个 RequestPredicate 参数,通过该参数可以表达额外的约束条件。
谓词
你可以编写自己的 RequestPredicate,但 RequestPredicates 工具类提供了常用的实现,这些实现基于请求路径、HTTP 方法、内容类型(content-type)等。
以下示例使用一个请求谓词(request predicate)根据 Accept 请求头创建约束条件:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
你可以通过使用以下方式将多个请求谓词组合在一起:
-
RequestPredicate.and(RequestPredicate)—— 两者都必须匹配。 -
RequestPredicate.or(RequestPredicate)—— 两者中任意一个匹配即可。
RequestPredicates 中的许多谓词都是组合而成的。
例如,RequestPredicates.GET(String) 就是由 RequestPredicates.method(HttpMethod)
和 RequestPredicates.path(String) 组合而成的。
上面所示的示例也使用了两个请求谓词,因为构建器在内部使用了
RequestPredicates.GET,并将其与 accept 谓词进行组合。
路由
路由函数按照顺序进行评估:如果第一个路由不匹配,则评估第二个,依此类推。 因此,将更具体的路由声明在通用路由之前是有意义的。 请注意,此行为与基于注解的编程模型不同,在基于注解的模型中,“最具体”的控制器方法会被自动选择。
使用路由函数构建器时,所有定义的路由会被组合成一个RouterFunction,并通过build()方法返回。
还有其他方式可以将多个路由函数组合在一起:
-
add(RouterFunction)方法位于RouterFunctions.route()构建器上 -
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()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
| 1 | 带有匹配 JSON 的 GET /person/{id} 请求头的 Accept 请求会被路由到
PersonHandler.getPerson |
| 2 | 带有匹配 JSON 的 GET /person 请求头的 Accept 请求会被路由到
PersonHandler.listPeople |
| 3 | POST /person 请求在没有附加谓词的情况下被映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是在其他地方创建的路由器函数,并被添加到所构建的路由中。 |
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
| 1 | 带有匹配 JSON 的 GET /person/{id} 请求头的 Accept 请求会被路由到
PersonHandler.getPerson |
| 2 | 带有匹配 JSON 的 GET /person 请求头的 Accept 请求会被路由到
PersonHandler.listPeople |
| 3 | POST /person 请求在没有附加谓词的情况下被映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是在其他地方创建的路由器函数,并被添加到所构建的路由中。 |
嵌套路由
通常,一组路由函数会共享一个谓词(predicate),例如共享一个路径。在上面的示例中,共享的谓词是一个路径谓词,匹配 /person,并被其中三个路由所使用。在使用注解时,您可以通过在类型级别上使用 @RequestMapping 注解(映射到 /person)来消除这种重复。在 WebFlux.fn 中,可以通过路由器函数构建器(router function builder)上的 path 方法来共享路径谓词。例如,上述示例的最后几行可以通过使用嵌套路由以如下方式进行改进:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
| 1 | 请注意,path 的第二个参数是一个消费者(Consumer),它接收一个路由器构建器(router builder)。 |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套最为常见,但你可以通过在构建器上使用 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("/person", handler::createPerson))
.build();
val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
1.5.4. 运行服务器
如何在 HTTP 服务器中运行一个路由函数?一个简单的选项是通过以下方式之一将路由函数转换为 HttpHandler:
-
RouterFunctions.toHttpHandler(RouterFunction) -
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
然后,您可以按照HttpHandler中的服务器特定说明,将返回的#webflux-httphandler与多个服务器适配器一起使用。
一个更典型的选项(Spring Boot 也采用此方式)是通过
DispatcherHandler 配置运行,该配置借助
WebFlux 配置,利用 Spring 配置来声明处理请求所需的组件。WebFlux Java 配置会声明以下基础设施组件以支持函数式端点:
-
RouterFunctionMapping:检测 Spring 配置中一个或多个RouterFunction<?>Bean,通过RouterFunction.andOther将它们组合起来,并将请求路由到所生成的组合后的RouterFunction。 -
HandlerFunctionAdapter:一个简单的适配器,允许DispatcherHandler调用已映射到请求的HandlerFunction。 -
ServerResponseResultHandler:通过调用HandlerFunction的writeTo方法,处理ServerResponse调用所产生的结果。
上述组件使函数式端点能够融入 DispatcherHandler 的请求处理生命周期,并且(如果声明了任何注解控制器)还可以与注解控制器并行运行。这也是 Spring Boot WebFlux Starter 启用函数式端点的方式。
以下示例展示了一个 WebFlux Java 配置(有关如何运行它的说明,请参见DispatcherHandler):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
1.5.5. 过滤处理器函数
你可以通过在路由函数构建器(routing function builder)上使用 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) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
| 1 | 仅对两个 GET 路由应用了添加自定义请求头的 before 过滤器。 |
| 2 | 应用于所有路由(包括嵌套路由)的after过滤器用于记录响应。 |
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST("/person", handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
}
| 1 | 仅对两个 GET 路由应用了添加自定义请求头的 before 过滤器。 |
| 2 | 应用于所有路由(包括嵌套路由)的after过滤器用于记录响应。 |
路由器构建器上的 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("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例表明,调用 next.handle(ServerRequest) 是可选的。
我们仅在允许访问时才执行处理函数。
除了在路由函数构建器上使用 filter 方法外,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用于现有的路由函数。
函数式端点的 CORS 支持通过专用的
CorsWebFilter提供。 |
1.6. URI 链接
本节介绍了 Spring 框架中用于准备 URI 的各种可用选项。
1.6.1. UriComponents
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 用于根据包含变量的 URI 模板构建 URI,如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
| 1 | 带有 URI 模板的静态工厂方法。 |
| 2 | 添加或替换 URI 组件。 |
| 3 | 请求对URI模板和URI变量进行编码。 |
| 4 | 构建一个 UriComponents。 |
| 5 | 展开变量并获取 URI。 |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
| 1 | 带有 URI 模板的静态工厂方法。 |
| 2 | 添加或替换 URI 组件。 |
| 3 | 请求对URI模板和URI变量进行编码。 |
| 4 | 构建一个 UriComponents。 |
| 5 | 展开变量并获取 URI。 |
前面的示例可以合并为一个链,并通过 buildAndExpand 进行简化,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val 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");
val 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");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.6.2. UriBuilder
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 实现 UriBuilder。您可以使用 UriBuilderFactory 创建一个
UriBuilder。UriBuilderFactory 和
UriBuilder 共同提供了一种可插拔的机制,用于基于共享配置(如基础 URL、编码偏好和其他细节)从 URI 模板构建 URI。
您可以使用 RestTemplate 来配置 WebClient 和 UriBuilderFactory,以自定义 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);
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = 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();
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val 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");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.6.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 在两个层级上提供了编码选项:
-
UriComponentsBuilder#encode(): 先对 URI 模板进行预编码,然后在展开时严格编码 URI 变量。
-
UriComponents#encode(): 在 URI 变量展开之后对 URI 组件进行编码。
这两种选项都会将非ASCII字符和非法字符替换为转义的八位字节序列。然而,第一种选项还会替换出现在URI变量中具有保留意义的字符。
| 考虑“;”字符,它在路径中是合法的,但具有保留含义。第一种选项会在 URI 变量中将“;”替换为“%3B”,但在 URI 模板中不会替换。相比之下,第二种选项永远不会替换“;”,因为它是路径中的合法字符。 |
在大多数情况下,第一种选项可能会得到预期的结果,因为它将 URI 变量视为不透明数据并进行完整编码,而第二种选项仅在 URI 变量有意包含保留字符时才有用。
以下示例使用了第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
你可以通过直接使用 URI(这隐含了编码)来简化前面的示例,如下例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val 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");
val 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);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
DefaultUriBuilderFactory 实现内部使用 UriComponentsBuilder 来展开和编码 URI 模板。作为一个工厂,它提供了一个统一的位置来配置编码方式,该方式基于以下其中一种编码模式:
-
TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,在展开时对 URI 模板进行预编码,并严格地对 URI 变量进行编码。 -
VALUES_ONLY:不对 URI 模板进行编码,而是在将 URI 变量展开到模板之前,通过UriUtils#encodeUriUriVariables对 URI 变量应用严格的编码。 -
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。
1.7. 跨域资源共享(CORS)
Spring WebFlux 允许你处理 CORS(跨域资源共享)。本节将介绍如何实现这一点。
1.7.1. 简介
出于安全原因,浏览器禁止向当前源(origin)之外的资源发起 AJAX 请求。 例如,你可能在一个标签页中打开了你的银行账户页面,而在另一个标签页中打开了 evil.com。来自 evil.com 的脚本不应能够使用你的凭据向你的银行 API 发起 AJAX 请求——例如,从你的账户中提取资金!
1.7.2. 处理
CORS 规范区分了预检请求(preflight)、简单请求(simple)和实际请求(actual)。 要了解 CORS 的工作原理,您可以阅读 这篇文章, 以及其他许多资料,或查阅规范以获取更多详细信息。
Spring WebFlux 的 HandlerMapping 实现提供了对 CORS 的内置支持。在成功将请求映射到处理器之后,HandlerMapping 会检查该请求和处理器对应的 CORS 配置,并执行后续操作。预检(Preflight)请求会被直接处理,而简单请求和实际的 CORS 请求则会被拦截、验证,并设置所需的 CORS 响应头。
为了启用跨域请求(即请求中包含 Origin 头部,且其值与请求的主机不同),您需要显式声明一些 CORS 配置。如果未找到匹配的 CORS 配置,预检(preflight)请求将被拒绝。简单 CORS 请求和实际 CORS 请求的响应中不会添加任何 CORS 相关头部,因此浏览器会拒绝这些请求。
每个 HandlerMapping 都可以
单独配置
基于 URL 模式的 CorsConfiguration 映射。在大多数情况下,应用程序使用 WebFlux 的 Java 配置来声明此类映射,从而生成一个全局的映射,并将其传递给所有的 HandlerMapping 实现。
你可以在 HandlerMapping 级别上结合全局 CORS 配置与更细粒度的处理器级别 CORS 配置。例如,带注解的控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。
全局配置与本地配置的组合规则通常是累加的——例如,所有全局来源和所有本地来源。对于仅能接受单个值的属性(如 allowCredentials 和 maxAge),本地配置将覆盖全局配置的值。详见 CorsConfiguration#combine(CorsConfiguration) 以获取更多详细信息。
|
要从源码中了解更多内容或进行高级自定义,请参阅:
|
1.7.3. @CrossOrigin
@CrossOrigin 注解可在带注解的控制器方法上启用跨域请求,如下例所示:
@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) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,@CrossOrigin 允许:
-
所有源。
-
所有标头。
-
控制器方法所映射的所有 HTTP 方法。
allowedCredentials 默认未启用,因为这会建立一种信任级别,暴露敏感的用户特定信息(例如 Cookie 和 CSRF Tokens),应仅在适当的情况下使用。
maxAge 被设置为 30 分钟。
@CrossOrigin 也支持在类级别上使用,并且会被所有方法继承。
以下示例指定了特定的域名,并将 maxAge 设置为一小时:
@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("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
你可以在类级别和方法级别同时使用 @CrossOrigin,如下例所示:
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com") (2)
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
| 1 | 在类级别使用 @CrossOrigin。 |
| 2 | 在方法级别使用 @CrossOrigin。 |
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com") (2)
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
| 1 | 在类级别使用 @CrossOrigin。 |
| 2 | 在方法级别使用 @CrossOrigin。 |
1.7.4. 全局配置
除了细粒度的控制器方法级别配置外,您可能还想定义一些全局的 CORS 配置。您可以在任意 CorsConfiguration 上单独设置基于 URL 的 HandlerMapping 映射。然而,大多数应用程序使用 WebFlux Java 配置来实现这一点。
默认情况下,全局配置启用了以下内容:
-
所有源。
-
所有标头。
-
GET、HEAD和POST方法。
allowedCredentials 默认未启用,因为这会建立一种信任级别,暴露敏感的用户特定信息(例如 Cookie 和 CSRF Tokens),应仅在适当的情况下使用。
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);
// Add more mappings...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
1.7.5. 跨域资源共享 (CORS) WebFilter
您可以通过内置的
CorsWebFilter 应用 CORS 支持,
它与 函数式端点 非常契合。
如果你尝试在 Spring Security 中使用 CorsFilter,请记住 Spring Security
内置了对 CORS 的支持。 |
要配置该过滤器,您可以声明一个 CorsWebFilter Bean,并将其构造函数传入一个 CorsConfigurationSource,如下例所示:
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// 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);
}
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
1.8. Web 安全
Spring Security 项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring Security 参考文档,包括:
1.9. 视图技术
Spring WebFlux 中视图技术的使用是可插拔的。无论您选择使用 Thymeleaf、FreeMarker 还是其他视图技术,主要都只需进行配置上的更改。本章介绍与 Spring WebFlux 集成的视图技术。我们假定您已经熟悉视图解析。
1.9.1. Thymeleaf
Thymeleaf 是一种现代的服务器端 Java 模板引擎,强调使用自然的 HTML 模板,这些模板只需双击即可在浏览器中预览,这对于独立开发 UI 模板(例如由设计师完成)非常有帮助,无需启动服务器。Thymeleaf 提供了丰富的功能集,并且正处于积极的开发和维护中。如需更全面的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目负责管理。该配置涉及几个 Bean 的声明,例如
SpringResourceTemplateResolver、SpringWebFluxTemplateEngine 和
ThymeleafReactiveViewResolver。更多详细信息,请参阅
Thymeleaf+Spring 以及 WebFlux 集成的
公告。
1.9.2. FreeMarker
Apache FreeMarker 是一个模板引擎,可用于生成各种类型的文本输出,从 HTML 到电子邮件等。Spring Framework 内置了对在 Spring WebFlux 中使用 FreeMarker 模板的支持。
视图配置
以下示例展示了如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates/freemarker")
}
}
您的模板需要存储在前面示例中 FreeMarkerConfigurer 所指定的目录中。根据上述配置,如果您的控制器返回视图名称 welcome,解析器将查找 classpath:/templates/freemarker/welcome.ftl 模板。
FreeMarker 配置
你可以通过在 Configuration bean 上设置相应的 bean 属性,将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 的 FreeMarkerConfigurer 对象(该对象由 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
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// ...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
}
}
有关设置和变量的详细信息,请参阅 FreeMarker 文档,这些内容适用于 Configuration 对象。
表单处理
Spring 提供了一个用于 JSP 的标签库,其中包含(但不限于)一个 <spring:bind/> 元素。该元素主要用于在表单中显示表单支持对象(form-backing objects)的值,并展示 Web 层或业务层中 Validator 验证失败的结果。Spring 还为 FreeMarker 提供了相同功能的支持,并额外提供了便捷的宏(macros),用于直接生成表单输入元素。
绑定宏
FreeMarker 的一组标准宏定义维护在 spring-webflux.jar 文件中,因此对于经过适当配置的应用程序而言,这些宏始终可用。
Spring 模板库中定义的一些宏被视为内部(私有)宏,但在宏定义中并不存在此类作用域限制,因此所有宏对调用代码和用户模板都是可见的。以下各节仅聚焦于您需要在模板中直接调用的宏。如果您希望直接查看宏的源代码,该文件名为 spring.ftl,位于 org.springframework.web.reactive.result.view.freemarker 包中。
有关绑定支持的更多详细信息,请参阅 Spring MVC 的简单绑定。
1.9.3. 脚本视图
Spring Framework 内置了与 Spring WebFlux 集成的功能,可配合任何基于 JSR-223 Java 脚本引擎运行的模板库使用。 下表列出了我们在不同脚本引擎上已测试过的模板库:
| 脚本库 | 脚本引擎 |
|---|---|
集成任何其他脚本引擎的基本规则是,它必须实现
ScriptEngine 和 Invocable 接口。 |
要求
您需要将脚本引擎放在类路径(classpath)中,具体细节因脚本引擎而异:
-
Nashorn JavaScript 引擎随 Java 8+ 一同提供。强烈建议使用最新的更新版本。
-
应将 JRuby 添加为依赖项以支持 Ruby。
-
Jython 应作为依赖项添加以支持 Python。
-
应添加
org.jetbrains.kotlin:kotlin-script-util依赖项,以及一个包含META-INF/services/javax.script.ScriptEngineFactory行的org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory文件,以支持 Kotlin 脚本。更多详情请参见此示例。
你需要拥有脚本模板库。对于 JavaScript 来说,一种实现方式是通过 WebJars。
脚本模板
你可以声明一个 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;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
render 函数被调用时带有以下参数:
-
String template:模板内容 -
Map model:视图模型 -
RenderingContext renderingContext:RenderingContext,它提供对应用程序上下文、区域设置、模板加载器以及 URL(自 5.0 版本起)的访问权限。
Mustache.render() 原生兼容此签名,因此你可以直接调用它。
如果你的模板技术需要一些自定义配置,你可以提供一个脚本,实现自定义的渲染函数。例如,Handlebars 在使用模板之前需要先对其进行编译,并且需要一个 polyfill 来模拟服务器端脚本引擎中不可用的某些浏览器功能。 以下示例展示了如何设置自定义渲染函数:
@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;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
当使用非线程安全的脚本引擎与未针对并发设计的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要将 sharedEngine 属性设置为 false。在这种情况下,由于此 bug,需要 Java SE 8 更新版本 60 或更高版本,但无论如何,通常都建议使用较新的 Java SE 补丁版本。 |
polyfill.js 仅定义了 Handlebars 正常运行所需的 window 对象,
如下代码片段所示:
var window = {};
这个基本的 render.js 实现在使用模板之前会先对其进行编译。一个可用于生产环境的实现还应该存储并重用缓存的模板或预编译的模板。
这可以在脚本端完成,也可以根据你的需要进行任何自定义(例如管理模板引擎的配置)。
以下示例展示了如何编译一个模板:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.9.4. JSON 和 XML
出于内容协商的目的,能够根据客户端请求的内容类型,在使用 HTML 模板渲染模型与其他格式(例如 JSON 或 XML)之间进行切换是非常有用的。为了支持这一功能,Spring WebFlux 提供了 HttpMessageWriterView,你可以用它来集成 #webflux-codecs 中任意可用的编解码器(Codecs),例如 Jackson2JsonEncoder、Jackson2SmileEncoder 或 Jaxb2XmlEncoder。
与其他视图技术不同,HttpMessageWriterView 不需要 ViewResolver,
而是作为默认视图进行配置。您可以配置一个或多个此类默认视图,
每个视图包装不同的 HttpMessageWriter 实例或 Encoder 实例。
在运行时,将使用与请求的内容类型相匹配的那个视图。
在大多数情况下,模型包含多个属性。要确定序列化哪一个属性,您可以使用要用于渲染的模型属性名称来配置 HttpMessageWriterView。如果模型仅包含一个属性,则使用该属性。
1.10. HTTP 缓存
HTTP 缓存可以显著提升 Web 应用程序的性能。HTTP 缓存围绕 Cache-Control 响应头以及后续的条件请求头(例如 Last-Modified 和 ETag)展开。Cache-Control 用于指示私有缓存(例如浏览器)和公共缓存(例如代理服务器)如何缓存及重用响应。当内容未发生变化时,ETag 头可用于发起一个条件请求,该请求可能返回不带响应体的 304(NOT_MODIFIED)状态码。ETag 可被视为比 Last-Modified 头更高级的继任者。
本节介绍 Spring WebFlux 中可用的 HTTP 缓存相关选项。
1.10.1. CacheControl
CacheControl 提供支持,用于配置与 Cache-Control 标头相关的设置,并可作为参数在多处接受:
尽管 RFC 7234 描述了 Cache-Control 响应头所有可能的指令,但 CacheControl 类型采用了一种以用例为导向的方法,专注于常见场景,如下例所示:
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
1.10.2. 控制器
控制器可以显式支持 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 is also available
.body(book);
}
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {
val book = findBook(id)
val version = book.getVersion()
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book)
}
如果与条件请求头的比较表明内容未发生变化,上述示例将返回一个带有空响应体的 304(未修改)响应。否则,ETag 和 Cache-Control 头将被添加到响应中。
你也可以在控制器中对条件请求头进行检查,如下例所示:
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {
long eTag = ... (1)
if (exchange.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
| 1 | 应用程序特定的计算。 |
| 2 | 响应已设置为 304(未修改)。无需进一步处理。 |
| 3 | 继续处理请求。 |
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {
val eTag: Long = ... (1)
if (exchange.checkNotModified(eTag)) {
return null(2)
}
model.addAttribute(...) (3)
return "myViewName"
}
| 1 | 应用程序特定的计算。 |
| 2 | 响应已设置为 304(未修改)。无需进一步处理。 |
| 3 | 继续处理请求。 |
有三种方式可用于根据 eTag 值、lastModified 值或两者同时检查条件请求。对于条件 GET 和 HEAD 请求,您可以将响应设置为 304(未修改)。对于条件 POST、PUT 和 DELETE 请求,则可以将响应设置为 412(前提条件失败),以防止并发修改。
1.11. WebFlux 配置
WebFlux 的 Java 配置声明了处理带注解控制器或函数式端点请求所需的组件,并提供了一个用于自定义配置的 API。这意味着您无需了解 Java 配置所创建的底层 Bean。然而,如果您希望了解这些 Bean,可以在 WebFluxConfigurationSupport 中查看,或在特殊 Bean 类型中阅读更多相关内容。
对于配置 API 中未提供的更高级自定义需求,您可以通过高级配置模式完全掌控配置。
1.11.1. 启用 WebFlux 配置
您可以在 Java 配置中使用 @EnableWebFlux 注解,如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig {
}
@Configuration
@EnableWebFlux
class WebConfig
前面的示例注册了多个 Spring WebFlux 基础设施 Bean,并根据类路径上可用的依赖项进行适配——例如 JSON、XML 等。
1.11.2. WebFlux 配置 API
在您的 Java 配置中,您可以实现 WebFluxConfigurer 接口,如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// Implement configuration methods...
}
1.11.3. 转换与格式化
默认情况下,系统会安装各种数字和日期类型的格式化器,并支持通过在字段上使用 @NumberFormat 和 @DateTimeFormat 进行自定义。
要在 Java 配置中注册自定义的格式化器(formatters)和转换器(converters),请使用以下方式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
默认情况下,Spring WebFlux 在解析和格式化日期值时会考虑请求的区域设置(Locale)。这适用于表单中使用 "input" 表单字段以字符串形式表示日期的情况。然而,对于 "date" 和 "time" 表单字段,浏览器会使用 HTML 规范中定义的固定格式。针对此类情况,可以按如下方式自定义日期和时间的格式化:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
请参阅 FormatterRegistrar SPI
以及 FormattingConversionServiceFactoryBean,以获取更多关于何时使用 FormatterRegistrar 实现的信息。 |
1.11.4. 验证
默认情况下,如果类路径中存在Bean Validation(例如 Hibernate Validator),则会将 LocalValidatorFactoryBean 注册为全局验证器,用于在 @Valid 方法参数上配合 @Validated 和 @Controller 使用。
在您的 Java 配置中,您可以自定义全局的 Validator 实例,如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public Validator getValidator(); {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun getValidator(): Validator {
// ...
}
}
请注意,您也可以注册本地的 Validator 实现,如下例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
如果你需要在某处注入一个 LocalValidatorFactoryBean,请创建一个 bean 并使用 @Primary 注解标记它,以避免与 MVC 配置中声明的那个发生冲突。 |
1.11.5. 内容类型解析器
您可以配置 Spring WebFlux 如何从请求中确定 @Controller 实例所请求的媒体类型。默认情况下,仅检查 Accept 请求头,但您也可以启用基于查询参数的策略。
以下示例展示了如何自定义请求的内容类型解析:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
// ...
}
}
1.11.6. HTTP 消息编解码器
以下示例展示了如何自定义请求和响应正文的读取与写入方式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(512 * 1024);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
}
}
ServerCodecConfigurer 提供了一组默认的读取器和写入器。您可以使用它来添加更多的读取器和写入器、自定义默认的读取器和写入器,或者完全替换默认的读取器和写入器。
对于 Jackson JSON 和 XML,建议考虑使用
Jackson2ObjectMapperBuilder,
它通过以下配置自定义了 Jackson 的默认属性:
如果在类路径中检测到以下知名模块,它还会自动注册这些模块:
-
jackson-datatype-joda:支持 Joda-Time 类型。 -
jackson-datatype-jsr310:支持 Java 8 日期和时间 API 类型。 -
jackson-datatype-jdk8:支持其他 Java 8 类型,例如Optional。 -
jackson-module-kotlin:支持 Kotlin 类和数据类。
1.11.7. 视图解析器
以下示例展示了如何配置视图解析:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// ...
}
}
ViewResolverRegistry 为 Spring 框架集成的视图技术提供了快捷方式。以下示例使用了 FreeMarker(这还需要配置底层的 FreeMarker 视图技术):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure Freemarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure Freemarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
}
}
你也可以插入任意的 ViewResolver 实现,如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ViewResolver resolver = ... ;
registry.viewResolver(resolver);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
val resolver: ViewResolver = ...
registry.viewResolver(resolver
}
}
为了支持内容协商并通过视图解析渲染其他格式(除 HTML 外),您可以基于HttpMessageWriterView实现配置一个或多个默认视图,该实现可接受#webflux-codecs中提供的任意编解码器(Codecs)。以下示例展示了如何进行此类配置:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = Jackson2JsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
// ...
}
有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术。
1.11.8. 静态资源
此选项提供了一种便捷的方式,用于从基于
Resource的位置列表中提供静态资源。
在下一个示例中,对于以 /resources 开头的请求,将使用相对路径在类路径下的 /static 目录中查找并提供静态资源。这些资源会设置一年的未来过期时间,以确保浏览器缓存得到最大程度的利用,并减少浏览器发起的 HTTP 请求次数。同时也会检查 Last-Modified 响应头,如果存在该头信息,则返回 304 状态码。以下列表展示了该示例:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
}
}
资源处理器还支持一系列
ResourceResolver 实现和
ResourceTransformer 实现,
可用于构建处理优化资源的工具链。
您可以使用 VersionResourceResolver,根据内容计算出的 MD5 哈希值、固定的应用程序版本或其他信息,为资源 URL 添加版本信息。ContentVersionStrategy(MD5 哈希)是一个不错的选择,但有一些明显的例外情况(例如与模块加载器一起使用的 JavaScript 资源)。
以下示例展示了如何在您的 Java 配置中使用 VersionResourceResolver:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
您可以使用 ResourceUrlProvider 来重写 URL,并应用完整的解析器和转换器链(例如,插入版本信息)。WebFlux 配置提供了一个 ResourceUrlProvider,以便可以将其注入到其他组件中。
与 Spring MVC 不同,目前在 WebFlux 中无法透明地重写静态资源 URL,因为尚无任何视图技术能够利用非阻塞的解析器和转换器链。当仅提供本地资源时,解决方法是直接使用 ResourceUrlProvider(例如,通过自定义元素),但这会导致阻塞。
请注意,当同时使用 EncodedResourceResolver(例如,Gzip、Brotli 编码)和
VersionedResourceResolver 时,必须按照此顺序注册它们,以确保基于内容的版本始终根据未编码的文件可靠地计算。
WebJars 也通过 WebJarsResourceResolver 得到支持,当类路径中存在 org.webjars:webjars-locator-core 库时,该解析器会自动注册。该解析器可以重写 URL 以包含 JAR 文件的版本号,也可以匹配不带版本号的传入 URL——例如,将 /jquery/jquery.min.js 映射到 /jquery/1.2.0/jquery.min.js。
1.11.9. 路径匹配
您可以自定义与路径匹配相关的选项。有关各个选项的详细信息,请参阅
PathMatchConfigurer javadoc。
以下示例展示了如何使用 PathMatchConfigurer:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController::class.java))
}
}
|
Spring WebFlux 依赖于一种称为 与 Spring MVC 不同,Spring WebFlux 也不支持后缀模式匹配,而在 Spring MVC 中,我们也建议不再依赖此功能。 |
1.11.10. 高级配置模式
@EnableWebFlux 导入 DelegatingWebFluxConfiguration,该配置会:
-
为 WebFlux 应用程序提供默认的 Spring 配置
-
检测并委托给
WebFluxConfigurer实现类,以自定义该配置。
在高级模式下,您可以移除 @EnableWebFlux,并直接继承 DelegatingWebFluxConfiguration,而不是实现 WebFluxConfigurer,如下例所示:
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {
// ...
}
您可以保留 WebConfig 中的现有方法,但现在也可以覆盖基类中的 Bean 声明,同时类路径上仍然可以存在任意数量的其他 WebMvcConfigurer 实现。
1.12. HTTP/2
HTTP/2 在 Reactor Netty、Tomcat、Jetty 和 Undertow 中均受支持。然而,这涉及到服务器配置方面的一些注意事项。更多详情,请参阅HTTP/2 Wiki 页面。
2. WebClient
Spring WebFlux 包含一个用于 HTTP 请求的响应式、非阻塞 WebClient。该客户端具有函数式、流畅的 API,并结合响应式类型以支持声明式的组合,参见 响应式库。WebFlux 的客户端和服务器端均依赖相同的非阻塞 编解码器(codecs) 来对请求和响应内容进行编码与解码。
在内部,WebClient 会委托给一个 HTTP 客户端库。默认情况下,它使用 Reactor Netty,同时也内置支持 Jetty 的 响应式 HttpClient,其他客户端则可以通过 ClientHttpConnector 进行集成。
2.1. 配置
创建 WebClient 最简单的方式是通过其中一个静态工厂方法:
-
WebClient.create() -
WebClient.create(String baseUrl)
上述方法使用了具有默认设置的 Reactor Netty HttpClient,并期望类路径中包含 io.projectreactor.netty:reactor-netty。
你也可以使用 WebClient.builder() 并附加更多选项:
-
uriBuilderFactory:用作基础 URL 的自定义UriBuilderFactory。 -
defaultHeader:每个请求的头部信息。 -
defaultCookie:每个请求的 Cookie。 -
defaultRequest:Consumer用于自定义每个请求。 -
filter:每个请求的客户端过滤器。 -
exchangeStrategies:HTTP 消息读取器/写入器的自定义配置。 -
clientConnector:HTTP 客户端库设置。
以下示例配置了HTTP 编解码器:
WebClient client = WebClient.builder()
.exchangeStrategies(builder -> {
return builder.codecs(codecConfigurer -> {
//...
});
})
.build();
val webClient = WebClient.builder()
.exchangeStrategies { strategies ->
strategies.codecs {
//...
}
}
.build()
一旦构建完成,WebClient 实例就是不可变的。但是,您可以克隆它并构建一个修改后的副本,而不会影响原始实例,如下例所示:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
2.1.1. 最大内存大小
Spring WebFlux 在编解码器(codec)中配置了内存缓冲限制, 以避免应用程序出现内存问题。默认情况下,该限制被设置为 256KB;如果你的使用场景需要更大的缓冲区,你将会看到以下内容:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
你可以通过以下代码示例为所有默认编解码器配置此限制:
WebClient webClient = WebClient.builder()
.exchangeStrategies(builder ->
builder.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();
val webClient = WebClient.builder()
.exchangeStrategies { builder ->
builder.codecs {
it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
}
}
.build()
2.1.2. Reactor Netty
要自定义 Reactor Netty 的设置,只需提供一个预先配置好的 HttpClient 即可:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
资源
默认情况下,HttpClient 会参与由 reactor.netty.http.HttpResources 管理的全局 Reactor Netty 资源,包括事件循环线程和连接池。
这是推荐的模式,因为对于事件循环并发而言,固定且共享的资源更为合适。在此模式下,全局资源会一直保持活跃状态,直到进程退出。
如果服务器与进程同步,则通常无需显式关闭。但是,如果服务器可以在进程内启动或停止(例如,以 WAR 包形式部署的 Spring MVC 应用程序),你可以声明一个类型为 ReactorResourceFactory 的 Spring 管理的 Bean,并将 globalResources=true(默认值)设置为 true,以确保在 Spring ApplicationContext 关闭时,Reactor Netty 的全局资源也会被关闭,如下例所示:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
@Bean
fun reactorResourceFactory() = 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 -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper); (2)
return WebClient.builder().clientConnector(connector).build(); (3)
}
| 1 | 创建独立于全局资源的资源。 |
| 2 | 使用带有资源工厂的 ReactorClientHttpConnector 构造函数。 |
| 3 | 将连接器插入到 WebClient.Builder 中。 |
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false (1)
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Further customizations...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)
return WebClient.builder().clientConnector(connector).build() (3)
}
| 1 | 创建独立于全局资源的资源。 |
| 2 | 使用带有资源工厂的 ReactorClientHttpConnector 构造函数。 |
| 3 | 将连接器插入到 WebClient.Builder 中。 |
超时
配置连接超时:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}
要配置读取和/或写入超时值:
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create().tcpConfiguration {
it.doOnConnected { conn -> conn
.addHandlerLast(ReadTimeoutHandler(10))
.addHandlerLast(WriteTimeoutHandler(10))
}
}
2.1.3. Jetty
以下示例展示了如何自定义 Jetty HttpClient 的设置:
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
val httpClient = HttpClient()
httpClient.cookieStore = ...
val connector = JettyClientHttpConnector(httpClient)
val webClient = WebClient.builder().clientConnector(connector).build();
默认情况下,HttpClient 会创建自己的资源(Executor、ByteBufferPool、Scheduler),
这些资源会一直保持活跃状态,直到进程退出或调用 stop() 方法为止。
您可以通过声明一个类型为 ApplicationContext 的 Spring 管理的 Bean,在多个 Jetty 客户端(以及服务器)实例之间共享资源,并确保在 Spring JettyResourceFactory 关闭时这些资源也被正确关闭,如下例所示:
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory()); (1)
return WebClient.builder().clientConnector(connector).build(); (2)
}
| 1 | 使用带有资源工厂的 JettyClientHttpConnector 构造函数。 |
| 2 | 将连接器插入到 WebClient.Builder 中。 |
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further customizations...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)
return WebClient.builder().clientConnector(connector).build() (2)
}
| 1 | 使用带有资源工厂的 JettyClientHttpConnector 构造函数。 |
| 2 | 将连接器插入到 WebClient.Builder 中。 |
2.2. retrieve()
retrieve() 方法是获取响应体并对其进行解码的最简单方式。
以下示例展示了如何实现这一点:
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()
你也可以获取从响应中解码出的对象流,如下例所示:
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()
默认情况下,状态码为 4xx 或 5xx 的响应会抛出 WebClientResponseException 异常,或其针对特定 HTTP 状态码的子类,例如 WebClientResponseException.BadRequest、WebClientResponseException.NotFound 等。
你也可以使用 onStatus 方法来自定义所抛出的异常,如下例所示:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()
当使用 onStatus 时,如果响应预期包含内容,则 onStatus 回调应负责消费该内容。否则,内容将被自动清空,以确保释放相关资源。
2.3. exchange()
exchange() 方法比 retrieve 方法提供更多的控制。以下示例与 retrieve() 等效,但同时还提供了对 ClientResponse 的访问:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.awaitBody<Person>()
在这一层级,你还可以创建一个完整的ResponseEntity:
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.toEntity<Person>()
请注意(与 retrieve() 不同),使用 exchange() 时,对于 4xx 和 5xx 响应不会自动触发错误信号。您需要自行检查状态码并决定如何继续处理。
|
与 |
2.4. 请求体
请求体可以从 ReactiveAdapterRegistry 所处理的任意异步类型进行编码,例如以下示例所示的 Mono 或 Kotlin 协程的 Deferred:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
你也可以对对象流进行编码,如下例所示:
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);
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()
或者,如果你已有实际的值,可以使用 bodyValue 快捷方法,如下例所示:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()
2.4.1. 表单数据
要发送表单数据,您可以提供一个 MultiValueMap<String, String> 作为请求体。请注意,内容类型会由 application/x-www-form-urlencoded 自动设置为 FormHttpMessageWriter。以下示例展示了如何使用 MultiValueMap<String, String>:
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()
你也可以通过使用 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);
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()
2.4.2. 多部分数据
要发送多部分(multipart)数据,您需要提供一个 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); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
val builder = MultipartBodyBuilder().apply {
part("fieldPart", "fieldValue")
part("filePart1", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
在大多数情况下,您无需为每个部分指定 Content-Type。内容类型会根据用于序列化该部分的 HttpMessageWriter 自动确定;如果是 Resource 类型,则根据文件扩展名自动确定。如有必要,您可以通过重载的构建器 MediaType 方法之一,显式地为每个部分指定要使用的 part。
一旦准备好 MultiValueMap,将其传递给 WebClient 最简单的方式是通过 body 方法,如下例所示:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()
如果 MultiValueMap 包含至少一个非 String 类型的值(这也可能表示普通的表单数据,即 application/x-www-form-urlencoded),则无需将 Content-Type 设置为 multipart/form-data。在使用 MultipartBodyBuilder 时总是如此,因为它会确保提供一个 HttpEntity 包装器。
作为 MultipartBodyBuilder 的替代方案,您也可以通过内置的 BodyInserters 以内联方式提供 multipart 内容,如下例所示:
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);
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()
2.5. 客户端过滤器
您可以通过 ExchangeFilterFunction 注册一个客户端过滤器(WebClient.Builder),以拦截并修改请求,如下例所示:
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
val client = WebClient.builder()
.filter { request, next ->
val filtered = ClientRequest.from(request)
.header("foo", "bar")
.build()
next.exchange(filtered)
}
.build()
这可用于横切关注点,例如身份验证。以下示例通过一个静态工厂方法使用过滤器实现基本身份验证:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()
过滤器会全局应用于每个请求。若要针对特定请求更改过滤器的行为,可以向 ClientRequest 添加请求属性,链中的所有过滤器均可访问这些属性,如下例所示:
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);
}
val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.awaitBody<Unit>()
你还可以复制一个现有的WebClient,插入新的过滤器,或移除已注册的过滤器。以下示例在索引0处插入了一个基本身份验证过滤器:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
2.6. 同步使用
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();
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}
然而,如果需要进行多次调用,更高效的做法是避免对每个响应单独阻塞,而是等待合并后的结果:
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, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
val data = runBlocking {
val personDeferred = async {
client.get().uri("/person/{id}", personId)
.retrieve().awaitBody<Person>()
}
val hobbiesDeferred = async {
client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
}
以上仅是一个示例。还有许多其他模式和操作符可用于构建响应式管道,在整个过程中发出多次远程调用(其中一些可能是嵌套的、相互依赖的),而无需在最终结果返回之前发生任何阻塞。
|
在 Spring MVC 或 Spring WebFlux 控制器中,使用 |
2.7. 测试
要测试使用 WebClient 的代码,您可以使用模拟 Web 服务器,例如
OkHttp MockWebServer。要查看其使用示例,请参阅
Spring Framework 测试套件中的 WebClientIntegrationTests
或 OkHttp 仓库中的 static-server
示例。
3. WebSockets
本参考文档的这一部分涵盖了对响应式栈(reactive-stack)WebSocket 消息传递的支持。
3.1. WebSocket 简介
WebSocket 协议(RFC 6455)提供了一种标准化的方式,可在客户端与服务器之间通过单一 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)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
| 1 | Upgrade 头部。 |
| 2 | 使用 Upgrade 连接。 |
与通常的 200 状态码不同,支持 WebSocket 的服务器会返回类似如下的输出:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
| 1 | 协议切换 |
握手成功后,HTTP 升级请求底层的 TCP 套接字将保持打开状态,供客户端和服务器继续发送和接收消息。
本文档的范围不包括对 WebSocket 工作原理的完整介绍。 有关详细信息,请参阅 RFC 6455、HTML5 中的 WebSocket 章节,或网上众多的入门指南和教程。
请注意,如果 WebSocket 服务器运行在 Web 服务器(例如 nginx)之后,您很可能需要配置该 Web 服务器,使其将 WebSocket 升级请求转发给 WebSocket 服务器。同样,如果应用程序运行在云环境中,请查阅云服务提供商关于 WebSocket 支持的相关说明。
3.1.1. HTTP 与 WebSocket
尽管 WebSocket 被设计为与 HTTP 兼容,并以 HTTP 请求开始, 但理解这两种协议会导致截然不同的架构和应用程序编程模型非常重要。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。客户端通过访问这些 URL,以请求-响应的方式与应用程序进行交互。服务器根据 HTTP URL、方法和请求头,将请求路由到相应的处理器。
相比之下,在 WebSocket 中,通常只有一个用于初始连接的 URL。 随后,所有应用消息都通过该相同的 TCP 连接进行传输。这指向了一种完全不同的异步、事件驱动的消息架构。
WebSocket 也是一种低层传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息的语义达成一致,否则无法对消息进行路由或处理。
WebSocket 客户端和服务器可以通过 HTTP 握手请求中的 Sec-WebSocket-Protocol 头协商使用更高层的消息协议(例如 STOMP)。如果没有该头信息,它们就需要自行约定通信规范。
3.1.2. 何时使用 WebSocket
WebSocket 可以使网页变得动态且具有交互性。然而,在许多情况下,结合使用 Ajax 与 HTTP 流式传输或长轮询即可提供一种简单而有效的解决方案。
例如,新闻、邮件和社交动态需要动态更新,但每隔几分钟更新一次可能就完全足够了。而协作应用、游戏和金融类应用则需要更接近实时的更新。
仅延迟本身并不是一个决定性因素。如果消息量相对较低(例如,监控网络故障),HTTP 流式传输或轮询可以提供一种有效的解决方案。 只有在低延迟、高频率和高吞吐量三者结合的情况下,才最能体现使用 WebSocket 的优势。
还需注意的是,在互联网上,您无法控制的限制性代理可能会阻止 WebSocket 通信,原因可能是它们未配置为传递 Upgrade 头部,或者因为它们会关闭看似空闲的长连接。这意味着,对于防火墙内部的应用程序而言,使用 WebSocket 是一个更为直接明确的选择,而对于面向公众的应用程序则不然。
3.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,可用于编写处理 WebSocket 消息的客户端和服务器端应用程序。
3.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) {
// ...
}
}
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}
然后你可以将其映射到一个 URL,并添加一个 WebSocketHandlerAdapter,如下例所示:
@Configuration
class WebConfig {
@Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/path", new MyWebSocketHandler());
int order = -1; // before annotated controllers
return new SimpleUrlHandlerMapping(map, order);
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
3.2.2. WebSocketHandler
handle 的 WebSocketHandler 方法接收一个 WebSocketSession 参数,并返回 Mono<Void>,
以指示应用程序对该会话的处理何时完成。该会话通过两个流进行处理,一个用于入站消息,另一个用于出站消息。下表
描述了用于处理这两个流的两个方法:
WebSocketSession 方法 |
描述 |
|---|---|
|
提供对入站消息流的访问,并在连接关闭时完成。 |
|
接收一个用于传出消息的源,写入这些消息,并返回一个 |
WebSocketHandler 必须将入站流和出站流组合成一个统一的数据流,并返回一个 Mono<Void>,以反映该数据流的完成状态。根据应用程序的需求,当满足以下条件时,该统一数据流即告完成:
-
入站或出站消息流完成。
-
入站流已完成(即连接已关闭),而出站流是无限的。
-
在选定的某个时间点,通过
close的WebSocketSession方法。
当入站和出站消息流组合在一起时,无需检查连接是否处于打开状态,因为响应式流(Reactive Streams)的信号会终止活动。 入站流会收到完成或错误信号,而出站流会收到取消信号。
处理器最基本的实现是处理入站流的实现。以下示例展示了此类实现:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() (1)
.doOnNext(message -> {
// ... (2)
})
.concatMap(message -> {
// ... (3)
})
.then(); (4)
}
}
| 1 | 访问入站消息流。 |
| 2 | 对每条消息执行某些操作。 |
| 3 | 执行使用消息内容的嵌套异步操作。 |
| 4 | 返回一个 Mono<Void>,当接收完成时,该 Mono 将完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive() (1)
.doOnNext {
// ... (2)
}
.concatMap {
// ... (3)
}
.then() (4)
}
}
| 1 | 访问入站消息流。 |
| 2 | 对每条消息执行某些操作。 |
| 3 | 执行使用消息内容的嵌套异步操作。 |
| 4 | 返回一个 Mono<Void>,当接收完成时,该 Mono 将完成。 |
对于嵌套的异步操作,你可能需要在使用池化数据缓冲区的底层服务器(例如 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)
}
}
| 1 | 处理入站消息流。 |
| 2 | 创建出站消息,生成一个组合流。 |
| 3 | 返回一个 Mono<Void>,只要我们持续接收数据,它就不会完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val output = session.receive() (1)
.doOnNext {
// ...
}
.concatMap {
// ...
}
.map { session.textMessage("Echo $it") } (2)
return session.send(output) (3)
}
}
| 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)
}
}
| 1 | 处理入站消息流。 |
| 2 | 发送传出消息。 |
| 3 | 合并两个流,并返回一个 Mono<Void>,当任一流结束时,该 Mono 即完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val input = session.receive() (1)
.doOnNext {
// ...
}
.concatMap {
// ...
}
.then()
val source: Flux<String> = ...
val output = session.send(source.map(session::textMessage)) (2)
return Mono.zip(input, output).then() (3)
}
}
| 1 | 处理入站消息流。 |
| 2 | 发送传出消息。 |
| 3 | 合并两个流,并返回一个 Mono<Void>,当任一流结束时,该 Mono 即完成。 |
3.2.3. DataBuffer
DataBuffer 是 WebFlux 中字节缓冲区的表示形式。参考文档的 Spring Core 部分在数据缓冲区与编解码器一节中有更多相关内容。需要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是池化的并采用引用计数机制,必须在使用完毕后释放,以避免内存泄漏。
当在 Netty 上运行时,如果应用程序希望保留输入的数据缓冲区以确保它们不会被释放,则必须使用 DataBufferUtils.retain(dataBuffer),并在缓冲区被消费后随后调用 DataBufferUtils.release(dataBuffer)。
3.2.4. 握手
WebSocketHandlerAdapter 委托给一个 WebSocketService。默认情况下,该服务是 HandshakeWebSocketService 的实例,它会对 WebSocket 请求执行基本检查,然后使用适用于当前服务器的 RequestUpgradeStrategy。目前,内置支持 Reactor Netty、Tomcat、Jetty 和 Undertow。
HandshakeWebSocketService 提供了一个 sessionAttributePredicate 属性,允许设置一个 Predicate<String>,用于从 WebSession 中提取属性并将其插入到 WebSocketSession 的属性中。
3.2.5. 服务器配置
每个服务器的 RequestUpgradeStrategy 会暴露底层 WebSocket 引擎可用的 WebSocket 相关配置选项。以下示例展示了在 Tomcat 上运行时设置 WebSocket 选项的方法:
@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);
}
}
@Configuration
class WebConfig {
@Bean
fun handlerAdapter() =
WebSocketHandlerAdapter(webSocketService())
@Bean
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
请检查您服务器的升级策略,以查看有哪些可用选项。目前,只有 Tomcat 和 Jetty 提供此类选项。
3.2.6. CORS
配置 CORS 并限制对 WebSocket 端点访问的最简单方法是让您的 WebSocketHandler 实现 CorsConfigurationSource 接口,并返回一个包含允许的源(origins)、请求头(headers)及其他详细信息的 CorsConfiguraiton 对象。如果您无法这样做,也可以在 corsConfigurations 上设置 SimpleUrlHandler 属性,通过 URL 模式来指定 CORS 设置。如果同时指定了这两种方式,系统将通过 combine 的 CorsConfiguration 方法将它们合并。
3.2.7. 客户端
Spring WebFlux 提供了 WebSocketClient 抽象,其实现包括 Reactor Netty、Tomcat、Jetty、Undertow 和标准 Java(即 JSR-356)。
Tomcat 客户端实际上是标准 Java 客户端的一个扩展,在 WebSocketSession 处理中增加了一些额外功能,以利用 Tomcat 特有的 API 来暂停接收消息,从而实现背压(back pressure)控制。 |
要启动一个 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());
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
session.receive()
.doOnNext(::println)
.then()
}
某些客户端(例如 Jetty)实现了 Lifecycle 接口,在使用之前需要先停止再启动。所有客户端都提供了与底层 WebSocket 客户端配置相关的构造函数选项。
4. 测试
spring-test 模块提供了 ServerHttpRequest、
ServerHttpResponse 和 ServerWebExchange 的模拟实现。
有关模拟对象的讨论,请参见Spring Web Reactive。
WebTestClient 基于这些模拟请求和响应对象,提供对无需 HTTP 服务器即可测试 WebFlux 应用程序的支持。您也可以将 WebTestClient 用于端到端集成测试。
5. RSocket
本节介绍 Spring Framework 对 RSocket 协议的支持。
5.1. 概述
RSocket 是一种应用层协议,支持通过 TCP、WebSocket 和其他字节流传输方式进行多路复用的双向通信,并采用以下交互模型之一:
-
Request-Response—— 发送一条消息并接收一条返回。 -
Request-Stream— 发送一条消息并接收返回的消息流。 -
Channel— 在两个方向上发送消息流。 -
Fire-and-Forget—— 发送单向消息。
一旦初始连接建立,“客户端”与“服务器端”的区别便不复存在,因为双方变得对称,每一方都可以发起上述交互之一。这也正是在协议调用中,参与双方被称为“请求方”(requester)和“响应方”(responder),而上述交互则被称为“请求流”(request streams)或简称为“请求”(requests)的原因。
这些是 RSocket 协议的主要特性和优势:
-
Reactive Streams 语义跨越网络边界——对于
Request-Stream和Channel等流式请求,背压(back pressure)信号在请求方与响应方之间传递,使请求方能够从源头减缓响应方的速度,从而减少对网络层拥塞控制的依赖,以及对网络层或任何层级缓冲的需求。 -
请求限流——此功能名为“租约”(Leasing),名称源自
LEASE帧,该帧可由任一端发送,用于在给定时间内限制另一端允许的请求总数。租约会定期续期。 -
会话恢复(Session resumption)——这是为应对连接中断而设计的,需要维护一些状态。 状态管理对应用程序是透明的,并且能够很好地与背压(back pressure)机制结合使用: 当可能时,背压可以停止生产者,从而减少所需维护的状态量。
-
大型消息的分片与重组。
-
保持连接(心跳)。
RSocket 在多种语言中都有实现。其Java 库基于Project Reactor构建,并使用Reactor Netty作为传输层。这意味着应用程序中来自 Reactive Streams Publisher 的信号能够透明地通过 RSocket 在网络中传播。
5.1.1. 协议
RSocket 的优势之一在于它在传输层具有明确定义的行为,并附带一份易于阅读的规范以及一些协议扩展。因此,无论使用何种语言的实现或高层框架 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 消息包含数据和元数据。元数据可用于发送路由、安全Tokens等信息。数据和元数据可以采用不同的格式。各自的 MIME 类型在 SETUP 帧中声明,并适用于给定连接上的所有请求。
虽然所有消息都可以包含元数据,但诸如路由(route)之类的元数据通常是按请求(per-request)的,
因此仅包含在请求的第一个消息中,即使用以下帧类型之一:
REQUEST_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL 或 REQUEST_FNF。
协议扩展定义了应用程序中使用的通用元数据格式:
5.1.2. Java 实现
RSocket 的 Java 实现 基于
Project Reactor 构建。TCP 和 WebSocket 传输层则
基于 Reactor Netty 构建。作为 Reactive Streams
库,Reactor 简化了协议的实现工作。对于应用程序而言,使用 Flux 和 Mono 配合声明式操作符以及透明的背压支持是一种自然而然的选择。
RSocket Java 中的 API 故意设计得简洁而基础。它专注于协议特性,而将应用程序编程模型(例如 RPC 代码生成或其他方式)作为更高层次、独立的关注点。
主要契约
io.rsocket.RSocket
通过 Mono 表示单条消息的承诺,Flux 表示消息流,以及 io.rsocket.Payload 表示实际消息(可访问数据和元数据的字节缓冲区),对四种请求交互类型进行了建模。RSocket 契约以对称方式使用。在发起请求时,应用程序会获得一个 RSocket 实例用于执行请求;在响应请求时,应用程序则需实现 RSocket 接口以处理请求。
这并不是一个详尽的入门介绍。在大多数情况下,Spring 应用程序无需直接使用其 API。然而,脱离 Spring 环境单独查看或试验 RSocket 可能是很有必要的。RSocket Java 代码仓库包含多个示例应用,用于演示其 API 和协议特性。
5.1.3. Spring 支持
spring-messaging 模块包含以下内容:
-
RSocketRequester — 通过
io.rsocket.RSocket发起请求的流畅 API,支持数据和元数据的编码/解码。 -
带注解的响应器 — 用于响应的带有
@MessageMapping注解的处理方法。
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 章节。
Spring Security 5.2 提供了对 RSocket 的支持。
Spring Integration 5.2 提供了入站和出站网关,用于与 RSocket 客户端和服务器进行交互。更多详细信息,请参阅《Spring Integration 参考手册》。
Spring Cloud Gateway 支持 RSocket 连接。
5.2. RSocketRequester
RSocketRequester 提供了一个流畅的 API 来执行 RSocket 请求,它接受和返回用于数据与元数据的对象,而不是底层的数据缓冲区。它可以对称地使用,既可用于客户端发起请求,也可用于服务器端发起请求。
5.2.1. 客户端请求者
在客户端获取 RSocketRequester 需要连接到服务器,并准备和发送初始的 RSocket SETUP 帧。RSocketRequester 为此提供了一个构建器。其内部基于 io.rsocket.core.RSocketConnector 构建。
这是使用默认设置进行连接的最基本方式:
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectTcp("localhost", 7000);
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectWebSocket(URI.create("https://example.org:8080/rsocket"));
import org.springframework.messaging.rsocket.connectTcpAndAwait
import org.springframework.messaging.rsocket.connectWebSocketAndAwait
val requester = RSocketRequester.builder()
.connectTcpAndAwait("localhost", 7000)
val requester = RSocketRequester.builder()
.connectWebSocketAndAwait(URI.create("https://example.org:8080/rsocket"))
上述操作是延迟执行的。要实际建立连接并使用请求器:
// Connect asynchronously
RSocketRequester.builder().connectTcp("localhost", 7000)
.subscribe(requester -> {
// ...
});
// Or block
RSocketRequester requester = RSocketRequester.builder()
.connectTcp("localhost", 7000)
.block(Duration.ofSeconds(5));
// Connect asynchronously
import org.springframework.messaging.rsocket.connectTcpAndAwait
class MyService {
private var requester: RSocketRequester? = null
private suspend fun requester() = requester ?:
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000).also { requester = it }
suspend fun doSomething() = requester().route(...)
}
// Or block
import org.springframework.messaging.rsocket.connectTcpAndAwait
class MyService {
private val requester = runBlocking {
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000)
}
suspend fun doSomething() = requester.route(...)
}
连接设置
RSocketRequester.Builder 提供了以下方法来自定义初始的 SETUP 帧:
-
dataMimeType(MimeType)— 设置连接上数据的 MIME 类型。 -
metadataMimeType(MimeType)— 设置连接上元数据的 MIME 类型。 -
setupData(Object)— 要包含在SETUP中的数据。 -
setupRoute(String, Object…)— 要在元数据中包含于SETUP中的路由。 -
setupMetadata(Object, MimeType)— 要在SETUP中包含的其他元数据。
对于数据,默认的 MIME 类型由第一个配置的 Decoder 决定。对于元数据,默认的 MIME 类型是
复合元数据(composite metadata),它允许每个请求包含多个元数据值和 MIME 类型对。通常情况下,这两者都不需要更改。
SETUP 帧中的数据和元数据是可选的。在服务器端,可以使用 @ConnectMapping 方法来处理连接的建立以及 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();
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcp("localhost", 7000);
import org.springframework.messaging.rsocket.connectTcpAndAwait
val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcpAndAwait("localhost", 7000)
RSocketStrategies 被设计为可重用的。在某些场景下,例如客户端和服务器位于同一个应用程序中,将其声明在 Spring 配置中可能是更优的选择。
客户端响应器
RSocketRequester.Builder 可用于配置对来自服务器的请求的响应。
你可以使用带注解的处理器,基于服务器端所用的相同基础设施来实现客户端响应,但需以编程方式注册,如下所示:
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();
SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.connectTcp("localhost", 7000);
| 1 | 如果存在 PathPatternRouteMatcher,请使用 spring-web 进行高效的路由匹配。 |
| 2 | 从一个包含 @MessageMaping 和/或 @ConnectMapping 方法的类创建响应器。 |
| 3 | 注册响应器。 |
import org.springframework.messaging.rsocket.connectTcpAndAwait
val strategies = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher()) (1)
.build()
val responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(responder) } (3)
.connectTcpAndAwait("localhost", 7000)
| 1 | 如果存在 PathPatternRouteMatcher,请使用 spring-web 进行高效的路由匹配。 |
| 2 | 从一个包含 @MessageMaping 和/或 @ConnectMapping 方法的类创建响应器。 |
| 3 | 注册响应器。 |
请注意,上述内容仅为用于以编程方式注册客户端响应器的快捷方式。在其他场景中,如果客户端响应器已在 Spring 配置中定义,您仍然可以将 RSocketMessageHandler 声明为一个 Spring Bean,然后按如下方式应用:
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.connectTcp("localhost", 7000);
import org.springframework.beans.factory.getBean
import org.springframework.messaging.rsocket.connectTcpAndAwait
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.connectTcpAndAwait("localhost", 7000)
对于上述情况,您可能还需要在 setHandlerPredicate 中使用 RSocketMessageHandler,
以切换到另一种用于检测客户端响应器(client responders)的策略,例如基于自定义注解
@RSocketClientResponder 而非默认的 @Controller。这在同一个应用程序中同时包含客户端与服务器,
或包含多个客户端的场景下是必要的。
另请参阅带注解的响应器,以了解更多关于该编程模型的信息。
高级
RSocketRequesterBuilder 提供了一个回调,用于暴露底层的 io.rsocket.core.RSocketConnector,以便进一步配置 keepalive 间隔、会话恢复、拦截器等选项。你可以在该层级按如下方式配置选项:
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.connectTcp("localhost", 7000);
import org.springframework.messaging.rsocket.connectTcpAndAwait
val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}.connectTcpAndAwait("localhost", 7000)
5.2.2. 服务器请求者
要从服务器向已连接的客户端发送请求,只需从服务器获取该已连接客户端的请求器即可。
在带注解的响应器中,@ConnectMapping 和 @MessageMapping 方法支持一个
RSocketRequester 参数。使用它可以访问该连接对应的请求方。请记住,@ConnectMapping 方法本质上是处理 SETUP 帧的处理器,
必须在开始处理请求之前完成处理。因此,在连接初始阶段发出的请求必须与处理逻辑解耦。例如:
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { (1)
// ...
});
return ... (2)
}
| 1 | 启动请求的异步处理,与请求处理过程相互独立。 |
| 2 | 执行处理并返回完成的 Mono<Void>。 |
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
// ...
}
}
/// ... (2)
}
| 1 | 启动请求的异步处理,与请求处理过程相互独立。 |
| 2 | 在挂起函数中执行处理。 |
5.2.3. 请求
ViewBox viewBox = ... ;
Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlux(AirportLocation.class); (3)
| 1 | 指定一个路由,以包含在请求消息的元数据中。 |
| 2 | 为请求消息提供数据。 |
| 3 | 声明预期的响应。 |
val viewBox: ViewBox = ...
val locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlow<AirportLocation>() (3)
| 1 | 指定一个路由,以包含在请求消息的元数据中。 |
| 2 | 为请求消息提供数据。 |
| 3 | 声明预期的响应。 |
交互类型由输入和输出的基数(cardinality)隐式确定。上述示例属于Request-Stream(Request-Stream),因为发送一个值并接收一个值的流。在大多数情况下,只要输入和输出的选择与 RSocket 交互类型以及响应方所期望的输入和输出类型相匹配,你无需过多考虑这一点。唯一无效的组合是“多对一”(many-to-one)。
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);
import org.springframework.messaging.rsocket.retrieveAndAwait
val location = requester.route("find.radar.EWR")
.retrieveAndAwait<AirportLocation>()
如果使用复合元数据(默认方式),并且所添加的值被已注册的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);
import org.springframework.messaging.rsocket.retrieveFlow
val requester: RSocketRequester = ...
val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")
val locations = requester.route("locate.radars.within")
.metadata(securityToken, mimeType)
.data(viewBox)
.retrieveFlow<AirportLocation>()
对于Fire-and-Forget场景,请使用返回send()的Mono<Void>方法。请注意,该Mono仅表示消息已成功发送,并不表示消息已被处理。
5.3. 带注解的响应器
RSocket 响应器可以实现为 @MessageMapping 和 @ConnectMapping 方法。
@MessageMapping 方法用于处理单个请求,而 @ConnectMapping 方法用于处理
连接级别的事件(建立连接和元数据推送)。带注解的响应器在服务端响应和客户端响应中均对称支持。
5.3.1. 服务器响应器
要在服务器端使用带注解的响应器,请在您的 Spring 配置中添加 RSocketMessageHandler,以自动检测带有 @Controller 和 @MessageMapping 方法的 @ConnectMapping Bean:
@Configuration
static class ServerConfig {
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.routeMatcher(new PathPatternRouteMatcher());
return handler;
}
}
@Configuration
class ServerConfig {
@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
routeMatcher = PathPatternRouteMatcher()
}
}
然后通过 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();
import org.springframework.beans.factory.getBean
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val server = RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.awaitFirst()
RSocketMessageHandler 默认支持
复合(composite) 和
路由(routing) 元数据。如果需要切换到其他 MIME 类型或注册额外的元数据 MIME 类型,可以设置其
MetadataExtractor。
你需要设置所需的 Encoder 和 Decoder 实例,以支持相应的元数据和数据格式。
你很可能需要 spring-web 模块来获取编解码器的实现。
默认情况下,使用 SimpleRouteMatcher 通过 AntPathMatcher 进行路由匹配。
我们建议接入来自 PathPatternRouteMatcher 的 spring-web,
以实现高效的路由匹配。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();
}
}
@Configuration
class ServerConfig {
@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
rSocketStrategies = rsocketStrategies()
}
@Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.routeMatcher(PathPatternRouteMatcher())
.build()
}
5.3.2. 客户端响应器
客户端的带注解的响应器需要在 RSocketRequester.Builder 中进行配置。详情请参见 客户端响应器。
5.3.3. @MessageMapping
@Controller
public class RadarsController {
@MessageMapping("locate.radars.within")
public Flux<AirportLocation> radars(MapRequest request) {
// ...
}
}
@Controller
class RadarsController {
@MessageMapping("locate.radars.within")
fun radars(request: MapRequest): Flow<AirportLocation> {
// ...
}
}
上述 @MessageMapping 方法用于响应路由为 "locate.radars.within" 的请求-流(Request-Stream)交互。它支持灵活的方法签名,可选择使用以下方法参数:
| 方法参数 | 描述 |
|---|---|
|
请求的有效载荷。这可以是诸如 注意: 使用该注解是可选的。如果一个方法参数不是简单类型,也不属于其他受支持的参数类型,则默认将其视为预期的有效载荷(payload)。 |
|
用于向远程端发起请求的请求器。 |
|
根据映射模式中的变量从路由中提取的值,例如
|
|
如MetadataExtractor中所述,已注册用于提取的元数据值。 |
|
所有按照MetadataExtractor中所述注册用于提取的元数据值。 |
返回值应为一个或多个将被序列化为响应负载(payloads)的对象。这可以是异步类型,例如 Mono 或 Flux、一个具体值,或者为 void 或无值的异步类型,例如 Mono<Void>。
@MessageMapping 方法所支持的 RSocket 交互类型由输入(即 @Payload 参数)和输出的基数(cardinality)决定,其中基数的含义如下:
| 基数 | 描述 |
|---|---|
1 |
可以是一个显式值,也可以是单值异步类型,例如 |
许多 |
一种多值异步类型,例如 |
0 |
对于输入而言,这意味着该方法没有 对于输出,这是 |
下表展示了所有输入和输出基数的组合及其对应的交互类型:
| 输入基数 | 输出基数 | 交互类型 |
|---|---|---|
0, 1 |
0 |
即发即忘、请求-响应 |
0, 1 |
1 |
Request-Response |
0, 1 |
许多 |
Request-Stream |
许多 |
0、1、多个 |
Request-Channel |
5.3.4. @ConnectMapping
@ConnectMapping 用于处理 RSocket 连接开始时的 SETUP 帧,以及后续通过 METADATA_PUSH 帧发送的任何元数据推送通知,即 metadataPush(Payload) 中的 io.rsocket.RSocket。
@ConnectMapping 方法支持与 @MessageMapping 相同的参数,但这些参数基于 SETUP 和 METADATA_PUSH 帧中的元数据和数据。@ConnectMapping 可以包含一个模式(pattern),用于将处理限定到元数据中包含特定路由的连接;如果没有声明任何模式,则所有连接都会匹配。
@ConnectMapping 方法不能返回数据,其返回值必须声明为 void 或
Mono<Void>。如果在处理新连接时发生错误,则该连接将被拒绝。处理过程中不得阻塞以向该连接的 RSocketRequester 发起请求。详情请参见
服务器端 Requester。
5.4. 元数据提取器
响应方必须解析元数据。 复合元数据(Composite metadata)允许使用各自独立格式的元数据值(例如用于路由、安全、追踪等),每种元数据都有自己的 MIME 类型。应用程序需要一种方式来配置所支持的元数据 MIME 类型,以及一种方式来访问已提取的值。
MetadataExtractor 是一个契约,用于接收序列化的元数据并返回解码后的名称-值对,这些名称-值对随后可通过名称像访问消息头一样进行访问,例如在带注解的处理方法中通过 @Header 进行访问。
DefaultMetadataExtractor 可以接收 Decoder 实例来解码元数据。开箱即用,它内置支持
"message/x.rsocket.routing.v0",
会将其解码为 String 并保存在 "route" 键下。对于其他任何 MIME 类型,您需要提供一个 Decoder,
并按如下方式注册该 MIME 类型:
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
复合元数据非常适合用于组合独立的元数据值。然而,请求方可能不支持复合元数据,或者可能选择不使用它。在这种情况下,DefaultMetadataExtractor 可能需要自定义逻辑,将解码后的值映射到输出的 Map 中。以下是一个使用 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);
});
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
outputMap.putAll(jsonMap)
}
通过 MetadataExtractor 配置 RSocketStrategies 时,可以让
RSocketStrategies.Builder 使用已配置的解码器创建提取器,并
只需使用回调来定制注册项,如下所示:
RSocketStrategies strategies = RSocketStrategies.builder()
.metadataExtractorRegistry(registry -> {
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
// ...
})
.build();
import org.springframework.messaging.rsocket.metadataToExtract
val strategies = RSocketStrategies.builder()
.metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
registry.metadataToExtract<Foo>(fooMimeType, "foo")
// ...
}
.build()
6. 响应式库
spring-webflux 依赖于 reactor-core,并在内部使用它来组合异步逻辑以及提供 Reactive Streams 支持。通常,WebFlux 的 API 返回 Flux 或 Mono(因为内部使用了它们),并且在接收输入时宽松地接受任何 Reactive Streams 的 Publisher 实现。使用 Flux 还是 Mono 非常重要,因为它有助于表达基数(cardinality)——例如,预期的是单个还是多个异步值,这一点在做决策时可能至关重要(例如,在编码或解码 HTTP 消息时)。
对于带注解的控制器,WebFlux 会透明地适配应用程序所选的响应式库。这是借助
ReactiveAdapterRegistry 实现的,它
为响应式库和其他异步类型提供了可插拔的支持。该注册表内置了对 RxJava 和 CompletableFuture 的支持,但您也可以注册其他库。
对于功能性 API(例如 功能性端点、WebClient 等),适用于 WebFlux API 的通用规则同样适用——将 Flux 和 Mono 作为返回值,并将响应式流 Publisher 作为输入。当提供 Publisher(无论是自定义的还是来自其他响应式库)时,它只能被视为具有未知语义(0..N)的流。然而,如果其语义已知,您可以使用 Flux 或 Mono.from(Publisher) 对其进行包装,而不是直接传递原始的 Publisher。
例如,给定一个不是 Publisher 的 Mono,Jackson JSON 消息写入器会预期接收到多个值。如果媒体类型表示无限流(例如 application/json+stream),则各个值会被单独写入并立即刷新。否则,这些值会被缓冲到一个列表中,并以 JSON 数组的形式进行渲染。