Web 响应式
1. Spring WebFlux
Spring Framework中最初包含的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中并存。每个模块都是可选的。
应用程序可以使用其中一个模块或在某些情况下同时使用两个模块——例如,Spring MVC控制器与响应式WebClient。
1.1. 概览
Spring WebFlux 是为什么创建的?
部分答案是需要一个非阻塞的Web堆栈来处理并发,使用少量线程,并且在较少的硬件资源下进行扩展。Servlet 3.1确实提供了一个非阻塞I/O的API。然而,使用它会导致偏离其余的Servlet API,其中契约是同步的(Filter、Servlet)或阻塞的(getParameter、getPart)。这是为任何非阻塞运行时提供一个新的通用API作为基础的动力。这对于服务器(如Netty)来说很重要,这些服务器在异步、非阻塞领域已经非常成熟。
另一个答案部分是函数式编程。就像Java 5中注解的添加创造了机会(例如注解REST控制器或单元测试),Java 8中lambda表达式的添加为Java中的函数式API创造了机会。这对于非阻塞应用程序和延续风格的API(如CompletableFuture和ReactiveX所普及的那样)允许声明式组合异步逻辑。在编程模型级别,Java 8使Spring WebFlux能够提供函数式Web端点以及注解控制器。
1.1.1. 定义“响应式”
我们提到了“非阻塞”和“函数式”,但“响应式”是什么意思?
术语“响应式”指的是围绕对变化做出反应而构建的编程模型——网络组件对I/O事件做出反应,UI控制器对鼠标事件做出反应,等等。 从这个意义上说,非阻塞是响应式的,因为现在我们不再被阻塞,而是处于对操作完成或数据可用的通知做出反应的状态。
我们Spring团队还与“响应式”相关的另一个重要机制是非阻塞的背压。在同步、命令式代码中,阻塞调用作为一种自然形式的背压,迫使调用者等待。在非阻塞代码中,控制事件速率变得很重要,以防止快速生产者淹没其目的地。
Reactive Streams 是一个 小规范 (也在 Java 9 中采用) 定义了异步组件之间的交互以及背压。 例如,数据存储库(作为 发布者) 可以生成数据,HTTP 服务器(作为 订阅者) 然后将数据写入响应。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 或其他响应式库的使用。有关更多详细信息,请参见 响应式库。
| 除了响应式API之外,WebFlux还可以与Kotlin中的协程API一起使用,这提供了一种更命令式的编程风格。以下Kotlin代码示例将使用协程API提供。 |
1.1.3. 编程模型
The spring-web 模块包含了 Spring WebFlux 的响应式基础,包括 HTTP 抽象、受支持服务器的 Reactive Streams 适配器、编解码器,以及与 Servlet API 类似的但具有非阻塞契约的核心 WebHandler API。
在这个基础上,Spring WebFlux 提供了两种编程模型的选择:
1.1.4. 适用性
Spring MVC 还是 WebFlux?
一个自然的问题,但却建立了一个不合理的二分法。实际上,两者共同作用以扩展可用选项的范围。这两者设计上注重连续性和一致性,它们并行存在,来自每一边的反馈都对两边有益。下图展示了两者之间的关系,它们的共同点,以及各自独特支持的内容:

我们建议您考虑以下具体要点:
-
如果你有一个运行良好的Spring MVC应用程序,就没有必要进行更改。 命令式编程是编写、理解和调试代码最容易的方式。 你可以选择最多的库,因为从历史上看,大多数库都是阻塞的。
-
如果你已经在寻找非阻塞的Web堆栈,Spring WebFlux 提供了与该领域其他技术相同的执行模型优势,并且还提供了服务器的选择(Netty、Tomcat、Jetty、Undertow 和 Servlet 3.1+ 容器)、编程模型的选择(注解控制器和函数式Web端点)以及响应式库的选择(Reactor、RxJava 或其他)。
-
如果你对使用Java 8 lambda或Kotlin的轻量级、功能性的Web框架感兴趣,可以使用Spring WebFlux功能性的Web端点。对于需求不太复杂的小型应用程序或微服务,这也是一个不错的选择,这些应用可以从更高的透明性和控制力中受益。
-
在微服务架构中,你可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器的应用程序,或者具有 Spring WebFlux 函数式端点的应用程序。在这两个框架中支持相同的基于注解的编程模型使得在重用知识的同时也能选择合适的工具来完成合适的工作变得更加容易。
-
评估应用程序的一种简单方法是检查其依赖项。如果你有阻塞的持久化API(如JPA、JDBC)或网络API需要使用,那么在常见的架构中,Spring MVC是最好的选择。从技术上讲,使用Reactor和RxJava在单独的线程上执行阻塞调用是可行的,但这样并不能充分利用非阻塞的Web堆栈。
-
如果你有一个Spring MVC应用程序,并且有调用远程服务的请求,尝试使用响应式
WebClient。 你可以直接从Spring MVC控制器方法中返回响应式类型(Reactor, RxJava, 或其他)。 每次调用的延迟越大或调用之间的相互依赖性越大,收益就越显著。Spring MVC控制器也可以调用其他响应式组件。 -
如果你有一个大型团队,请记住在转向非阻塞、函数式和声明式编程时会有陡峭的学习曲线。一个实用的开始方法是在不完全转换的情况下使用响应式
WebClient。除此之外,从小处着手并衡量其好处。我们预计,对于广泛的应用程序来说,这种转变是不必要的。如果你不确定要寻找哪些好处,可以先了解一下非阻塞I/O的工作原理(例如,单线程Node.js上的并发)及其影响。
1.1.5. 服务器
Spring WebFlux 支持 Tomcat、Jetty、Servlet 3.1+ 容器,以及 Netty 和 Undertow 等非 Servlet 运行时。所有服务器都被适配为低级别的通用 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。它不暴露以供直接使用。
对于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 提供了线程池抽象,称为调度器,用于与
publishOn操作符一起使用,该操作符用于切换到不同的线程池进行处理。调度器的名称暗示了一种特定的并发策略——例如,“parallel”(适用于有限数量线程的 CPU 密集型工作)或“elastic”(适用于大量线程的 I/O 密集型工作)。如果你看到这样的线程,这意味着某些代码正在使用特定的线程池Scheduler策略。 -
数据访问库和其他第三方依赖也可以创建和使用它们自己的线程。
1.2. 响应式核心
The spring-web 模块包含以下用于响应式 Web 应用程序的基础支持:
-
对于服务器请求处理,有两种级别的支持。
-
HttpHandler: HTTP请求处理的基本契约,支持非阻塞I/O和Reactive Streams背压,并提供Reactor Netty、Undertow、Tomcat、Jetty和任何Servlet 3.1+容器的适配器。
-
WebHandlerAPI: 稍高层次、通用目的的Web API,用于处理请求,在其上构建了具体的编程模型,如注解控制器和函数端点。
-
-
对于客户端,有一个基本的
ClientHttpConnector合约,用于执行HTTP请求,具有非阻塞I/O和Reactive Streams背压,并且包含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 用于读写 ByteBuffers 而不是 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中。该类包装了一个HttpHandler与ServletHttpHandlerAdapter并注册
它作为一个Servlet。
1.2.2. WebHandler API
The org.springframework.web.server 包基于 HttpHandler 合同
提供一个通用的Web API,用于通过多个
WebExceptionHandler、多个
WebFilter 和一个单一的
WebHandler 组件处理请求。该链可以通过 WebHttpHandlerBuilder 简单地指向一个 Spring
ApplicationContext 来组合,在那里组件是
自动检测 的,并/或通过向构建器注册组件。
While HttpHandler 有一个简单的目标,即抽象不同 HTTP 服务器的使用,WebHandler API 旨在提供一组在 Web 应用程序中常用的功能,例如:
-
用户会话及其属性。
-
请求属性。
-
Resolved
Locale或Principal用于请求。 -
访问解析和缓存的表单数据。
-
多部分数据的抽象。
-
和更多...
特殊bean类型
以下表格列出了WebHttpHandlerBuilder可以在Spring ApplicationContext中自动检测的组件,或者可以直接注册到其中的组件:
| 豆名称 | Beans类型 | 计数 | 描述 |
|---|---|---|---|
任何 |
|
0..N |
为来自 |
任何 |
|
0..N |
将拦截样式逻辑应用于过滤器链的其余部分和目标 |
|
|
1 |
处理请求的处理器。 |
|
|
0..1 |
管理器通过 |
|
|
0..1 |
对于访问 |
|
|
0..1 |
The resolver for |
|
|
0..1 |
对于转发类型标头的处理,可以通过提取并删除它们或仅删除它们来实现。默认情况下不使用。 |
表单数据
ServerWebExchange 提供了以下方法来访问表单数据:
Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>
The DefaultServerWebExchange 使用配置的 HttpMessageReader 来解析表单数据
(application/x-www-form-urlencoded) 成为一个 MultiValueMap. 默认情况下,
FormHttpMessageReader 被配置用于 ServerCodecConfigurer bean
(参见 Web Handler API)。
多部分数据
ServerWebExchange 提供了以下方法来访问多部分数据:
Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>
The DefaultServerWebExchange 使用配置的
HttpMessageReader<MultiValueMap<String, Part>> 来解析 multipart/form-data 内容
为一个 MultiValueMap。目前,
Synchronoss NIO Multipart 是唯一
支持的第三方库,也是我们所知的唯一用于非阻塞解析
multipart 请求的库。它通过 ServerCodecConfigurer bean
启用(请参阅 Web Handler API)。
要以流式方式解析多部分数据,您可以使用从HttpMessageReader<Part>返回的Flux<Part>。例如,在注解控制器中,使用@RequestPart意味着按名称访问各个部分的Map-like访问,并因此需要完全解析多部分数据。相反,您可以使用@RequestBody将内容解码为Flux<Part>,而无需收集到MultiValueMap。
请求头转发
当请求通过代理(如负载均衡器)时,主机、端口和协议可能会发生变化。这使得从客户端的角度来看,创建指向正确主机、端口和协议的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 标头,代理可以使用该标头提供有关原始请求的信息。还有其他非标准标头,包括 X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderTransformer 是一个组件,它根据请求头转发修改请求的主机、端口和协议,然后删除这些标头。如果你将其声明为名称为 forwardedHeaderTransformer 的 bean,它将被
检测到 并使用。
转发头可能存在安全问题,因为应用程序无法知道这些头是被代理按预期添加的,还是被恶意客户端添加的。这就是为什么在信任边界的代理应该配置为移除来自外部的不可信转发流量。你也可以配置ForwardedHeaderTransformer与removeOnly=true,在这种情况下,它会移除但不使用这些头。
在 5.1 中,ForwardedHeaderFilter 已被弃用并被 ForwardedHeaderTransformer 取代,因此请求头转发可以在交换创建之前更早地进行处理。如果无论如何都配置了该过滤器,则会将其从过滤器列表中移除,并使用 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实现:
| 异常处理 | 描述 |
|---|---|
|
为类型为
|
|
Extension of 此处理器在WebFlux Config中声明。 |
1.2.5. 编码解码器
The spring-web 和 spring-core 模块提供了通过非阻塞 I/O 与 Reactive Streams 背压进行序列化和反序列化字节内容到高级对象的支持。以下描述了这种支持:
-
HttpMessageReader和HttpMessageWriter是用于编码和解码 HTTP 消息内容的合同。 -
一个
Encoder可以用EncoderHttpMessageWriter包装,以便在 Web 应用程序中使用,而一个Decoder可以用DecoderHttpMessageReader包装。 -
DataBuffer抽象了不同的字节缓冲区表示(例如 NettyByteBuf,java.nio.ByteBuffer等),并且是所有编解码器工作的基础。有关此主题的更多信息,请参见“Spring 核心”部分中的数据缓冲区和编解码器。
The spring-core 模块提供了 byte[]、ByteBuffer、DataBuffer、Resource、和
String 编码器和解码器实现。The spring-web 模块提供了 Jackson
JSON、Jackson Smile、JAXB2、Protocol Buffers 以及其他编码器和解码器,同时还提供了
仅限 Web 的 HTTP 消息读取器和写入器实现,用于表单数据、多部分内容、服务器发送的事件等。
ClientCodecConfigurer 和 ServerCodecConfigurer 通常用于配置和
自定义要在应用程序中使用的编解码器。请参见有关配置
HTTP 消息编解码器的部分。
Jackson JSON
JSON 和二进制 JSON (Smile) 都在 Jackson 库存在时得到支持。
The Jackson2Decoder works as follows:
-
Jackson的异步、非阻塞解析器用于聚合字节块流 成
TokenBuffer,每个代表一个JSON对象。 -
每个
TokenBuffer都被传递给 Jackson 的ObjectMapper以创建一个更高级的对象。 -
当解码为单值发布者(例如
Mono)时,有一个TokenBuffer。 -
当解码为多值发布者(例如
Flux)时,每个TokenBuffer在接收到足够字节以形成完整对象后立即传递给ObjectMapper。输入内容可以是 JSON 数组,或者 行分隔的 JSON 如果内容类型是application/stream+json。
The Jackson2Encoder works as follows:
-
对于单值发布者(例如
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 进行实际解析。
在服务器端,当可能需要从多个地方访问multipart表单内容时,ServerWebExchange 提供了一个专门的 getMultipartData() 方法,该方法通过 MultipartHttpMessageReader 解析内容,然后缓存结果以供重复访问。请参见 Multipart Data 在 WebHandler API 部分。
一旦使用了getMultipartData(),就无法再从请求体中读取原始的原始内容。因此,应用程序必须始终使用getMultipartData()来重复、类似映射地访问部分,或者依赖于SynchronossPartHttpMessageReader进行一次性访问Flux<Part>。
限制
Decoder 和 HttpMessageReader 实现可以配置为在内存中缓冲部分或全部输入流,并且可以设置最大缓冲字节数的限制。
在某些情况下,缓冲发生是因为输入被聚合并表示为单个对象——例如,具有 @RequestBody byte[]、x-www-form-urlencoded 数据等的控制器方法。缓冲也可能发生在流式处理时,例如分割输入流——例如,分隔文本、JSON 对象流等。对于这些流式处理情况,限制适用于流中一个对象关联的字节数。
要配置缓冲区大小,您可以检查给定的Decoder或HttpMessageReader是否暴露了maxInMemorySize属性,如果暴露了,Javadoc将提供有关默认值的详细信息。在服务器端,ServerCodecConfigurer提供了一个设置所有编解码器的单一位置,详情请参见HTTP消息编解码器。在客户端,可以在WebClient.Builder中更改所有编解码器的限制。
对于 多部分解析,maxInMemorySize 属性限制了非文件部分的大小。对于文件部分,它确定了将部分写入磁盘的阈值。对于写入磁盘的文件部分,还有一个 maxDiskUsagePerPart 属性来限制每个部分的磁盘空间量。还有一个 maxParts 属性来限制multipart请求中的部分总数。要在WebFlux中配置这三个属性,您需要向 ServerCodecConfigurer 提供一个预配置的 MultipartHttpMessageReader 实例。
流式传输
当向HTTP响应流式传输数据(例如,text/event-stream, application/stream+json)时,定期发送数据非常重要,以便能够尽早可靠地检测到已断开连接的客户端。这种发送可以是一个仅包含注释的空SSE事件或任何其他“无操作”数据,这些数据实际上可以作为心跳。
DataBuffer
DataBuffer 是 WebFlux 中字节缓冲区的表示。Spring Core 部分的参考文档在
数据缓冲区和编解码器一节中有更多相关内容。关键点是理解在某些服务器(如 Netty)上,字节缓冲区是被池化和引用计数的,并且在使用后必须释放以避免内存泄漏。
WebFlux应用程序通常不需要关心此类问题,除非它们直接消费或生成数据缓冲区,而不是依赖编解码器将数据转换为更高层次的对象,或者除非它们选择创建自定义编解码器。对于此类情况,请参阅数据缓冲区和编解码器中的信息,特别是使用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 声明为具有 bean 名称 webHandler,
则它反过来会被 WebHttpHandlerBuilder 发现,
后者将组件组合成一个请求处理链,如 WebHandler API 中所述。
在WebFlux应用程序中,Spring配置通常包含:
-
DispatcherHandler具有 bean 名称webHandler -
WebFilter和WebExceptionHandlerbeans -
其他人
配置被赋予WebHttpHandlerBuilder以构建处理链,
如下例所示:
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
结果的 HttpHandler 已准备好与 服务器适配器 一起使用。
1.3.1. 特殊的Bean类型
The DispatcherHandler 委托给特殊 bean 来处理请求并渲染适当的响应。所谓的“特殊 bean”是指实现了 WebFlux 框架契约的 Spring 管理的 Object 实例。这些通常带有内置契约,但您可以自定义它们的属性、扩展它们或替换它们。
以下表格列出了由DispatcherHandler检测到的特殊bean。请注意,还有一些在较低级别检测到的其他bean(请参见Web Handler API中的特殊bean类型)。
| Beans类型 | 说明 |
|---|---|
|
将请求映射到处理器。映射基于某些标准,具体细节因 主要的 |
|
帮助 |
|
处理处理器调用的结果并最终确定响应。 见结果处理。 |
1.3.2. WebFlux 配置
应用程序可以声明处理请求所需的基础设施bean(列在Web Handler API和DispatcherHandler下)。但是,在大多数情况下,WebFlux Config是最好的起点。它声明了所需的bean,并提供了一个更高层次的配置回调API来定制它。
| Spring Boot 依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供了许多额外的便捷选项。 |
1.3.3. 处理
DispatcherHandler 处理请求如下:
-
每个
HandlerMapping都会被要求找到一个匹配的处理器,并且使用第一个匹配项。 -
如果找到了处理器,它将通过适当的
HandlerAdapter运行,该HandlerAdapter会将执行的返回值暴露为HandlerResult。 -
The
HandlerResultis given to an appropriateHandlerResultHandlerto complete processing by writing to the response directly or by using a view to render.
1.3.4. 结果处理
通过调用处理器返回的值,通过一个HandlerAdapter,被包装为一个HandlerResult,并附带一些额外的上下文信息,传递给第一个声明支持它的HandlerResultHandler。下表列出了可用的HandlerResultHandler实现,所有这些实现都在WebFlux Config中声明:
| 结果处理器类型 | 返回值 | 默认顺序 |
|---|---|---|
|
|
0 |
|
|
0 |
|
处理从 |
100 |
|
另请参见 查看解决方案。 |
|
1.3.5. 异常
从 HandlerAdapter 返回的 HandlerResult 可以基于某种处理器特定的机制暴露一个用于错误处理的功能。如果满足以下条件,将调用此错误函数:
-
处理器(例如,
@Controller)调用失败。 -
处理处理器返回值通过
HandlerResultHandler失败。
错误处理函数可以更改响应(例如,更改为错误状态),只要在处理器返回的响应式类型产生任何数据项之前发生错误信号即可。
这是如何支持 @ExceptionHandler 个方法在 @Controller 个类中。
相比之下,Spring MVC 中对相同内容的支持是基于一个 HandlerExceptionResolver。
这通常不应该有影响。但是,请记住,在 WebFlux 中,您不能使用
@ControllerAdvice 来处理在选择处理器之前发生的异常。
1.3.6. 视图解析
视图解析功能允许使用HTML模板和模型向浏览器呈现,而无需绑定到特定的视图技术。在Spring WebFlux中,视图解析通过专用的HandlerResultHandler支持,该处理器使用ViewResolver实例将字符串(表示逻辑视图名称)映射到View实例。View然后用于渲染响应。
处理
传递给 ViewResolutionResultHandler 的 HandlerResult 包含处理器返回的值以及在请求处理期间添加的属性的模型。返回值将按以下方式之一进行处理:
-
String,CharSequence: 一个逻辑视图名称,通过配置的ViewResolver实现列表解析为View。 -
void: 根据请求路径选择一个默认视图名称,去掉前导和尾随斜杠,并将其解析为View。当未提供视图名称时(例如,返回了模型属性)或异步返回值为空时(例如,Mono完成为空),也会发生同样的情况。 -
渲染: API 用于视图解析场景。在您的 IDE 中使用代码补全来探索选项。
-
Model,Map: 为请求添加到模型中的额外模型属性。 -
其他任何:其他任何返回值(除了简单类型,由 BeanUtils#isSimpleProperty 确定) 被视为要添加到模型中的模型属性。属性名称通过使用 约定 从类名派生, 除非存在带有
@ModelAttribute注解的处理器方法。
模型可以包含异步、响应式类型(例如,来自Reactor或RxJava)。在渲染之前,AbstractView将这些模型属性解析为具体值并更新模型。单值响应式类型被解析为单个值或无值(如果为空),而多值响应式类型(例如,Flux<T>)被收集并解析为List<T>。
要配置视图解析,只需在您的Spring配置中添加一个ViewResolutionResultHandler bean。WebFlux 配置提供了一个专门的配置API用于视图解析。
See 视图技术 for more on the view technologies integrated with Spring WebFlux.
重定向
特殊前缀 redirect: 在视图名称中允许你执行重定向。UrlBasedViewResolver(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向URL。
最终效果与控制器返回RedirectView或Rendering.redirectTo("abc").build()相同,但现在控制器本身可以在逻辑视图名称的术语下操作。像redirect:/some/resource这样的视图名称相对于当前应用程序,而像redirect:https://example.com/arbitrary/path这样的视图名称重定向到绝对URL。
内容协商
ViewResolutionResultHandler 支持内容协商。它将请求的媒体类型与每个选定 View 支持的媒体类型进行比较。首先使用支持请求的媒体类型(s) 的 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 立体类型允许自动检测,并与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. 请求映射
The @RequestMapping 注解用于将请求映射到控制器方法。它有多种属性可以匹配 URL、HTTP 方法、请求参数、标头和媒体类型。您可以在类级别使用它来表达共享映射,或在方法级别使用它来缩小到特定的端点映射。
还有针对HTTP方法的@RequestMapping的快捷变体:
-
@GetMapping -
@PostMapping -
@PutMapping -
@DeleteMapping -
@PatchMapping
上述注解是自定义注解,它们被提供是因为,可以说,大多数控制器方法应该映射到特定的HTTP方法,而不是使用@RequestMapping,默认情况下,@RequestMapping匹配所有HTTP方法。同时,在类级别仍然需要一个@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 模式
您可以使用通配符模式和通配符来映射请求:
| 模式 | 描述 | 示例 |
|---|---|---|
|
匹配一个字符 |
|
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径结束 |
|
|
匹配路径段并将其捕获为名为"name"的变量 |
|
|
匹配正则表达式 |
|
|
匹配零个或多个路径段,直到路径结束,并将其捕获为名为 "path" 的变量。 |
|
捕获的URI变量可以通过@PathVariable访问,如下例所示:
@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 编译器标志,则可以省略该细节。
The syntax {*varName} declares a URI variable that matches zero or more remaining path
segments. For example /resources/{*path} matches all files under /resources/, and the
"path" variable captures the complete relative 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 中,并且专门设计用于在具有大量 URI 路径模式的 Web 应用程序中与 HTTP URL 路径一起使用,这些路径模式在运行时进行匹配。 |
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) {
// ...
}
consumes属性还支持否定表达式——例如,!text/plain表示任何
内容类型,除了text/plain。
您可以在类级别声明一个共享的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 方法列表来处理。
对于一个@RequestMapping没有HTTP方法声明,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方法,而不是使用 @RequestMapping,
默认情况下,@RequestMapping 匹配所有HTTP方法。如果你需要组合
注解的例子,看看这些是如何声明的。
Spring WebFlux 还支持带有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要子类化 RequestMappingHandlerMapping 并重写 getCustomMethodCondition 方法,在这里你可以检查自定义属性并返回你自己的 RequestCondition。
显式注册
您可以编程地注册处理器方法,这些方法可以用于动态注册或高级情况,例如在不同 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 处理方法具有灵活的签名,并可以从一系列支持的控制器方法参数和返回值中进行选择。
方法参数
下表列出了支持的控制器方法参数。
响应式类型(Reactor、RxJava、或其他)在需要阻塞 I/O(例如,读取请求体)才能解决的参数上是受支持的。这在描述列中标记。对于不需要阻塞的参数,不期望使用响应式类型。
JDK 1.8 的 java.util.Optional 作为方法参数被支持,并且可以与具有 required 属性的注解结合使用(例如,@RequestParam、@RequestHeader 等),并且等价于 required=false。
| 控制器方法参数 | 描述 |
|---|---|
|
对整个 |
|
访问 HTTP 请求或响应。 |
|
访问会话。这不会强制开始一个新会话,除非添加了属性。支持响应式类型。 |
|
当前已认证的用户——可能是一个特定的 |
|
请求的HTTP方法。 |
|
当前请求的本地化设置,由最具体的 |
|
与当前请求相关联的时区,由 |
|
访问URI模板变量。请参见URI模式。 |
|
用于访问URI路径段中的名称值对。请参见矩阵变量。 |
|
用于访问Servlet请求参数。参数值将转换为声明的方法参数类型。请参见 注意 |
|
用于访问请求头。头值将转换为声明的方法参数类型。请参见 |
|
用于访问cookie。Cookie值将转换为声明的方法参数类型。
见 |
|
对于访问HTTP请求正文。正文内容通过使用 |
|
对于访问请求头和正文。正文通过 |
|
对于在 |
|
用于访问在HTML控制器中使用的模型,并作为视图渲染的一部分暴露给模板。 |
|
对于访问模型中现有属性(如果不存在则实例化)并应用数据绑定和验证。请参见 注意 |
|
对于访问验证和数据绑定错误的命令对象,即 |
|
用于标记表单处理完成,这将触发通过类级别 |
|
用于根据当前请求的主机、端口、协议和路径准备相对URL。 见 URI 链接。 |
|
对于访问任何会话属性——与作为类级别 |
|
用于访问请求属性。有关更多详细信息,请参见 |
任何其他参数 |
如果一个方法参数没有匹配到上述任何一种情况,默认情况下,它将被解析为
|
返回值
以下表格显示了支持的控制器方法返回值。请注意,来自Reactor、RxJava或其他库的响应式类型通常支持所有返回值。
| 控制器方法返回值 | 描述 |
|---|---|
|
返回值通过 |
|
返回值指定了完整的响应,包括HTTP头,并且正文通过 |
|
对于返回带有标头但无正文的响应。 |
|
一个视图名称将与 |
|
一个 |
|
要添加到隐式模型中的属性,视图名称根据请求路径隐式确定。 |
|
要添加到模型中的属性,视图名称根据请求路径隐式确定。 请注意, |
|
一个用于模型和视图渲染场景的API。 |
|
一个具有 如果上述情况都不成立,返回类型为 |
|
发送服务器发送的事件。当只需要写入数据时,可以省略 |
任何其他返回值 |
如果返回值与上述任何一项都不匹配,默认情况下,它被视为视图名称,如果是 |
类型转换
某些注解控制器方法参数表示基于字符串的请求输入(例如,@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
您可以使用@注解将查询参数绑定到控制器方法参数。以下代码片段展示了使用方法:
@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。 |
The Servlet API “request parameter” concept conflates query parameters, form data, and multiparts into one. However, in WebFlux, each is accessed individually through
ServerWebExchange. While @RequestParam binds to query parameters only, you can use
data binding to apply query parameters, form data, and multiparts to a
command object. |
Method parameters that use the @RequestParam annotation are required by default, but
you can specify that a method parameter is optional by setting the required flag of a @RequestParam
to false or by declaring the argument with a java.util.Optional
wrapper.
类型转换会自动应用,如果目标方法参数类型不是String。请参见类型转换。
当一个@RequestParam注解被声明在Map<String, String>或
MultiValueMap<String, String>参数上时,该映射将填充所有查询参数。
请注意,使用 @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 注解在方法参数上访问模型中的属性,或者在不存在时实例化它。模型属性还覆盖与查询参数和表单字段名称匹配的字段名称的值。这被称为数据绑定,它可以节省您处理解析和转换单个查询参数和表单字段的时间。以下示例绑定一个 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 的一个实例。 |
The Pet 实例在前面的示例中解析如下:
-
从模型中如果已经通过
Model添加。 -
从HTTP会话通过
@SessionAttributes。 -
从默认构造函数的调用。
-
从调用带有与查询参数或表单字段匹配的参数的“主构造函数”开始。参数名称通过JavaBeans
@ConstructorProperties或运行时保留的字节码中的参数名称来确定。
在获取模型属性实例后,数据绑定将被应用。WebExchangeDataBinder 类会将查询参数和表单字段的名称与目标 Object 上的字段名称进行匹配。在应用类型转换后,匹配的字段将被填充。有关数据绑定(和验证)的更多信息,请参见
验证。有关自定义数据绑定的更多信息,请参见
DataBinder。
数据绑定可能会导致错误。默认情况下,会抛出一个 WebExchangeBindException,但是,在控制器方法中检查此类错误,你可以在 @ModelAttribute 立即下一个位置添加一个 BindingResult 参数,如下例所示:
@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 validation)。以下示例使用@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 WebFlux 与 Spring MVC 不同,支持模型中的响应式类型——例如,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
如果您需要访问预先存在的全局管理的会话属性(即,在控制器之外管理的,例如,通过过滤器)并且这些属性可能不存在,您可以使用@SessionAttributes注解在方法参数上,如下例所示:
@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。 |
多部分内容
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 验证。验证
错误会导致一个 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 {
// ...
}
要访问所有多部分数据作为 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。 |
要按顺序访问多部分数据,以流式方式,可以使用@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注解来读取和反序列化请求体为一个3通过一个HttpMessageReader。
以下示例使用了一个@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 注解,这会导致 Standard 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
您可以使用@注解在方法上,以便将返回序列化到响应体中,通过一个HttpMessageWriter。以下示例展示了如何操作:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody 在类级别也受支持,在这种情况下,它将被所有控制器方法继承。这是 @RestController 的效果,它实际上只是一个用 @Controller 和 @ResponseBody 标记的元注解。
你可以结合使用@ResponseBody方法与JSON序列化视图。
有关详细信息,请参阅Jackson JSON。
您可以使用HTTP消息编解码器选项的WebFlux配置来配置或自定义消息写入。
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 的序列化视图,
这允许仅渲染 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注解:
-
在 方法参数 中,在
@RequestMapping个方法 创建或访问模型中的对象并将其绑定到请求通过WebDataBinder。 -
作为方法级别的注解在
@Controller或@ControllerAdvice类中,帮助在任何@RequestMapping方法调用之前初始化模型。 -
在
@RequestMapping方法中标记其返回值为模型属性。
本节讨论 @ModelAttribute 方法,或前面列表中的第二个项。
控制器可以有任意数量的 @ModelAttribute 方法。所有这些方法都在同一控制器中的 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器之间共享。有关详细信息,请参阅控制器建议部分。
@ModelAttribute 方法具有灵活的方法签名。它们支持与 @RequestMapping 方法相同的许多参数(除了 @ModelAttribute 本身和任何与请求正文相关的内容)。
以下示例使用了 @ModelAttribute 方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// 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);
}
When a name is not explicitly specified, a default name is chosen based on the type,
as explained in the javadoc for Conventions.
You can always assign an explicit name by using the overloaded addAttribute method or
through the name attribute on @ModelAttribute (for a return value). |
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 和其他)转换为目标类型的控制器方法参数。 -
Format model object values as
Stringvalues when rendering HTML forms.
@InitBinder 方法可以注册控制器特定的 java.bean.PropertyEditor 或
Spring Converter 和 Formatter 组件。此外,您可以使用
WebFlux Java 配置 在全局共享的 FormattingConversionService 中注册 Converter 和
Formatter 类型。
@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))
}
// ...
}
或者,当通过共享的FormattingConversionService使用基于Formatter的设置时,您可以重用相同的方法并注册控制器特定的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请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象的属性。
仅 public 个属性遵循
JavaBeans命名约定
并公开用于数据绑定 —— 例如,public String getFirstName() 和
public void setFirstName(String) 方法用于 firstName 属性。
| 模型对象及其嵌套的对象图有时也被称为命令对象、表单支持对象或POJO(简单的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;
}
}
如果你不能或不想为每个数据绑定用例使用一个专用的模型对象,你必须限制允许进行数据绑定的属性。理想情况下,你可以通过在WebDataBinder上使用setAllowedFields()方法来注册允许的字段模式。
例如,要在应用程序中注册允许的字段模式,您可以在@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 中)。
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的例子所示。或者,注解声明可以缩小匹配的异常类型。我们通常建议在参数签名中尽可能具体,并将主要根异常映射声明为优先级@ControllerAdvice,并设置相应的顺序。
请参见MVC部分以获取详细信息。
An @ExceptionHandler 方法在 WebFlux 中支持与 @RequestMapping 方法相同的参数和返回值,但不包括与请求正文和 @ModelAttribute 相关的参数。 |
Spring WebFlux 中对 @ExceptionHandler 方法的支持由 HandlerAdapter 为 @RequestMapping 方法提供。有关详细信息,请参阅 DispatcherHandler。
REST API 异常
对于REST服务的一个常见需求是在响应体中包含错误详细信息。Spring框架不会自动这样做,因为响应体中错误详细信息的表示是特定于应用程序的。但是,可以通过使用具有@RestController返回值的方法来设置响应的状态和体。此类方法也可以在@ControllerAdvice类中声明,以便全局应用它们。
请注意,Spring WebFlux 没有 Spring MVC 中 ResponseEntityExceptionHandler 的对应物,因为 WebFlux 只抛出 ResponseStatusException (或其子类),而这些异常不需要被转换为 HTTP 状态码。 |
1.4.7. 控制器建议
通常,@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法适用于声明它们的 @Controller 类(或类层次结构)中。如果您希望这些方法具有更全局的作用(跨控制器),您可以将它们声明在使用 @ControllerAdvice 或 @RestControllerAdvice 注解的类中。
@ControllerAdvice 被注解为 @Component,这意味着此类可以通过 组件扫描 注册为 Spring beans。@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>).
请求和响应对象都有不可变的契约,提供与JDK 8兼容的访问HTTP请求和响应的方式。
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 | 使用协程路由器DSL创建路由器,也可以通过router { }提供响应式替代方案。 |
一种运行RouterFunction的方法是将其转换为HttpHandler并通过内置的服务器适配器之一进行安装:
-
RouterFunctions.toHttpHandler(RouterFunction) -
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多数应用程序可以通过WebFlux Java配置运行,见运行服务器。
1.5.2. 处理函数
ServerRequest 和 ServerResponse 是不可变接口,提供 JDK 8 友好的访问 HTTP 请求和响应的方式。请求和响应都提供了对主体流的 Reactive Streams 回压支持。
请求体由 Reactor Flux 或 Mono 表示。
响应体可以使用任何 Reactive Streams Publisher 来表示,包括 Flux 和 Mono。
有关更多信息,请参见 Reactive 库。
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()
以下示例展示了如何将多部分数据作为映射来访问:
Mono<MultiValueMap<String, Part> map = request.multipartData();
val map = request.awaitMultipartData()
以下示例展示了如何以流式方式逐个访问多部分数据:
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse
ServerResponse 提供对 HTTP 响应的访问,并且由于它是不可变的,您可以使用 build 方法来创建它。您可以使用构建器设置响应状态、添加响应头或提供正文。以下示例创建了一个 200 (OK) 响应,带有 JSON 内容:
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(已创建)响应:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
根据所使用的编解码器,可以传递提示参数来定制主体的序列化或反序列化方式。例如,指定一个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>) 方法在收到完成信号时发送响应。 |
| 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响应抛出异常。 |
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
a global Validator instance based on LocalValidatorFactoryBean.
See Spring Validation.
1.5.3. RouterFunction
Router functions are used to route the requests to the corresponding HandlerFunction.
Typically, you do not write router functions yourself, but rather use a method on the
RouterFunctions utility class to create one.
RouterFunctions.route() (no parameters) provides you with a fluent builder for creating a router function,
whereas RouterFunctions.route(RequestPredicate, HandlerFunction) offers a direct way
to create a router.
通常,建议使用route()构建器,因为它为典型的映射场景提供了方便的快捷方式,而不需要难以发现的静态导入。
例如,路由器函数构建器提供了方法GET(String, HandlerFunction)来创建GET请求的映射;以及POST(String, HandlerFunction)用于POST请求。
除了基于HTTP方法的映射外,路由构建器还提供了一种在映射到请求时引入额外谓词的方法。对于每种HTTP方法,都有一个重载变体,该变体接受一个RequestPredicate作为参数,通过该参数可以表达额外的约束条件。
谓词
你可以编写自己的 RequestPredicate,但是RequestPredicates工具类提供了常用的实现,这些实现基于请求路径、HTTP方法、内容类型等。
以下示例使用请求谓词来根据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,该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 | GET /person/{id} 带有匹配JSON的 Accept 标头被路由到
PersonHandler.getPerson |
| 2 | GET /person 带有匹配JSON的 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 | GET /person/{id} 带有匹配JSON的 Accept 标头被路由到
PersonHandler.getPerson |
| 2 | GET /person 带有匹配JSON的 Accept 标头被路由到
PersonHandler.listPeople |
| 3 | POST /person 没有附加谓词时映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是一个在其他地方创建并添加到路由构建中的路由器函数。 |
嵌套路由
一组路由函数通常会有一个共同的谓词,例如共享路径。在上面的例子中,这个共同的谓词将是一个匹配/person的路径谓词,被三条路由使用。当使用注解时,你可以通过使用类型级别的@RequestMapping注解来消除这种重复。在WebFlux.fn中,可以通过路由器函数构建器上的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是一个接受路由器构建器的消费者。 |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以通过使用构建器上的nest方法来根据任何类型的谓词进行嵌套。
上述代码中仍然包含一些重复的部分,即共享的Accept-header谓词。
我们可以通过结合使用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 与多个服务器适配器配合使用,具体操作请遵循
HttpHandler 获取特定服务器的说明。
一个更典型的选择,也被Spring Boot使用,是通过基于DispatcherHandler的设置来运行,通过DispatcherHandler-based setup 通过WebFlux Config,它使用Spring配置来声明处理请求所需的组件。WebFlux Java配置声明了以下基础设施组件以支持功能性端点:
-
RouterFunctionMapping: 检测一个或多个RouterFunction<?>beans 在 Spring 配置中,通过RouterFunction.andOther将它们组合,并将请求路由到组合后的RouterFunction。 -
HandlerFunctionAdapter: 简单的适配器,允许DispatcherHandler调用 一个被映射到请求的HandlerFunction。 -
ServerResponseResultHandler: 处理通过调用HandlerFunction并调用ServerResponse的writeTo方法的结果。
上述组件让功能端点适应DispatcherHandler请求处理生命周期,并且(可能)与注解控制器并行运行,如果声明了注解控制器的话。这也是Spring Boot WebFluxStarters启用功能端点的方式。
以下示例展示了一个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. 过滤处理器函数
你可以通过使用路由函数构建器上的 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 | 添加自定义请求头的before过滤器仅应用于两个GET路由。 |
| 2 | The 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 | 添加自定义请求头的before过滤器仅应用于两个GET路由。 |
| 2 | The after 过滤器会记录响应,它被应用于所有路由,包括嵌套的路由。 |
The 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. 您可以创建一个
UriBuilder,进而使用 UriBuilderFactory. 一起,UriBuilderFactory 和 UriBuilder 提供了一个可插拔的机制来根据 URI 模板构建 URI,基于共享配置,例如基础 URL、编码偏好和其他细节。
您可以使用 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(): Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
-
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")
The WebClient and the RestTemplate expand and encode URI templates internally through
the UriBuilderFactory strategy. Both can be configured with a custom strategy.
as the following example shows:
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()
The DefaultUriBuilderFactory implementation uses UriComponentsBuilder internally to
expand and encode URI templates. As a factory, it provides a single place to configure
the approach to encoding, based on one of the below encoding modes:
-
TEMPLATE_AND_VALUES: 使用UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,用于预编码URI模板,并在扩展时严格编码URI变量。 -
VALUES_ONLY: 不对URI模板进行编码,而是通过UriUtils#encodeUriUriVariables在将URI变量展开到模板之前应用严格的编码。 -
URI_COMPONENT: 使用UriComponents#encode(),对应于前面列表中的第二个选项,用于在 URI 变量扩展后对 URI 组件值进行编码。 -
NONE: 未应用任何编码。
The RestTemplate is set to EncodingMode.URI_COMPONENT for historic
reasons and for backwards compatibility. The WebClient relies on the default value
in DefaultUriBuilderFactory, which was changed from EncodingMode.URI_COMPONENT in
5.0.x to EncodingMode.TEMPLATE_AND_VALUES in 5.1.
1.7. 跨域资源共享(CORS)
Spring WebFlux 允许你处理 CORS(跨域资源共享)。本节将介绍如何进行处理。
1.7.1. 介绍
出于安全原因,浏览器禁止向当前源之外的资源发起 AJAX 请求。例如,你可以在一个标签页中打开你的银行账户,在另一个标签页中打开 evil.com。evil.com 中的脚本不应该能够使用你的凭证向你的银行 API 发起 AJAX 请求——例如,从你的账户中提款!
跨源资源共享(CORS)是由W3C规范实现的,大多数浏览器都支持它,它允许你指定授权的跨域请求类型,而不是使用基于IFRAME或JSONP的不安全且功能较弱的变通方案。
1.7.2. 处理
CORS规范区分了预检请求、简单请求和实际请求。 要了解CORS的工作原理,你可以阅读 这篇文章,或者查看规范以获取更多详细信息。
Spring WebFlux HandlerMapping 实现提供了内置的CORS支持。在成功将请求映射到处理器后,HandlerMapping 会检查给定请求和处理器的CORS配置并采取进一步操作。预检请求会被直接处理,而简单的和实际的CORS请求会被拦截、验证,并设置所需的CORS响应头。
为了启用跨域请求(即Origin标头存在且与请求的主机不同),你需要有一些明确声明的CORS配置。如果没有找到匹配的CORS配置,预检请求将被拒绝。不会向简单和实际的CORS请求的响应中添加任何CORS标头,因此浏览器会拒绝这些请求。
每个 HandlerMapping 可以通过
配置
基于 URL 模式的 CorsConfiguration 映射单独进行配置。在大多数情况下,应用程序
使用 WebFlux Java 配置来声明此类映射,这将导致传递给所有 HandlerMapping 实现的单一、全局映射。
你可以结合在 HandlerMapping 级别的全局CORS配置与更精细的、处理器级别的CORS配置。例如,注解控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。
全局和本地配置组合的规则通常是累加的——例如,所有全局和所有本地源。对于那些只能接受单个值的属性,如allowCredentials和maxAge,本地值会覆盖全局值。有关更多详细信息,请参见
CorsConfiguration#combine(CorsConfiguration)。
|
要从源代码中学习更多内容或进行高级自定义,请参见:
|
1.7.3. @CrossOrigin
The @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配置。你可以为任何HandlerMapping单独设置基于URL的CorsConfiguration映射。然而,大多数应用程序使用WebFlux Java配置来实现这一点。
默认的全局配置启用了以下功能:
-
所有源。
-
所有头部。
-
GET,HEAD, 和POST方法。
allowedCredentials 默认情况下未启用,因为这会建立一个信任级别,该级别会暴露敏感的用户特定信息(例如 cookie 和 CSRF Tokens),并且仅应在适当的情况下使用。
maxAge 设置为 30 分钟。
要启用WebFlux中的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支持,这非常适合功能性端点。
如果你尝试使用CorsFilter与Spring Security一起,需要注意的是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 安全
The 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 框架内置了使用 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 配置
您可以直接将FreeMarker的“设置”和“共享变量”传递给由Spring管理的FreeMarker
Configuration 对象,方法是设置FreeMarkerConfigurer bean的相应bean属性。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/> 元素。此元素主要用于让表单显示来自表单支持对象的值,并显示来自 web 或业务层中的 Validator 的验证失败结果。Spring 还支持在 FreeMarker 中使用相同的功能,并提供了额外的便利宏来生成表单输入元素。
绑定宏
一组标准的宏在spring-webflux.jar文件中维护,用于FreeMarker,因此它们始终可用于配置适当的应用程序。
Spring模板库中定义的一些宏被认为是内部的(私有的),但宏定义中没有这样的作用域,使得所有宏对调用代码和用户模板都是可见的。以下部分仅集中于你需要直接在模板中调用的宏。如果你想直接查看宏代码,文件名为spring.ftl,位于org.springframework.web.reactive.result.view.freemarker包中。
有关绑定支持的详细信息,请参阅简单绑定以了解Spring MVC。
1.9.3. 脚本视图
Spring框架内置了使用Spring WebFlux与任何可以在JSR-223 Java脚本引擎上运行的模板库集成的功能。 下表列出了我们在不同脚本引擎上测试过的模板库:
| 脚本库 | 脚本引擎 |
|---|---|
集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngine和Invocable接口。 |
要求
你需要将脚本引擎添加到你的类路径中,具体细节因脚本引擎而异:
-
The 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"
}
}
The render 函数的调用参数如下:
-
String template: 模板内容 -
Map model: 视图模型 -
RenderingContext renderingContext: 提供对应用程序上下文、区域设置、模板加载器和URL(自5.0版本起)访问的RenderingContext
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
}
}
将sharedEngine属性设置为false是在使用非线程安全的脚本引擎和未设计为支持并发的模板库(如Handlebars或在Nashorn上运行的React)时所必需的。在这种情况下,需要Java SE 8更新60,因为这个bug,但通常建议使用最近的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,你可以使用它来插入任何可用的编解码器从spring-web,例如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. 控制器
Controllers 可以显式地支持 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 (NOT_MODIFIED)响应,且响应体为空。否则,将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 (NOT_MODIFIED)。无需进一步处理。 |
| 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 (NOT_MODIFIED)。无需进一步处理。 |
| 3 | 继续处理请求。 |
有三种变体用于检查针对eTag值、lastModified值或两者组合的条件请求。对于条件GET和HEAD请求,您可以将响应设置为304(NOT_MODIFIED)。对于条件POST、PUT和DELETE,您可以将响应设置为412(PRECONDITION_FAILED),以防止并发修改。
1.11. WebFlux 配置
WebFlux Java配置声明了处理带有注解的控制器或功能端点请求所需的组件,并提供了一个API来定制配置。这意味着你不需要理解Java配置创建的底层bean。但是,如果你想了解它们,你可以在WebFluxConfigurationSupport中看到它们,或者在特殊Bean类型中阅读更多关于它们的信息。
对于更高级的自定义设置,如果配置API无法提供,您可以通过 高级配置模式获得对配置的完全控制。
1.11.1. 启用WebFlux配置
您可以使用@EnableWebFlux注解在您的Java配置中,如下例所示:
@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配置中注册自定义格式化程序和转换器,请使用以下方法:
@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 在解析和格式化日期值时会考虑请求的区域设置。这对于日期以字符串形式表示的表单有效,这些字符串在“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) {
// ...
}
}
The ViewResolverRegistry 为视图技术提供了快捷方式,这些技术与Spring Framework集成。以下示例使用了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实现配置一个或多个默认视图,该实现接受来自spring-web的任何可用编解码器。以下示例展示了如何进行配置:
@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 WebFlux 也不支持后缀模式匹配,与 Spring MVC 不同,在 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 维基页面。
2. WebClient
Spring WebFlux 包含一个用于 HTTP 请求的响应式、非阻塞的 WebClient。客户端具有功能齐全、流畅的 API,带有响应式类型以进行声明式组合,参见 响应式库。WebFlux 客户端和服务器依赖于相同的非阻塞 编解码器 来编码和解码请求和响应内容。
内部 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: 自定义UriBuilderFactory用作基础URL。 -
defaultHeader: 每个请求的头部。 -
defaultCookie: 每次请求的Cookies。 -
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(默认值)来确保当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()。
您可以在多个Jetty客户端(和服务器)实例之间共享资源,并确保在Spring ApplicationContext关闭时资源被关闭,方法是声明一个类型为JettyResourceFactory的Spring管理bean,如下例所示:
@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()
The 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()
The 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 响应没有自动错误信号。您需要检查状态代码并决定如何处理。
|
Unlike |
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> 作为正文。请注意,内容会由 FormHttpMessageWriter 自动设置为 application/x-www-form-urlencoded。以下示例展示了如何使用 MultiValueMap<String, String>:
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
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. 多部分数据
要发送多部分数据,你需要提供一个 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的情况下,基于文件扩展名。如果必要,你可以通过其中一个重载的构建器part方法显式提供每个部分要使用的MediaType。
一旦准备好了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 包装器。
作为替代方案,你也可以通过内置的BodyInserters以多部分内容、内联样式提供,如下例所示:
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
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()
Filters 会全局应用于每个请求。要为特定请求更改过滤器的行为,可以向 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())
}
上面只是一个例子。还有很多其他模式和操作符可以用来组合一个响应式管道,该管道可以进行许多远程调用,可能有些是嵌套的、相互依赖的,但直到最后都不会阻塞。
|
使用 |
2.7. 测试
要测试使用WebClient的代码,你可以使用一个模拟Web服务器,例如
OkHttp MockWebServer。要查看其使用的示例,请参阅
WebClientIntegrationTests
在Spring Framework测试套件中的示例或
static-server
在OkHttp仓库中的示例。
3. WebSockets
这部分参考文档涵盖了对响应式堆栈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套接字保持打开状态,客户端和服务器可以继续发送和接收消息。
关于WebSockets的工作原理的完整介绍超出了本文档的范围。请参阅RFC 6455、HTML5中的WebSocket章节,或网络上的众多介绍和教程。
请注意,如果WebSocket服务器运行在Web服务器(例如nginx)后面,您可能需要配置它以将WebSocket升级请求传递给WebSocket服务器。同样,如果应用程序在云环境中运行,请查看云提供商关于WebSocket支持的相关说明。
3.1.1. HTTP 与 WebSocket
尽管WebSocket被设计为与HTTP兼容并以HTTP请求开始,但重要的是要理解这两种协议会导致非常不同的架构和应用程序编程模型。
在HTTP和REST中,应用程序被建模为许多URL。要与应用程序交互,客户端访问这些URL,采用请求-响应的方式。服务器根据HTTP URL、方法和标头将请求路由到适当的处理器。
相比之下,在WebSockets中,通常只有一个URL用于初始连接。 随后,所有应用程序消息都在同一个TCP连接上流动。这指向了一种完全不同的异步、事件驱动的消息架构。
WebSocket 也是一种低级别的传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息。
WebSocket 客户端和服务器可以通过 HTTP 握手请求中的 Sec-WebSocket-Protocol 头部协商使用更高层级的消息协议(例如,STOMP)。如果没有这个头部,它们需要自己制定约定。
3.1.2. 何时使用WebSockets
WebSockets 可以使网页变得动态和互动。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供一个简单而有效的解决方案。
例如,新闻、邮件和社交动态需要动态更新,但每隔几分钟更新一次可能是完全可以接受的。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是决定性因素。如果消息量相对较低(例如,监控网络故障),HTTP流或轮询可以提供有效的解决方案。正是低延迟、高频率和高容量的结合使得使用WebSocket成为最佳选择。
请记住,通过互联网,您无法控制的限制性代理可能会阻止WebSocket交互,要么是因为它们没有配置为传递Upgrade标头,要么是因为它们关闭了看起来空闲的长连接。这意味着在防火墙内的内部应用程序中使用WebSocket是一个更直接的决定,而不是在面向公众的应用程序中。
3.2. WebSocket API
Spring框架提供了一个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
The handle 方法的 WebSocketHandler 接受 WebSocketSession 并返回 Mono<Void>
以指示应用程序处理会话完成的时间。会话通过两个流进行处理,一个用于传入消息,另一个用于传出消息。下表描述了处理这两个流的两种方法:
WebSocketSession 方法 |
描述 |
|---|---|
|
提供对入站消息流的访问,并在连接关闭时完成。 |
|
接受传出消息的来源,写入消息,并在源完成且写入完成后返回一个 |
一个 WebSocketHandler 必须将入站和出站流组合成一个统一的流,并返回一个 Mono<Void>,该值反映了该流的完成。根据应用程序需求,统一流在以下情况下完成:
-
要么是入站消息流完成,要么是出站消息流完成。
-
入站流完成(即连接关闭),而出站流是无限的。
-
在某个选定的点,通过
close方法的WebSocketSession。
当入站和出站消息流组合在一起时,无需检查连接是否打开,因为响应式流会发出结束活动的信号。入站流会收到完成或错误信号,而出站流会收到取消信号。
最基础的处理器实现是处理入站流的。以下示例展示了这种实现:
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>,在接收到完成时完成。 |
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>,在接收到完成时完成。 |
对于嵌套的异步操作,您可能需要在使用池化数据缓冲区(例如,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>。 |
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>。 |
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并返回一个CorsConfiguraiton,其中包含允许的源、标头和其他详细信息。如果你不能这样做,你也可以设置SimpleUrlHandler上的corsConfigurations属性,通过URL模式指定CORS设置。如果两者都指定了,它们将通过在CorsConfiguration上调用combine方法进行组合。
3.2.7. 客户端
Spring WebFlux 提供了一个 WebSocketClient 抽象,具有 Reactor Netty、Tomcat、Jetty、Undertow 和标准 Java(即 JSR-356)的实现。
Tomcat客户端实际上是标准Java客户端的扩展,其中在WebSocketSession处理中添加了一些额外的功能,以利用Tomcat特有的API来暂停接收消息以实现背压。 |
要启动WebSocket会话,您可以创建客户端的实例并使用其execute
方法:
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
session.receive()
.doOnNext(::println)
.then()
}
某些客户端(如Jetty)实现了Lifecycle,需要在使用前停止和启动。所有客户端都有与底层WebSocket客户端配置相关的构造函数选项。
4. 测试
The spring-test 模块提供了ServerHttpRequest、ServerHttpResponse和ServerWebExchange的模拟实现。
有关模拟对象的讨论,请参见Spring Web Reactive。
WebTestClient 基于这些模拟请求和响应对象,为在没有HTTP服务器的情况下测试WebFlux应用程序提供支持。你也可以使用WebTestClient进行端到端集成测试。
5. RSocket
本节描述了Spring框架对RSocket协议的支持。
5.1. 概览
RSocket 是一个应用程序协议,用于通过 TCP、WebSocket 和其他字节流传输进行多路复用和双向通信,使用以下交互模型之一:
-
Request-Response— 发送一条消息并接收一条回复。 -
Request-Stream— 发送一条消息并接收返回的消息流。 -
Channel— 双向发送消息流。 -
Fire-and-Forget— 发送单向消息。
一旦初始连接建立,客户端与服务器的区别就会消失,因为双方变得对称,每一方都可以发起上述交互之一。这就是为什么在协议调用中,参与的双方被称为“请求者”和“响应者”,而上述交互被称为“请求流”或简称为“请求”。
以下是RSocket协议的关键特性和优势:
-
响应式流语义跨越网络边界——对于诸如
Request-Stream和Channel的流请求,背压信号在请求者和响应者之间传递,允许请求者在源头减慢响应者的速度,从而减少对网络层拥塞控制的依赖,以及在网络层或任何层级缓冲的需求。 -
请求节流——此功能以“租赁”命名,源自
LEASE帧,该帧可以从每一端发送以限制另一端在给定时间内允许的请求数量。租约会定期续订。 -
会话恢复——这是为了解决连接中断而设计的,需要维护一些状态。状态管理对应用程序是透明的,并且与背压配合得很好,可以在可能的情况下停止生产者并减少所需的状态量。
-
大消息的分段和重新组装。
-
保活(心跳)。
RSocket 有多种语言的实现。Java 库基于Project Reactor构建,并使用Reactor Netty进行传输。这意味着来自应用程序中的 Reactive Streams Publishers 的信号会透明地通过 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 帧中声明,并适用于给定连接上的所有请求。
虽然所有消息都可以包含元数据,但通常像路由这样的元数据是按请求的,因此只包含在请求的第一个消息中,即带有帧之一REQUEST_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL或REQUEST_FNF的消息。
协议扩展定义了应用程序中常用的元数据格式:
5.1.2. Java 实现
The Java实现 for RSocket is built on
Project Reactor. The transports for TCP and WebSocket are
built on Reactor Netty. As a Reactive Streams
library, Reactor simplifies the job of implementing the protocol. For applications it is
a natural fit to use Flux and Mono with declarative operators and transparent back
pressure support。
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 支持
The spring-messaging 模块包含以下内容:
-
RSocketRequester — 流畅的API,用于通过
io.rsocket.RSocket进行请求,并对数据和元数据进行编码/解码。 -
注解响应器 —
@MessageMapping个注解处理器方法用于响应。
The spring-web 模块包含 Encoder 和 Decoder 实现,如 Jackson CBOR/JSON 和 Protobuf,这些实现是 RSocket 应用程序可能需要的。它还包含可以用于高效路由匹配的 PathPatternParser。
Spring Boot 2.2 支持通过 TCP 或 WebSocket 构建 RSocket 服务器,包括在 WebFlux 服务器中通过 WebSocket 暴露 RSocket 的选项。还有客户端支持和对 RSocketRequester.Builder 和 RSocketStrategies 的自动配置。
有关详细信息,请参阅
RSocket 部分
在 Spring Boot 参考文档中。
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需要连接到服务器并准备和发送初始的RSocketSETUP帧。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类型是复合元数据,它允许每个请求中包含多个元数据值和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)
对于上述情况,您可能还需要在 RSocketMessageHandler 中使用 setHandlerPredicate 来切换到不同的客户端响应者检测策略,例如基于自定义注解 @RSocketClientResponder 而不是默认的 @Controller。这在客户端和服务器或同一应用程序中的多个客户端场景中是必要的。
另见 注解响应器,了解更多编程模型。
高级
RSocketRequesterBuilder 提供了一个回调来暴露底层的
io.rsocket.core.RSocketConnector 以提供更多配置选项,例如保活间隔、会话恢复、拦截器等。您可以按以下方式在该级别配置选项:
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 | 声明预期的响应。 |
交互类型是根据输入和输出的基数隐式确定的。上面的例子是一个Request-Stream,因为发送了一个值并接收了一个值流。在大多数情况下,只要输入和输出的选择与RSocket交互类型以及响应者期望的输入和输出类型匹配,你就不需要考虑这一点。唯一一个无效组合的例子是多对一。
The 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 使用返回 Mono<Void> 的 send() 方法。请注意,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 支持
复合 和
路由 元数据,默认情况下。你可以设置它的
MetadataExtractor 如果你需要切换到不同的 MIME 类型或注册额外的元数据 MIME 类型。
您需要设置支持元数据和数据格式所需的Encoder和Decoder实例。您可能需要spring-web模块来实现编解码器。
默认情况下使用 SimpleRouteMatcher 进行路由匹配。我们建议插入来自 spring-web 的 PathPatternRouteMatcher 以实现高效的路由匹配。RSocket 路由可以是层次结构的,但不是 URL 路径。两个路由匹配器都默认配置为使用 "." 作为分隔符,并且没有像 HTTP URL 那样的 URL 解码。
RSocketMessageHandler 可以通过 RSocketStrategies 进行配置,这在需要在同一进程中共享客户端和服务器配置时可能很有用:
@Configuration
static class ServerConfig {
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.setRSocketStrategies(rsocketStrategies());
return handler;
}
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.routeMatcher(new PathPatternRouteMatcher())
.build();
}
}
@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 交互。它支持灵活的方法签名,并且可以选择使用以下方法参数:
| 方法参数 | 描述 |
|---|---|
|
请求的有效载荷。这可以是异步类型的具体值,如
注意: 使用该注解是可选的。如果方法参数不是简单类型 并且不是其他支持的参数之一,则假定为预期的有效载荷。 |
|
请求方用于向远程端发起请求。 |
|
从路由中根据映射模式中的变量提取的值,例如:
|
|
Metadata value registered for extraction as described in MetadataExtractor. |
|
所有已注册用于提取的元数据值,如MetadataExtractor中所述。 |
返回值预期是一个或多个将被序列化为响应负载的对象。这可以是异步类型,如Mono或Flux,一个具体值,或者void或一个无值的异步类型,如Mono<Void>。
一个@MessageMapping方法支持的RSocket交互类型由输入(即@Payload个参数)和输出的基数决定,其中基数的含义如下:
| 基数 | 描述 |
|---|---|
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 可以有一个模式来缩小处理范围到
具有元数据中路由的特定连接,或者如果没有声明任何模式
则所有连接都匹配。
@ConnectMapping 方法不能返回数据,并且必须声明为以 void 或
Mono<Void> 作为返回值。如果处理新连接时出现错误,则会拒绝该连接。处理过程中不应因为向 RSocketRequester 发送请求而被延迟。详情请参见
服务器请求者。
5.4. 元数据提取器
Responders 必须解释元数据。 复合元数据 允许独立格式化的元数据值(例如路由、安全、追踪)每个都有自己的 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")
Composite metadata 适用于组合独立的元数据值。但是,请求者可能不支持复合元数据,或者可能选择不使用它。为此,DefaultMetadataExtractor 可能需要自定义逻辑将解码值映射到输出映射。以下是一个使用 JSON 作为元数据的示例:
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
MimeType.valueOf("application/vnd.myapp.metadata+json"),
new ParameterizedTypeReference<Map<String,String>>() {},
(jsonMap, outputMap) -> {
outputMap.putAll(jsonMap);
});
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 并在内部使用它来组合异步逻辑并提供响应式流支持。通常,WebFlux API 返回 Flux 或 Mono(因为这些在内部被使用)并且宽容地接受任何响应式流 Publisher 实现作为输入。使用 Flux 与 Mono 的选择很重要,因为它有助于表达基数——例如,预期的是单个还是多个异步值,这对于做出决策(例如,在编码或解码 HTTP 消息时)至关重要。
对于注解控制器,WebFlux 会透明地适应应用程序选择的响应式库。这是通过
ReactiveAdapterRegistry 来实现的,它
提供了对响应式库和其他异步类型的可插拔支持。该注册表
内置支持 RxJava 和 CompletableFuture,但你也可以注册其他库。
对于功能性API(例如功能端点、WebClient等),WebFlux API的一般规则适用——返回值为Flux和Mono,输入为Reactive Streams的Publisher。当提供了一个Publisher,无论是自定义的还是来自其他响应式库的,它只能被视为具有未知语义的流(0..N)。但是,如果语义已知,您可以使用Flux或Mono.from(Publisher)包装它,而不是传递原始的Publisher。
例如,给定一个 Publisher 而不是一个 Mono,Jackson JSON 消息写入器期望多个值。如果媒体类型暗示了一个无限流(例如,application/json+stream),则值将单独写入并刷新。否则,值将被缓冲到一个列表中,并作为 JSON 数组呈现。