Web 响应式
1. Spring WebFlux
Spring 框架中包含的原始 Web 框架 Spring Web MVC 是 专为 Servlet API 和 Servlet 容器构建。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版后期添加的。它是完全非阻塞的,支持 Reactive Streams 背压,并运行在 Netty、Undertow 和 Servlet 3.1+ 容器。
这两个 Web 框架都镜像其源模块的名称
(spring-webmvc 和 spring-webflux)并并存于
Spring 框架。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者
在某些情况下,两者都有 — 例如,带有 reactive 的 Spring MVC 控制器。WebClient
1.1. 概述
为什么创建 Spring WebFlux?
部分答案是需要一个非阻塞 Web 堆栈来处理并发
线程数量少,硬件资源较少。Servlet 3.1 确实提供了
用于非阻塞 I/O 的 API。但是,使用它会导致 Servlet API 的其余部分
其中 Contract 是同步的 (, ) 或阻塞的 (, )。这就是一个新的通用 API 作为
任何非阻塞运行时。这很重要,因为服务器(例如 Netty)是
在异步、非阻塞空间中建立良好。Filter
Servlet
getParameter
getPart
答案的另一部分是函数式编程。就像添加注释一样
在 Java 5 中创建的机会(例如带注释的 REST 控制器或单元测试)中,添加的
的 lambda 表达式为 Java 中的功能性 API 创造了机会。
这对于非阻塞应用程序和延续式 API(如流行的
by 和 ReactiveX),它们允许声明式
异步逻辑的组成。在编程模型级别,Java 8 启用了 Spring
WebFlux 提供功能性 Web 端点以及带注释的控制器。CompletableFuture
1.1.1. 定义 “Reactive”
我们谈到了“非阻塞”和“函数式”,但响应式是什么意思?
术语“反应式”是指围绕响应变化而构建的编程模型 — 网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。 从这个意义上说,非阻塞是反应性的,因为我们现在处于模式中,而不是被阻塞 在操作完成或数据可用时对通知做出反应。
我们 Spring 团队还有另一个重要的机制与 “reactive” 相关联 那就是非阻塞背压。在同步的命令式代码中,阻塞调用 作为一种自然形式的背压,迫使呼叫者等待。在非阻塞 代码中,控制事件的速率变得很重要,这样快速生产者就不会 压倒它的目的地。
Reactive Streams 是一个小规范(在 Java 9 中也采用了) 它定义了异步组件与背压之间的交互。 例如,数据存储库(充当 Publisher) 可以生成 HTTP 服务器(充当订阅者)的数据 然后可以写入响应。Reactive Streams 的主要目的是让 subscriber 控制发布服务器生成数据的速度或速度。
常见问题:如果出版商不能放慢速度怎么办? Reactive Streams 的目的只是建立机制和边界。 如果发布者无法放慢速度,则必须决定是缓冲、丢弃还是失败。 |
1.1.2. 响应式 API
Reactive Streams 在互操作性方面起着重要作用。图书馆对此感兴趣
和基础设施组件,但作为应用程序 API 的用处不大,因为它太
低级。应用程序需要更高级别、更丰富的功能 API 来
compose async logic — 类似于 Java 8 API,但不仅适用于集合。
这就是响应式库所扮演的角色。Stream
Reactor 是
Spring WebFlux 的 Web Flux 中。它提供 Mono
和 Flux
API 类型
通过一组丰富的运算符来处理 0..1 () 和 0..N () 的数据序列,这些运算符与
运算符的 ReactiveX 词汇表。
Reactor 是一个 Reactive Streams 库,因此,它的所有运算符都支持非阻塞背压。
Reactor 非常注重服务器端 Java。它是密切合作开发的
与Spring。Mono
Flux
WebFlux 需要 Reactor 作为核心依赖项,但它可以与其他反应式
库。作为一般规则,WebFlux API 接受 plain 作为 input,在内部将其适应 Reactor 类型,使用它,并返回 a 或 a 作为输出。因此,您可以将 any 作为输入传递,并且可以应用
操作,但您需要调整输出以用于另一个反应式库。
只要可行(例如,带注释的控制器),WebFlux 就会透明地适应
RxJava 或其他反应式库。有关更多详细信息,请参阅 Reactive Libraries 。Publisher
Flux
Mono
Publisher
除了反应式 API 之外,WebFlux 还可以与 Kotlin 中的协程 API 一起使用,后者提供了一种更加命令式的编程风格。 以下 Kotlin 代码示例将随协程 API 一起提供。 |
1.1.3. 对模型进行编程
该模块包含作为 Spring WebFlux 基础的反应式基础,
包括 HTTP 抽象、支持的 Reactive Streams 适配器
服务器、编解码器和核心 WebHandler
API 可与
Servlet API,但具有非阻塞 Contract。spring-web
在此基础上, 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 的轻量级、功能性 Web 框架感兴趣 或 Kotlin 中,您可以使用 Spring WebFlux 功能性 Web 端点。那也可能是一个不错的选择 适用于要求不太复杂的小型应用程序或微服务,可受益 来自更高的透明度和控制。
-
在微服务架构中,您可以将应用程序与 Spring MVC 混合使用 或 Spring WebFlux 控制器或具有 Spring WebFlux 功能端点。获得支持 对于两个框架中相同的基于注释的编程模型,可以更轻松地 重用知识,同时为正确的工作选择正确的工具。
-
评估应用程序的一种简单方法是检查其依赖项。如果你有阻塞 持久化 API(JPA、JDBC)或网络 API 使用,Spring MVC 是最好的选择 至少对于常见的架构来说是这样。从 Reactor 和 RxJava 在单独的线程上执行阻塞调用,但您不会将 大多数非阻塞 Web 堆栈。
-
如果您有一个调用远程服务的 Spring MVC 应用程序,请尝试 reactive 。 您可以返回反应式类型(Reactor、RxJava 或其他) 直接从 Spring MVC 控制器方法。每次调用的延迟较大或 调用之间的相互依赖关系,好处就越显著。Spring MVC 控制器 也可以调用其他响应式组件。
WebClient
-
如果你有一个大型团队,请记住,在向非阻塞的转变中,学习曲线很陡峭。 函数式编程和声明式编程。无需完全切换即可开始的实用方法 是使用响应式 .除此之外,从小处着手并衡量收益。 我们预计,对于广泛的应用程序,这种转变是不必要的。如果你是 不确定要寻找什么好处,请先了解非阻塞 I/O 的工作原理 (例如,单线程 Node.js) 上的并发性)及其影响。
WebClient
1.1.5. 服务器
Spring WebFlux 在 Tomcat、Jetty、Servlet 3.1+ 容器以及 非 Servlet 运行时,例如 Netty 和 Undertow。所有服务器都适用于低级通用 API,因此可以跨服务器支持更高级别的编程模型。
Spring WebFlux 没有内置支持来启动或停止服务器。然而,事实确实如此 从 Spring 配置和 WebFlux 基础设施轻松组装应用程序,并使用几个 代码行。
Spring Boot 有一个 WebFlux Starters,可以自动执行这些步骤。默认情况下,Starters使用 Netty,但通过更改您的 Maven 或 Gradle 依赖项。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(以及一般的非阻塞服务器)中,假定应用程序 不要阻止。因此,非阻塞服务器使用小型的固定大小的线程池 (事件循环工作程序)来处理请求。
“To scale” 和 “small number of thread” 听起来可能很矛盾,但永远不要阻止 current thread (并依赖于回调) 意味着您不需要额外的线程,因为 没有阻塞调用需要吸收。 |
如果您确实需要使用阻塞库怎么办?Reactor 和 RxJava 都提供了在不同的线程上继续处理的运算符。这意味着有一个
轻松逃生舱口。但请记住,阻止 API 并不适合
this concurrency model 的publishOn
在 Reactor 和 RxJava 中,您可以通过运算符声明逻辑。在运行时,响应式 在不同的阶段按顺序处理数据的地方形成管道。主要优势 这是因为它使应用程序不必保护可变状态,因为 该管道中的应用程序代码永远不会并发调用。
您应该期望在运行 Spring WebFlux 的服务器上看到哪些线程?
-
在“vanilla”Spring WebFlux 服务器上(例如,没有数据访问或其他可选 dependencies),您可以期望服务器有一个线程,而 request 则有其他几个线程 处理(通常与 CPU 内核的数量一样多)。但是,Servlet 容器 可以从更多线程开始(例如,Tomcat 上的 10 个),以支持 servlet(阻塞)I/O 以及 servlet 3.1(非阻塞)I/O 使用情况。
-
reactive 以事件循环方式运行。所以你可以看到一个小的、固定的 与该相关的处理线程数(例如,使用 Reactor Netty 连接器)。但是,如果 Reactor Netty 同时用于客户端和服务器,则这两个 默认情况下共享事件循环资源。
WebClient
reactor-http-nio-
-
Reactor 和 RxJava 提供了线程池抽象,称为调度程序,用于用于将处理切换到其他线程池的运算符。 计划程序的名称表示特定的并发策略,例如,“parallel” (对于具有有限线程数的 CPU 绑定工作)或 “elastic” (对于 I/O 绑定工作 大量线程)。如果您看到这样的线程,则表示某些代码正在使用 特定的线程池策略。
publishOn
Scheduler
-
数据访问库和其他第三方依赖项也可以创建和使用线程 他们自己的。
1.2. 响应式核心
该模块包含以下对反应式 Web 的基本支持
应用:spring-web
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:HTTP 请求处理的基本协定 非阻塞 I/O 和反应流背压,以及用于 Reactor Netty 的适配器, Undertow、Tomcat、Jetty 和任何 Servlet 3.1+ 容器。
-
WebHandler
API:稍高级的通用 Web API,用于 请求处理,除此之外,还有具体的编程模型,如 Annotated 构建控制器和功能端点。
-
-
对于客户端,有一个基本的 Contract 来执行 HTTP 具有非阻塞 I/O 和反应流背压的请求,以及用于 Reactor Netty、反应式 Jetty HttpClient 和 Apache HttpComponents 的适配器。 应用程序中使用的更高级别的 WebClient 建立在这个基本契约之上。
ClientHttpConnector
-
对于客户端和服务器,用于序列化的编解码器和 HTTP 请求和响应内容的反序列化。
1.2.1.HttpHandler
HttpHandler 是一个简单的协定,具有处理请求和响应的单一方法。是的 有意最小化,其主要且唯一目的是成为最小抽象 通过不同的 HTTP 服务器 API 进行。
下表描述了支持的服务器 API:
服务器名称 | 使用的服务器 API | Reactive Streams 支持 |
---|---|---|
网 |
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 写入 ByteBuffers 与 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 |
反应器-NETTY |
Undertow |
io.undertow |
undertow-core |
Tomcat |
org.apache.tomcat.embed |
tomcat-embed-core |
Jetty |
org.eclipse.jetty 网站 |
jetty 服务器、jetty-servlet |
下面的代码片段显示了将适配器与每个服务器 API 一起使用:HttpHandler
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+ 容器,你可以在 WAR 中扩展并包含 AbstractReactiveWebInitializer
。该类将 with 包装并注册
that 作为 .HttpHandler
ServletHttpHandlerAdapter
Servlet
1.2.2. 应用程序接口WebHandler
该包基于 HttpHandler
协定构建
提供一个通用的 Web API,用于通过多个 WebExceptionHandler
、多个 WebFilter
和单个 WebHandler
组件的链处理请求。链条可以
通过简单地指向自动检测组件的 Spring,和/或通过注册组件来组合在一起
与构建器。org.springframework.web.server
WebHttpHandlerBuilder
ApplicationContext
虽然 API 的目标很简单,即抽象出不同 HTTP 服务器的使用,但 API 旨在提供 Web 应用程序中常用的更广泛的功能集
如:HttpHandler
WebHandler
-
具有属性的用户会话。
-
请求属性。
-
已解决或针对请求。
Locale
Principal
-
访问已解析和缓存的表单数据。
-
多部分数据的抽象。
-
和更多..
特殊 bean 类型
下表列出了可以在
Spring ApplicationContext,或者可以直接使用它注册:WebHttpHandlerBuilder
Bean 名称 | Bean 类型 | 计数 | 描述 |
---|---|---|---|
<任意> |
|
0..N |
为实例链和 target 中的异常提供处理。有关更多详细信息,请参阅 异常。 |
<任意> |
|
0..N |
将拦截样式逻辑应用于过滤器链的其余部分之前和之后,并且
目标 .有关更多详细信息,请参阅筛选器。 |
|
|
1 |
请求的处理程序。 |
|
|
0..1 |
通过 上的方法公开的实例的管理器。 默认情况下。 |
|
|
0..1 |
要访问实例以解析表单数据和多部分数据,则
通过 上的方法公开。 默认情况下。 |
|
|
0..1 |
通过 上的方法公开的解析程序。 默认情况下。 |
|
|
0..1 |
用于处理转发类型的标头,可以通过提取和删除它们,或者仅删除它们。 默认情况下不使用。 |
表单数据
ServerWebExchange
公开以下用于访问表单数据的方法:
Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>
使用 configured 来解析表单数据
() 转换为 .默认情况下,配置为供 Bean 使用
(请参阅 Web 处理程序 API)。DefaultServerWebExchange
HttpMessageReader
application/x-www-form-urlencoded
MultiValueMap
FormHttpMessageReader
ServerCodecConfigurer
多部分数据
ServerWebExchange
公开以下用于访问分段数据的方法:
Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>
使用配置的来解析内容
转换为 .
默认情况下,这是 ,它没有任何第三方
依赖。
或者,也可以使用基于 Synchronoss NIO Multipart 库的 the。
两者都是通过 bean 配置的
(请参阅 Web 处理程序 API)。DefaultServerWebExchange
HttpMessageReader<MultiValueMap<String, Part>>
multipart/form-data
MultiValueMap
DefaultPartHttpMessageReader
SynchronossPartHttpMessageReader
ServerCodecConfigurer
要以流式处理方式解析多部分数据,您可以改用 returned from an 的 n.例如,在带注释的控制器中,使用 of 隐含了按名称对各个部分的类似 -,因此,需要
完整解析多部分数据。相比之下,您可以使用来解码
内容到不收集到 .Flux<Part>
HttpMessageReader<Part>
@RequestPart
Map
@RequestBody
Flux<Part>
MultiValueMap
请求头转发
当请求通过代理(例如负载均衡器)、主机、端口和 方案可能会发生变化。从客户的角度来看,这使得创建指向正确 host、port 和 scheme。
RFC 7239 定义了 HTTP 标头
代理可用于提供有关原始请求的信息。还有其他
非标准标头,包括 、 和 .Forwarded
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Proto
X-Forwarded-Ssl
X-Forwarded-Prefix
ForwardedHeaderTransformer
是一个组件,用于修改
请求,然后删除这些标头。如果您声明
it 作为名为 的 bean ,它将被检测和使用。forwardedHeaderTransformer
转发的 Headers 存在安全注意事项,因为应用程序无法知道
标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么
应将信任边界的代理配置为删除传入的不受信任的转发流量
从外面。您还可以配置 with ,在这种情况下,它会删除但不使用 Headers。ForwardedHeaderTransformer
removeOnly=true
在 5.1 中已被弃用并被取代,因此请求头转发可以更早地处理,在
Exchange 已创建。如果仍然配置了过滤器,则会将其从
filters,并改用 。ForwardedHeaderFilter ForwardedHeaderTransformer ForwardedHeaderTransformer |
1.2.3. 过滤器
在 WebHandler
API 中,您可以使用 a 来应用拦截样式
过滤器其余处理链和目标之前和之后的逻辑 。使用 WebFlux 配置时,注册一个
将其声明为 Spring Bean 并(可选地)使用 on 表示优先级
bean 声明或通过实现 .WebFilter
WebHandler
WebFilter
@Order
Ordered
CORS
Spring WebFlux 通过对 CORS 配置的注释提供细粒度的支持
控制器。但是,当您将其与 Spring Security 一起使用时,我们建议依赖 built-in ,它必须在 Spring Security 的过滤器链之前订购。CorsFilter
有关更多详细信息,请参阅 CORS 和 CORS WebFilter
部分。
1.2.4. 异常
在 WebHandler
API 中,你可以使用 a 来处理
实例链和目标 的异常 。当使用 WebFlux Config 时,注册 a 就像将其声明为
Spring bean 和(可选)通过在 bean 声明上使用来表示优先级,或者
通过实施 .WebExceptionHandler
WebFilter
WebHandler
WebExceptionHandler
@Order
Ordered
下表介绍了可用的实施:WebExceptionHandler
异常处理程序 | 描述 |
---|---|
|
通过将响应设置为异常的 HTTP 状态代码,提供对 |
|
的扩展也可以确定 HTTP 状态
code 的注释。 此处理程序在 WebFlux Config 中声明。 |
1.2.5. 编解码器
和 模块提供对序列化和
通过非阻塞 I/O 将字节内容反序列化到更高级别的对象或从更高级别的对象反序列化
反应流背压。下面介绍了此支持:spring-web
spring-core
-
HttpMessageReader
和HttpMessageWriter
是 Contract 对 HTTP 消息内容进行编码和解码。 -
可以包装 an 以适应在 web 中使用 application,而 a 可以用 .
Encoder
EncoderHttpMessageWriter
Decoder
DecoderHttpMessageReader
-
DataBuffer
抽象出不同的 字节缓冲区表示形式(例如 Netty 、 等)和 is 所有编解码器都适用于什么。请参阅 Data Buffers and Codecs 中的 “Spring Core” 部分,了解有关此主题的更多信息。ByteBuf
java.nio.ByteBuffer
该模块提供 、 、 、 以及 编码器 和 解码器 实现。该模块提供 Jackson
JSON、Jackson Smile、JAXB2、Protocol Buffers 和其他编码器和解码器以及
用于表单数据、多部分内容、
server-sent 事件等。spring-core
byte[]
ByteBuffer
DataBuffer
Resource
String
spring-web
ClientCodecConfigurer
,通常用于配置和
自定义要在应用程序中使用的编解码器。请参阅 配置 HTTP 消息编解码器 部分。ServerCodecConfigurer
Jackson JSON
JSON 和二进制 JSON (Smile) 是 当存在 Jackson 库时,两者都受支持。
工作原理如下:Jackson2Decoder
-
Jackson 的异步、非阻塞解析器用于聚合字节块流 into 的 ,每个都表示一个 JSON 对象。
TokenBuffer
-
每个对象都传递给 Jackson's 以创建更高级别的对象。
TokenBuffer
ObjectMapper
-
解码为单值发布者(例如 )时,有一个 .
Mono
TokenBuffer
-
当解码到多值发布者(例如 )时,每个都会传递给 一旦收到足够多的字节用于完全形成的对象。这 input 内容可以是 JSON 数组,也可以是任何以行分隔的 JSON 格式,例如 NDJSON, JSON 行或 JSON 文本序列。
Flux
TokenBuffer
ObjectMapper
工作原理如下:Jackson2Encoder
-
对于单值发布者(例如 ),只需通过 .
Mono
ObjectMapper
-
对于具有 的多值发布者,默认情况下,使用 收集值,然后序列化生成的集合。
application/json
Flux#collectToList()
-
对于具有 or 、 encode、write 和 等流媒体类型的多值发布者 使用以行分隔的 JSON 格式单独刷新每个值。其他 流媒体类型可以注册到编码器。
application/x-ndjson
application/stream+x-jackson-smile
-
对于 SSE,将按事件调用 ,并刷新输出以确保 毫不拖延地交货。
Jackson2Encoder
默认情况下,both 和 都不支持 .相反,默认假设是一个字符串或一系列字符串
表示序列化的 JSON 内容,由 .如果
您需要从 、use 和
编码 . |
表单数据
FormHttpMessageReader
并支持对内容进行解码和编码。FormHttpMessageWriter
application/x-www-form-urlencoded
在经常需要从多个位置访问表单内容的服务器端,提供解析内容的专用方法
,然后缓存结果以供重复访问。
请参阅 WebHandler
API 部分中的表单数据。ServerWebExchange
getFormData()
FormHttpMessageReader
使用后,将无法再从
请求正文。因此,应用程序需要始终如一地访问缓存的表单数据,而不是从原始请求正文中读取数据。getFormData()
ServerWebExchange
多部分
MultipartHttpMessageReader
并支持解码和
对 “multipart/form-data” 内容进行编码。反过来,委托给
另一个用于实际解析为 a,然后简单地
将各部分收集到 .
默认情况下,使用 the,但这可以通过 .
有关 的更多信息,请参阅 DefaultPartHttpMessageReader
的 javadoc。MultipartHttpMessageWriter
MultipartHttpMessageReader
HttpMessageReader
Flux<Part>
MultiValueMap
DefaultPartHttpMessageReader
ServerCodecConfigurer
DefaultPartHttpMessageReader
在服务器端,可能需要从多个访问多部分表单内容
places 提供了一个专用的解析方法
内容,然后缓存结果以供重复访问。
请参阅 WebHandler
API 部分中的 Multipart Data。ServerWebExchange
getMultipartData()
MultipartHttpMessageReader
使用后,将无法再从
请求正文。因此,应用程序必须始终如一地使用对部分的重复、类似映射的访问,或者依赖 进行对 的一次性访问。getMultipartData()
getMultipartData()
SynchronossPartHttpMessageReader
Flux<Part>
限制
Decoder
以及缓冲部分或全部 input 的实现
stream 可以配置对内存中缓冲的最大字节数的限制。
在某些情况下,之所以发生缓冲,是因为输入被聚合并表示为单个
object — 例如,具有 、 data 等的控制器方法。流式处理也可能发生缓冲,当
拆分输入流 — 例如,分隔文本、JSON 对象流,以及
等等。对于这些流式处理情况,限制适用于关联的字节数
在流中有一个对象。HttpMessageReader
@RequestBody byte[]
x-www-form-urlencoded
要配置缓冲区大小,您可以检查给定的 or 公开属性,如果是,则 Javadoc 将包含有关 default 的详细信息
值。在服务器端,提供从 where 到
设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,对
所有编解码器都可以在 WebClient.Builder 中更改。Decoder
HttpMessageReader
maxInMemorySize
ServerCodecConfigurer
对于 Multipart 解析,属性限制
非文件部分的大小。对于文件部分,它确定部分达到的阈值
已写入磁盘。对于写入磁盘的文件部分,有一个附加属性来限制每个部分的磁盘空间量。还有
用于限制 Multipart 请求中段总数的属性。
要在 WebFlux 中配置所有三个实例,您需要提供 的预配置实例 to 。maxInMemorySize
maxDiskUsagePerPart
maxParts
MultipartHttpMessageReader
ServerCodecConfigurer
流
流式传输到 HTTP 响应(例如 , , )时,必须定期发送数据,以便
尽早可靠地检测断开连接的客户端。这样的发送可以是
仅注释、空 SSE 事件或任何其他“无操作”数据,这些数据实际上可以用作
心跳。text/event-stream
application/x-ndjson
DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。Spring Core 的
此参考在 Data Buffers and Codecs 一节中提供了更多相关信息。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区是池化的和引用计数的,并且必须释放
当使用以避免内存泄漏时。
WebFlux 应用程序通常不需要关心此类问题,除非它们 直接使用或生成数据缓冲区,而不是依赖编解码器转换为 以及更高级别的对象,或者除非他们选择创建自定义编解码器。对于这样的 情况请查看 Data Buffers 和 Codecs、 尤其是 使用 DataBuffer 的部分。
1.2.6. 日志记录
DEBUG
Spring WebFlux 中的级别日志记录被设计为紧凑、最小和
人性化。它侧重于有用的高价值信息
over again 与仅在调试特定问题时有用的其他 cookie 进行比较。
TRACE
级别日志记录通常遵循与 (例如,还将
不应是 Firehose),但可用于调试任何问题。此外,一些日志
消息可能会在 VS 中显示不同级别的详细信息。DEBUG
TRACE
DEBUG
良好的日志记录来自使用日志的经验。如果您发现任何 未达到既定目标,请告诉我们。
日志 ID
在 WebFlux 中,单个请求可以在多个线程上运行,线程 ID 对于关联属于特定请求的日志消息没有用。这就是为什么 默认情况下,WebFlux 日志消息以特定于请求的 ID 为前缀。
在服务器端,日志 ID 存储在属性
(LOG_ID_ATTRIBUTE
)、
而基于该 ID 的完全格式化前缀可从 中获得。在一端,日志 ID 存储在属性
(LOG_ID_ATTRIBUTE
)
,而完全格式化的前缀可从 获得。ServerWebExchange
ServerWebExchange#getLogPrefix()
WebClient
ClientRequest
ClientRequest#logPrefix()
敏感数据
DEBUG
logging 可以记录敏感信息。这就是为什么表单参数和
默认情况下,标头是屏蔽的,您必须显式启用其日志记录。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()
附加程序
日志记录库(如 SLF4J 和 Log4J 2)提供了异步记录器,可避免 阻塞。虽然这些有其自身的缺点,例如可能会丢弃消息 无法排队进行日志记录,它们是目前最好的可用选项 用于反应式、非阻塞应用程序。
自定义编解码器
应用程序可以注册自定义编解码器以支持其他媒体类型、 或默认编解码器不支持的特定行为。
以下示例说明如何对客户端请求执行此操作:
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 配置中发现它需要的 delegate 组件。
它还被设计为 Spring Bean 本身,并实现对运行它的上下文的访问。If 是使用 Bean 声明的
name 的 ,它反过来又被 WebHttpHandlerBuilder
发现,
它把一个请求处理链放在一起,如 WebHandler
API 中所述。ApplicationContextAware
DispatcherHandler
webHandler
WebFlux 应用程序中的 Spring 配置通常包含:
-
DispatcherHandler
替换为 bean 名称webHandler
-
WebFilter
和BeansWebExceptionHandler
-
别人
配置用于构建处理链,
如下例所示:WebHttpHandlerBuilder
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
结果已准备好与服务器适配器一起使用。HttpHandler
1.3.1. 特殊 bean 类型
委托给特殊 bean 来处理请求并呈现
适当的回应。我们所说的“特殊 bean”是指 Spring 管理的实例,这些实例
实现 WebFlux 框架 Contract。这些通常带有内置合约,但
您可以自定义、扩展或替换其属性。DispatcherHandler
Object
下表列出了 检测到的特殊 bean。请注意,
还有一些其他 bean 在较低级别检测到(参见 Web 处理程序 API 中的特殊 bean 类型)。DispatcherHandler
Bean 类型 | 解释 |
---|---|
|
将请求映射到处理程序。映射基于一些标准,详细信息
因实现而异 — 带注释的控制器、简单的
URL 模式映射等。 主要实现用于带 Comments 的方法,用于功能端点
routes,以及 URI 路径模式的显式注册
和实例。 |
|
帮助 调用映射到请求的处理程序,而不管
处理程序的实际调用方式。例如,调用带注释的控制器
需要解析注释。a 的主要目的是屏蔽这些细节。 |
|
处理处理程序调用的结果并完成响应。 请参阅 结果处理。 |
1.3.2. WebFlux 配置
应用程序可以声明处理请求所需的基础结构 Bean(列在 Web Handler API 和 DispatcherHandler
下)。
但是,在大多数情况下,WebFlux Config 是最好的起点。它声明
所需的 bean,并提供更高级别的配置回调 API 来自定义它。
Spring Boot 依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供 许多额外的方便选择。 |
1.3.3. 处理
DispatcherHandler
按如下方式处理请求:
-
要求每个 Bean 查找匹配的处理程序,并使用第一个匹配项。
HandlerMapping
-
如果找到处理程序,则通过适当的 运行该处理程序 ,该 将执行的返回值公开为 。
HandlerAdapter
HandlerResult
-
该 被 交给 一个 适当 的 完成 通过直接写入响应或使用 View 进行渲染来进行处理。
HandlerResult
HandlerResultHandler
1.3.4. 结果处理
通过 , 调用处理程序的返回值被包装
作为 ,以及一些额外的上下文,并传递给第一个声称支持它的 Context。下表显示了可用的实现,所有这些实现都在 WebFlux Config 中声明:HandlerAdapter
HandlerResult
HandlerResultHandler
HandlerResultHandler
结果处理程序类型 | 返回值 | 默认订单 |
---|---|---|
|
|
0 |
|
|
0 |
|
处理方法或类的返回值。 |
100 |
|
另请参阅 View Resolution。 |
|
1.3.5. 异常
从 a 返回的 s 可以暴露一个 error 函数
基于某些处理程序特定机制的处理。如果出现以下情况,则调用此错误函数:HandlerResult
HandlerAdapter
-
处理程序(例如 )调用失败。
@Controller
-
通过 处理处理程序返回值失败。
HandlerResultHandler
只要 error 出现 信号发生在从处理程序返回的反应式类型生成任何数据项之前。
这就是支持类中方法的方式。
相比之下,Spring MVC 中对相同的支持是建立在 .
这通常无关紧要。但是,请记住,在 WebFlux 中,您不能使用 a 来处理在选择处理程序之前发生的异常。@ExceptionHandler
@Controller
HandlerExceptionResolver
@ControllerAdvice
1.3.6. 视图分辨率
视图分辨率允许使用 HTML 模板和模型呈现到浏览器,而无需
将您与特定的 View 技术联系起来。在 Spring WebFlux 中,视图分辨率为
通过专用的 HandlerResultHandler 提供支持,该 HandlerResultHandler 使用实例将 String(表示逻辑视图名称)映射到实例。然后,使用 the 来呈现响应。ViewResolver
View
View
处理
传入的 include 包含返回值
从处理程序和包含请求期间添加的属性的模型
处理。返回值将作为以下值之一进行处理:HandlerResult
ViewResolutionResultHandler
-
String
, : 要解析为 through 的逻辑视图名称 已配置的 implementations 列表。CharSequence
View
ViewResolver
-
void
:根据请求路径选择默认视图名称,减去前导和 尾部斜杠,并将其解析为 .当视图名称 未提供(例如,返回 model 属性)或异步返回值 (例如,completed empty)。View
Mono
-
渲染:API for 查看解决方案。探索 IDE 中的代码完成选项。
-
Model
, : 要添加到请求模型的额外模型属性。Map
-
任何其他:任何其他返回值(简单类型除外,由 BeanUtils#isSimpleProperty 确定) 被视为要添加到模型的 model 属性。属性名称是派生的 从类名中通过使用约定, 除非存在 Handler Method 注解。
@ModelAttribute
该模型可以包含异步的反应式类型(例如,来自 Reactor 或 RxJava)。事先
进行渲染,将此类模型属性解析为具体值
并更新模型。单值响应式类型被解析为单个
value 或no value (如果为空),而多值响应式类型(例如 )是
收集并解析为 。AbstractView
Flux<T>
List<T>
配置视图分辨率就像添加 bean 一样简单
添加到您的 Spring 配置中。WebFlux Config 提供了一个
用于视图解析的专用配置 API。ViewResolutionResultHandler
有关与 Spring WebFlux 集成的视图技术的更多信息,请参见View Technologies。
重 定向
视图名称中的特殊前缀允许您执行重定向。(和子类)将此视为一个指令,即
redirect 是必需的。视图名称的其余部分是重定向 URL。redirect:
UrlBasedViewResolver
实际效果与控制器返回 或 相同,但现在控制器本身可以
根据逻辑视图名称进行操作。视图名称 (例如)是相对于当前应用程序的,而视图名称 (例如)重定向到绝对 URL。RedirectView
Rendering.redirectTo("abc").build()
redirect:/some/resource
redirect:https://example.com/arbitrary/path
内容协商
ViewResolutionResultHandler
支持内容协商。它比较请求
媒体类型,其中包含每个选定 .使用第一个支持请求的媒体类型的媒体。View
View
为了支持 JSON 和 XML 等媒体类型, Spring WebFlux 提供了,这是一种通过 HttpMessageWriter 呈现的特殊功能。通常,您会将这些配置为默认
视图。默认视图为
如果它们与请求的媒体类型匹配,则始终选中并使用。HttpMessageWriterView
View
1.4. 带注解的控制器
Spring WebFlux 提供了一个基于 Comments 的编程模型,其中 和 Components 使用 Comments 来表示请求映射、请求输入、
处理异常等。带 Comments 的控制器具有灵活的方法签名和
不必扩展基类,也不必实现特定的接口。@Controller
@RestController
下面的清单显示了一个基本示例:
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
@RestController
class HelloController {
@GetMapping("/hello")
fun handle() = "Hello WebFlux"
}
在前面的示例中,该方法返回 a to be written to the response body.String
1.4.1.@Controller
您可以使用标准 Spring Bean 定义来定义控制器 Bean。
构造型允许自动检测,并与 Spring 通用支持保持一致
用于检测 Classpath 中的类并自动注册 bean 定义
对他们来说。它还充当带注释的类的构造型,指示其角色为
Web 组件。@Controller
@Component
要启用此类 bean 的自动检测,您可以将组件扫描添加到
您的 Java 配置,如下例所示:@Controller
@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
是一个组合的注释,它是
本身的元注释为 和 ,表示其
每个方法都继承了类型级注释,因此,写入
直接到响应正文与视图解析和使用 HTML 模板进行渲染。@Controller
@ResponseBody
@ResponseBody
1.4.2. 请求映射
该注解用于将请求映射到 controllers 方法。它有
按 URL、HTTP 方法、请求参数、标头和媒体匹配的各种属性
类型。您可以在类级别使用它来表示共享映射,也可以在方法级别使用它
以缩小到特定终端节点映射的范围。@RequestMapping
还有 HTTP 方法特定的快捷方式变体:@RequestMapping
-
@GetMapping
-
@PostMapping
-
@PutMapping
-
@DeleteMapping
-
@PatchMapping
上述注释是提供的自定义注释
因为,可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是
using ,默认情况下,它与所有 HTTP 方法匹配。同时,在类级别仍然需要 a 来表示共享映射。@RequestMapping
@RequestMapping
以下示例使用类型和方法级别映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式
您可以使用 glob 模式和通配符映射请求:
模式 | 描述 | 例 |
---|---|---|
|
匹配一个字符 |
|
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径的结尾 |
|
|
匹配路径段并将其捕获为名为 “name” 的变量 |
|
|
将 regexp 匹配为名为 “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 变量会自动转换为适当的类型,或者引发 a。默认情况下支持简单类型(、、 、 等),您可以
注册对任何其他数据类型的支持。
请参见类型转换和 DataBinder
。TypeMismatchException
int
long
Date
URI 变量可以显式命名(例如 ),但您可以
如果名称相同,并且您使用调试编译代码,请省略该详细信息
信息或 Java 8 上的 compiler 标志。@PathVariable("customId")
-parameters
该语法声明一个 URI 变量,该变量与零个或多个剩余路径匹配
段。例如,匹配 下的所有文件,变量捕获 下的完整路径。{*varName}
/resources/{*path}
/resources/
"path"
/resources
该语法使用正则表达式声明一个 URI 变量,该正则表达式具有
语法:。例如,给定 URL 为 ,以下方法
提取名称、版本和文件扩展名:{varName:regex}
{varName:regex}
/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 路径模式还可以具有在启动时解析的嵌入占位符
通过针对本地、系统、环境和
其他属性来源。例如,您可以使用它来根据
一些外部配置。${…}
PropertySourcesPlaceholderConfigurer
Spring WebFlux 使用和 for URI 路径匹配支持。
这两个类都位于 HTTP URL 中,并明确设计用于 HTTP URL
paths 的 Web 应用程序中的 paths,其中在运行时匹配了大量 URI 路径模式。PathPattern PathPatternParser spring-web |
Spring WebFlux 不支持后缀模式匹配——与 Spring MVC 不同,其中
映射(如 也匹配到 .对于基于 URL 的内容
negotiation,如果需要,我们建议使用 query 参数,它更简单、更
显式的,并且不易受到基于 URL 路径的漏洞利用。/person
/person.*
形态比较
当多个模式与一个 URL 匹配时,必须比较它们以找到最佳匹配。此操作已完成
其中,它查找更具体的模式。PathPattern.SPECIFICITY_COMPARATOR
对于每个模式,都会根据 URI 变量和通配符的数量计算分数。 其中 URI 变量的得分低于通配符。总分较低的模式 赢了。如果两个模式具有相同的分数,则选择较长的模式。
捕获全模式(例如 , , )从评分中排除,并且始终
排序为 last。如果两个模式都是 catch-all,则选择较长的模式。**
{*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
您可以在类级别声明共享属性。与大多数其他请求不同
mapping 属性,但是,当在类级别使用时,方法级别的属性
覆盖而不是扩展类级声明。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 petId: String): Pet {
// ...
}
媒体类型可以指定字符集。支持否定表达式 — 例如,表示除 .!text/plain
text/plain
您可以在类级别声明共享属性。与大多数其他请求不同
mapping 属性,但是,当在类级别使用时,方法级别的属性
覆盖而不是扩展类级别声明。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 |
您还可以将 SAME 与请求标头条件一起使用,如下例所示:
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查 是否等于 。myHeader myValue |
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查 是否等于 。myHeader myValue |
HTTP 头, 选项
@GetMapping
并支持 HTTP HEAD
透明地用于请求映射目的。控制器方法不需要更改。
在服务器适配器中应用的响应包装器可确保将标头设置为写入的字节数,而无需实际写入响应。@RequestMapping(method=HttpMethod.GET)
HttpHandler
Content-Length
默认情况下,HTTP OPTIONS 通过将响应头设置为 HTTP 列表来处理
方法。Allow
@RequestMapping
对于没有 HTTP 方法声明的 Headers,标头设置为 。控制器方法应始终声明
支持的 HTTP 方法(例如,通过使用特定于 HTTP 方法的变体 — 、 和 others)。@RequestMapping
Allow
GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
@GetMapping
@PostMapping
你可以显式地将方法映射到 HTTP HEAD 和 HTTP OPTIONS,但是
在常见情况下不是必需的。@RequestMapping
自定义注释
Spring WebFlux 支持使用组合注释进行请求映射。这些注释本身是元注释和组合的,以重新声明具有更窄、更具体目的的属性的子集(或全部)。@RequestMapping
@RequestMapping
@GetMapping
、 、 、 和 是
组合注释的示例。之所以提供它们,是因为可以说,大多数
控制器方法应映射到特定的 HTTP 方法,而不是使用 ,
默认情况下,它与所有 HTTP 方法匹配。如果你需要一个组合的示例
annotations 中,看看这些是怎么声明的。@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
@RequestMapping
Spring WebFlux 还支持具有自定义请求匹配的自定义请求映射属性
逻辑。这是一个更高级的选项,需要子类化和覆盖方法,其中
您可以检查 custom 属性并返回您自己的 .RequestMappingHandlerMapping
getCustomMethodCondition
RequestCondition
显式注册
您可以通过编程方式注册 Handler 方法,这些方法可用于动态 registrations 或高级情况,例如同一处理程序的不同实例 在不同的 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 | 注入 target handlers 和控制器的处理程序 Map。 |
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 | 注入 target handlers 和控制器的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
1.4.3. 处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列
支持的控制器方法参数和返回值。
方法参数
下表显示了支持的控制器方法参数。
响应式类型(Reactor、RxJava 或其他)是 支持需要阻塞 I/O 的参数(例如,读取请求正文)到 被解决。这在 Description (描述) 列中标记。不需要响应式类型 在不需要阻塞的参数上。
JDK 1.8 支持作为方法参数与
具有属性(例如、、 、
和其他)并且等效于 .java.util.Optional
required
@RequestParam
@RequestHeader
required=false
控制器方法参数 | 描述 |
---|---|
|
访问 full — HTTP 请求和响应的容器,
请求和会话属性、方法等。 |
|
访问 HTTP 请求或响应。 |
|
访问会话。这不会强制启动新会话,除非属性 被添加。支持响应式类型。 |
|
当前经过身份验证的用户 — 如果已知,则可能是特定的 implementation class。
支持响应式类型。 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的可用区域设置确定 — 在
effect 中,配置的 /. |
|
与当前请求关联的时区,由 确定。 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的名称-值对。请参见矩阵变量。 |
|
用于访问查询参数。参数值将转换为声明的方法参数
类型。请参阅 请注意,使用 of 是可选的 — 例如,设置其属性。
请参阅此表后面的“任何其他参数”。 |
|
用于访问请求标头。标头值将转换为声明的方法参数
类型。请参阅 |
|
用于访问 Cookie。Cookie 值将转换为声明的方法参数类型。
请参阅 |
|
用于访问 HTTP 请求正文。正文内容被转换为声明的方法
参数类型。支持响应式类型。
请参阅 |
|
用于访问请求标头和正文。body 随实例转换。
支持响应式类型。请参阅 |
|
|
|
用于访问在 HTML 控制器中使用并作为模板公开的模型 视图渲染的一部分。 |
|
要访问模型中的现有属性(如果不存在,则实例化),请使用
应用了数据绑定和验证。另请参阅 请注意,使用 of 是可选的 — 例如,设置其属性。
请参阅此表后面的“任何其他参数”。 |
|
用于访问命令对象(即参数)的验证和数据绑定中的错误。必须声明 , 或 参数
紧跟在 validated method 参数之后。 |
|
用于将表单处理标记为完成,从而触发会话属性的清理
通过类级注解声明。
有关详细信息 |
|
用于准备相对于当前请求的 host、port、scheme 和 context 路径。请参阅 URI 链接。 |
|
用于访问任何 session 属性 — 与 session 中存储的 model 属性相反
作为类级声明的结果。有关详细信息 |
|
用于访问请求属性。有关详细信息 |
任何其他参数 |
如果方法参数与上述任何内容都不匹配,则默认情况下,它会解析为
a 如果它是一个简单类型,由 BeanUtils#isSimpleProperty 确定,
否则为 , 。 |
返回值
下表显示了支持的控制器方法返回值。请注意,反应式 来自 Reactor、RxJava 或其他库的类型是 通常支持所有返回值。
控制器方法返回值 | 描述 |
---|---|
|
返回值通过实例进行编码并写入响应。
请参阅 |
|
返回值指定完整响应,包括 HTTP 标头,并且正文经过编码
通过实例并写入响应。
请参阅 |
|
用于返回带有标头且没有正文的响应。 |
|
要与实例一起解析并与隐式
model — 通过命令对象和方法确定。处理程序
方法还可以通过声明参数以编程方式丰富模型
(如前所述)。 |
|
用于与隐式模型一起呈现的实例 — determined
通过命令对象和方法。handler 方法还可以
通过声明参数以编程方式丰富模型
(如前所述)。 |
|
要添加到隐式模型的属性,其中视图名称是隐式确定的 基于请求路径。 |
|
要添加到模型中的属性,视图名称隐式确定 在请求路径上。 请注意,这是可选的。请参阅后面的“任何其他返回值”
这个表。 |
|
用于模型和视图渲染场景的 API。 |
|
具有 、 可能是异步 (例如,) 返回类型(或返回
值)如果响应还具有 ,
一个参数或一个注释。情况也是如此
如果控制器进行了积极的 ETag 或时间戳检查。
有关详细信息,请参阅 控制器 。 如果以上都不是 true,则返回类型也可以指示 “no response body”
REST 控制器或 HTML 控制器的默认视图名称选择。 |
|
发出服务器发送的事件。当只有 data 需要时,可以省略 wrapper
写入(但是,必须在映射中请求或声明
通过属性)。 |
其他返回值 |
如果返回值以任何其他方式保持未解析状态,则将其视为模型 属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型, 在这种情况下,它仍然未解决。 |
类型转换
一些带注释的控制器方法参数,表示基于字符串的请求输入(例如, , 和 )
如果参数声明为 .@RequestParam
@RequestHeader
@PathVariable
@MatrixVariable
@CookieValue
String
对于此类情况,将根据配置的转换器自动应用类型转换。
默认情况下,支持简单类型(如 、 、 、 等)。类型转换
可以通过 (参见 DataBinder
) 或通过注册 (参见 Spring Field Formatting) 进行自定义。int
long
Date
WebDataBinder
Formatters
FormattingConversionService
类型转换中的一个实际问题是空 String 源值的处理。
如果此类值因类型转换而变为缺失值,则将其视为缺失。
、 和其他目标类型可能就是这种情况。如果要允许注入,请在参数注释上使用标志,或者声明
参数设置为 .null
Long
UUID
null
required
@Nullable
矩阵变量
矩阵变量可以出现在任何路径段中,每个变量用分号和
多个值,以逗号分隔 — 例如 .倍数
值也可以通过重复的变量名称来指定,例如 ."/cars;color=red,green;year=2012"
"color=red;color=green;color=blue"
与 Spring MVC 不同,在 WebFlux 中,URL 中存在或不存在矩阵变量 不会影响请求映射。换句话说,您不需要使用 URI 变量 以屏蔽可变内容。也就是说,如果你想从 controller 方法中,你需要在路径段中添加一个 URI 变量,其中 matrix 变量是预期的。以下示例显示了如何执行此操作:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有路径段都可以包含矩阵变量,您有时可能需要 消除矩阵变量预期位于哪个路径变量中的歧义, 如下例所示:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
您可以定义一个矩阵变量,该变量可以定义为可选变量,并指定一个默认值 如下例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,请使用 ,如下例所示:MultiValueMap
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
您可以使用注解将查询参数绑定到
控制器。以下代码片段显示了用法:@RequestParam
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | 用。@RequestParam |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 用。@RequestParam |
Servlet API“请求参数”概念将查询参数、表单
data 和 multipart 合二为一。但是,在 WebFlux 中,每个都可以通过 .虽然仅绑定到查询参数,但您可以使用
数据绑定,用于将查询参数、表单数据和多部分应用于命令对象。ServerWebExchange @RequestParam |
默认情况下,使用注释的方法参数是必需的,但
您可以通过将 a 的 required 标志设置为 to 或使用包装器声明参数来指定方法参数是可选的。@RequestParam
@RequestParam
false
java.util.Optional
如果目标方法参数 type 不是 ,则会自动应用类型转换。请参阅类型转换。String
在 or 参数上声明注释时,映射中将填充所有查询参数。@RequestParam
Map<String, String>
MultiValueMap<String, String>
请注意,使用 of 是可选的 — 例如,设置其属性。由
default,任何作为简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟。@RequestParam
@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
以下示例获取 and 标头的值:Accept-Encoding
Keep-Alive
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取标头的值。Accept-Encoding |
2 | 获取标头的值。Keep-Alive |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | 获取标头的值。Accept-Encoding |
2 | 获取标头的值。Keep-Alive |
如果目标方法参数 type 不是 ,则会自动应用类型转换。请参阅类型转换。String
当对 、 、 或 参数使用注释时,将填充映射
替换为所有标头值。@RequestHeader
Map<String, String>
MultiValueMap<String, String>
HttpHeaders
内置支持可用于将逗号分隔的字符串转换为
字符串的数组或集合,或者类型转换系统已知的其他类型的集合。为
例如,带注释的方法参数可以是 type of 或 。@RequestHeader("Accept") String String[] List<String> |
@CookieValue
您可以使用注释将 HTTP Cookie 的值绑定到方法参数
在控制器中。@CookieValue
以下示例显示了一个带有 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 值。 |
如果目标方法参数 type 不是 ,则会自动应用类型转换。请参阅类型转换。String
@ModelAttribute
您可以在 method 参数上使用注解来访问
model 或实例化它(如果不存在)。model 属性还与
名称与字段名称匹配的查询参数和表单字段的值。这是
称为数据绑定,它使您不必处理解析和
转换单个查询参数和表单字段。以下示例绑定 的实例 :@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 |
上述示例中的实例解析如下:Pet
-
如果已通过
Model
添加,则从模型。 -
从 HTTP 会话到
@SessionAttributes
。 -
从默认构造函数的调用。
-
从具有与 query 匹配的参数的“主构造函数”的调用 参数或表单字段。参数名称是通过 JavaBeans 或字节码中运行时保留的参数名称确定的。
@ConstructorProperties
获取 model 属性实例后,将应用数据绑定。该类将查询参数和表单字段的名称与字段匹配
目标 上的名称 。在应用类型转换后填充匹配字段
必要时。有关数据绑定(和验证)的更多信息,请参阅验证。有关自定义数据绑定的更多信息,请参阅 DataBinder
。WebExchangeDataBinder
Object
数据绑定可能会导致错误。默认情况下,会引发 a,但是,
要在控制器方法中检查此类错误,您可以添加一个参数
紧挨着 ,如下例所示:WebExchangeBindException
BindingResult
@ModelAttribute
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 添加 .BindingResult |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 添加 .BindingResult |
您可以通过添加 Comments 或 Spring 的 Comments 在数据绑定后自动应用验证(另请参见 Bean 验证和 Spring 验证)。以下示例使用注释:javax.validation.Valid
@Validated
@Valid
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 对 model 属性参数使用@Valid |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 对 model 属性参数使用@Valid |
与 Spring MVC 不同,Spring WebFlux 在模型中支持反应式类型——例如,或 .您可以声明一个参数
有或没有响应式类型包装器,并且它将被相应地解析,
如有必要,调整为实际值。但是,请注意,要使用参数,必须在参数之前声明参数,而不使用反应式
type 包装器,如前所述。或者,您可以通过
reactive 类型,如下例所示: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 ->
// ...
}
}
请注意,使用 of 是可选的 — 例如,设置其属性。
默认情况下,任何不是简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟。@ModelAttribute
@ModelAttribute
@SessionAttributes
@SessionAttributes
用于在
请求。它是一个类型级注释,用于声明
特定控制器。这通常列出模型属性的名称或
model 属性,这些属性应该透明地存储在会话中以供后续使用
请求访问。WebSession
请考虑以下示例:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用注释。@SessionAttributes |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | 使用注释。@SessionAttributes |
在第一个请求中,当将名称为 的 model 属性添加到模型时,
它会自动提升并保存在 .它一直留在那里,直到
另一个控制器方法使用 method 参数来清除存储空间
如下例所示: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
如果您需要访问全局管理的预先存在的会话属性
(即,在控制器外部 — 例如,通过过滤器),并且可能存在也可能不存在,
您可以在 method 参数上使用 Annotation,如下例所示:@SessionAttribute
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | 用。@SessionAttribute |
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
1 | 用。@SessionAttribute |
对于需要添加或删除会话属性的使用案例,请考虑注入到 controller 方法中。WebSession
作为控制器的一部分,在会话中临时存储模型属性
workflow,请考虑使用 ,如 @SessionAttributes
中所述。SessionAttributes
@RequestAttribute
与 类似,您可以使用注解来
访问之前创建的预先存在的请求属性(例如,通过 ),
如下例所示:@SessionAttribute
@RequestAttribute
WebFilter
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 用。@RequestAttribute |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 用。@RequestAttribute |
多部分内容
如 Multipart Data 中所述,提供对 multipart 的访问
内容。在控制器中处理文件上传表单(例如,从浏览器)的最佳方法
是通过数据绑定到 Command 对象,
如下例所示:ServerWebExchange
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 服务中的非浏览器客户端提交分段请求 场景。以下示例将文件与 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 — 类似于 ),
您可以声明 具体 target ,而不是 ,如下例所示:@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 |
可以与 Spring 的 Comments 结合使用,这将应用标准 Bean 验证。验证
错误会导致 a,从而导致 400 (BAD_REQUEST) 响应。
异常包含错误详细信息,也可以进行处理
在控制器方法中,使用异步包装器声明参数,然后使用
错误相关运算符:@RequestPart
javax.validation.Valid
@Validated
WebExchangeBindException
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 |
要以流式处理方式按顺序访问分段数据,您可以改用 with(或在 Kotlin 中),如下例所示:@RequestBody
Flux<Part>
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
你可以使用 Comments 让请求正文通过 HttpMessageReader 读取并反序列化为 。
以下示例使用参数:@RequestBody
Object
@RequestBody
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
与 Spring MVC 不同,在 WebFlux 中,method 参数支持反应式类型
以及完全无阻塞的读取和(客户端到服务器)流式处理。@RequestBody
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
您可以使用 WebFlux Config 的 HTTP 消息编解码器选项来 配置或自定义消息阅读器。
可以与 Spring 的 Comments 结合使用,这将应用标准 Bean 验证。验证
错误会导致 ,从而导致 400 (BAD_REQUEST) 响应。
异常包含错误详细信息,可以在
controller 方法,方法是使用 async 包装器声明参数,然后使用 error
相关运算符:@RequestBody
javax.validation.Valid
@Validated
WebExchangeBindException
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
大致相同,但基于
Container 对象,该对象公开请求标头和正文。以下示例使用 :HttpEntity
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
您可以在方法上使用 Comments 来序列化返回值
传递给响应正文。以下内容
示例展示了如何做到这一点:@ResponseBody
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别也受支持,在这种情况下,它由
所有控制器方法。这就是 的效果,仅此而已
比标有 和 的元注释 .@RestController
@Controller
@ResponseBody
您可以将方法与 JSON 序列化视图结合使用。
有关详细信息,请参阅 Jackson JSON。@ResponseBody
您可以使用 WebFlux Config 的 HTTP 消息编解码器选项来 配置或自定义消息写入。
ResponseEntity
ResponseEntity
与 @ResponseBody 类似,
但具有 status 和 headers。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux 支持使用单值响应式类型
生成异步和/或单值和多值响应式类型
为了身体。这允许各种异步响应,如下所示:ResponseEntity
ResponseEntity
-
ResponseEntity<Mono<T>>
或将响应状态设为 标头,而正文将在稍后异步提供。 如果主体由 0..1 个值组成,或者它可以生成多个值,则使用。ResponseEntity<Flux<T>>
Mono
Flux
-
Mono<ResponseEntity<T>>
提供所有三个 — 响应状态、标头和正文, 异步的。这允许响应状态和标头发生变化 取决于异步请求处理的结果。 -
Mono<ResponseEntity<Mono<T>>>
或者是另一个 可能,尽管不太常见的替代方案。它们提供响应状态和标头 首先是异步的,然后是响应正文,也是异步的,其次。Mono<ResponseEntity<Flux<T>>>
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring WebFlux 为 Jackson 的序列化视图提供了内置支持,
,它只允许在 .要将其与 or 控制器方法一起使用,您可以使用 Jackson 的 annotation 来激活序列化视图类,如下例所示:Object
@ResponseBody
ResponseEntity
@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 允许视图类数组,但您只能为每个
controller 方法。如果需要激活多个视图,请使用复合界面。 |
1.4.4.Model
您可以使用注释:@ModelAttribute
-
在方法中的方法参数上 从模型创建或访问 Object,并通过 .
@RequestMapping
WebDataBinder
-
作为 or 类中的方法级注释,帮助 在调用任何方法之前初始化模型。
@Controller
@ControllerAdvice
@RequestMapping
-
在方法上将其返回值标记为 model 属性。
@RequestMapping
本节讨论方法或前面列表中的第二项。
控制器可以有任意数量的方法。所有这些方法都是
在同一个控制器中的方法之前调用。还可以通过 在控制器之间共享方法。有关更多详细信息,请参阅 Controller Advice 部分。@ModelAttribute
@ModelAttribute
@RequestMapping
@ModelAttribute
@ControllerAdvice
@ModelAttribute
方法具有灵活的方法签名。它们支持许多相同的
参数作为方法(除了 itself 和 anything
与请求正文相关)。@RequestMapping
@ModelAttribute
以下示例使用一种方法:@ModelAttribute
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
如果未明确指定名称,则根据类型选择默认名称。
如 Conventions 的 javadoc 中所述。
您始终可以使用重载的方法或
通过 name 属性 on(对于返回值)。addAttribute @ModelAttribute |
与 Spring MVC 不同,Spring WebFlux 在模型中显式支持响应式类型
(例如,或 )。这种异步模型
属性可以透明地解析(并更新模型)为其实际值
在调用时,如果参数为
在没有包装器的情况下声明,如下例所示:Mono<Account>
io.reactivex.Single<Account>
@RequestMapping
@ModelAttribute
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
// ...
}
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 {
// ...
}
此外,任何具有响应式类型包装器的模型属性都会被解析为它们的 实际值(并更新了模型)。
您还可以用作方法的方法级注释,在这种情况下,方法的返回值将解释为
model 属性。这通常不是必需的,因为它是 HTML 中的默认行为
控制器,除非返回值是 a 否则会被解释
作为视图名称。 还可以帮助自定义 Model 属性名称,
如下例所示:@ModelAttribute
@RequestMapping
@RequestMapping
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
-
将请求参数(即表单数据或查询)绑定到模型对象。
-
转换基于 的请求值(例如请求参数、路径变量、 headers、cookie 等)传递给 controller 方法参数的目标类型。
String
-
在呈现 HTML 表单时将模型对象值格式化为值。
String
@InitBinder
方法可以注册特定于控制器的 或
弹簧和组件。此外,您可以使用 WebFlux Java 配置在全局共享的 .java.beans.PropertyEditor
Converter
Formatter
Converter
Formatter
FormattingConversionService
@InitBinder
方法支持许多与方法
do,但 (Command Object) 参数除外。通常,它们被声明
带有参数、用于注册和返回值。
以下示例使用注释:@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))
}
// ...
}
或者,当通过共享 使用基于 的设置时,您可以重复使用相同的方法并注册
特定于 Controller 的实例,如下例所示:Formatter
FormattingConversionService
Formatter
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
1 | 添加自定义格式化程序(在本例中为 a , )。DateFormatter |
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
1 | 添加自定义格式化程序(在本例中为 a , )。DateFormatter |
模型设计
在 Web 应用程序的上下文中,数据绑定涉及 HTTP 请求的绑定 parameters ((即表单数据或查询参数) ) 添加到模型对象中的属性,以及 其嵌套对象。
仅公开遵循 JavaBeans 命名约定的属性以进行数据绑定,例如,公开属性的方法。public
public String getFirstName()
public void setFirstName(String)
firstName
模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO (Plain Old Java Object)。 |
默认情况下, Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为 客户端可以面向任何公共属性路径,甚至是一些不需要的公共属性路径 针对给定的使用案例。
例如,给定一个 HTTP 表单数据端点,恶意客户端可能会提供 存在于模型对象图中但不属于 HTML 表单的属性 在浏览器中显示。这可能会导致在模型对象上设置数据,并且任何 的嵌套对象中。
推荐的方法是使用仅公开
与表单提交相关的属性。例如,在用于更改
用户的电子邮件地址,则 Model 对象应声明一组最小属性,例如
如下所示。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;
}
}
如果您不能或不想为每个数据使用专用模型对象
binding 用例中,您必须限制允许进行数据绑定的属性。
理想情况下,您可以通过 上的方法注册允许的字段模式来实现此目的。setAllowedFields()
WebDataBinder
例如,要在应用程序中注册允许的字段模式,您可以在 or 组件中实现一个方法,如下所示:@InitBinder
@Controller
@ControllerAdvice
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以注册不允许的
field 模式。
但请注意,“允许列表”比“拒绝列表”更安全。因此, 应该优先于 .setDisallowedFields()
DataBinder
setAllowedFields()
setDisallowedFields()
请注意,与允许的字段模式匹配区分大小写;鉴于匹配 Against Disallowed 字段模式不区分大小写。此外,匹配 不允许的模式将不被接受,即使它也恰好与 允许列表。
正确配置允许和不允许的字段模式非常重要 直接公开域模型以进行数据绑定时。否则,它是一个 安全风险大。 此外,强烈建议您不要使用域中的类型 模型(如 JPA 或 Hibernate 实体)作为数据绑定场景中的模型对象。 |
1.4.6. 管理异常
@Controller
@ControllerAdvice类可以具有处理控制器方法异常的方法。以下内容
example 包含这样的处理程序方法:@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 |
该异常可以与正在传播的顶级异常(即,直接引发的异常)或顶级包装器中的直接原因匹配
exception(例如,包装在 ) 中的 an )。IOException
IOException
IllegalStateException
对于匹配的异常类型,最好将目标异常声明为方法参数,
如前面的示例所示。或者,注释声明可以缩小
异常类型进行匹配。我们通常建议在
argument 签名,并在具有相应顺序的 Prioritized 上声明您的主根异常映射。
有关详细信息,请参阅 MVC 部分。@ControllerAdvice
WebFlux 中的方法支持相同的方法参数和
返回值作为方法,请求正文除外 -
和 -相关的方法参数。@ExceptionHandler @RequestMapping @ModelAttribute |
对 Spring WebFlux 中方法的支持由 for 方法提供。有关更多详细信息,请参阅 DispatcherHandler
。@ExceptionHandler
HandlerAdapter
@RequestMapping
REST API 异常
REST 服务的一个常见要求是在
响应。Spring Framework 不会自动这样做,因为表示
of error details 是特定于应用程序的。但是,可以使用带有 return 的方法
值以设置响应的状态和正文。此类方法也可以声明
在类中将它们应用于全局。@RestController
@ExceptionHandler
ResponseEntity
@ControllerAdvice
请注意, Spring WebFlux 没有 Spring MVC 的等效项,因为 WebFlux 仅引发(或其子类),并且不需要转换为
HTTP 状态代码。ResponseEntityExceptionHandler ResponseStatusException |
1.4.7. 控制器建议
通常,、 和 方法适用
在声明它们的类 (或类层次结构) 中。如果你
希望这些方法更全局地应用(跨控制器),你可以在
类注释为 或 。@ExceptionHandler
@InitBinder
@ModelAttribute
@Controller
@ControllerAdvice
@RestControllerAdvice
@ControllerAdvice
被注释为 ,这意味着此类可以是
通过组件扫描注册为 Spring bean。 是带有注解的合成注解
同时具有 和 ,这实质上意味着方法通过消息转换呈现到响应正文
(相对于视图分辨率或模板渲染)。@Component
@RestControllerAdvice
@ControllerAdvice
@ResponseBody
@ExceptionHandler
在启动时,和 方法的基础设施类会检测带 Comments 的 Spring bean,然后应用它们的
方法。全局方法(来自 ) 是
在本地 (来自 ) 之后应用 。相比之下,global 和 methods 在 local ones 之前应用。@RequestMapping
@ExceptionHandler
@ControllerAdvice
@ExceptionHandler
@ControllerAdvice
@Controller
@ModelAttribute
@InitBinder
默认情况下,方法适用于每个请求(即所有控制器),
但是,您可以通过使用
annotation 中,如下例所示:@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,这是一个轻量级函数式编程模型,其中函数 用于路由和处理请求,而 Contract 旨在实现不变性。 它是基于 Comments 的编程模型的替代方案,但在其他方面运行 相同的 Reactive Core 基础。
1.5.1. 概述
在 WebFlux.fn 中,HTTP 请求使用 : 处理,该函数接受并返回延迟(即 )。
请求和响应对象都有不可变的契约,这些契约提供对 JDK 8 友好的
访问 HTTP 请求和响应。 等效于
基于注释的编程模型。HandlerFunction
ServerRequest
ServerResponse
Mono<ServerResponse>
HandlerFunction
@RequestMapping
传入请求被路由到具有 : 的处理程序函数,该函数
接受并返回一个延迟的(即 )。
当 router 函数匹配时,将返回一个处理程序函数;否则为空 Mono。 等同于注释,但使用 major
不同之处在于 router 函数不仅提供数据,还提供行为。RouterFunction
ServerRequest
HandlerFunction
Mono<HandlerFunction>
RouterFunction
@RequestMapping
RouterFunctions.route()
提供便于创建路由器的 Router 构建器,
如下例所示:
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 | 使用协程 router DSL 创建路由器,也可以通过 .router { } |
运行 a 的一种方法是将其转换为 an 并安装它
通过其中一个内置服务器适配器:RouterFunction
HttpHandler
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多数应用程序可以通过 WebFlux Java 配置运行,请参阅运行服务器。
1.5.2. HandlerFunction 函数
ServerRequest
并且是提供 JDK 8 友好的不可变接口
访问 HTTP 请求和响应。
请求和响应都提供 Reactive Streams 背压
与身体流相反。
请求正文用 Reactor 或 表示。
响应正文由任何 Reactive Streams 表示,包括 和 。
有关更多信息,请参阅 反应式库。ServerResponse
Flux
Mono
Publisher
Flux
Mono
服务器请求
ServerRequest
提供对 HTTP 方法、URI、标头和查询参数的访问,
而对 body 的访问是通过 methods.body
以下示例将请求正文提取为 :Mono<String>
Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()
以下示例将正文提取为 a(或 Kotlin 中的 a),
其中,对象是从某种序列化形式(例如 JSON 或 XML)解码的:Flux<Person>
Flow<Person>
Person
Flux<Person> people = request.bodyToFlux(Person.class);
val people = request.bodyToFlow<Person>()
前面的示例是使用更通用的 、
它接受函数式策略接口。Utility 类提供对许多实例的访问。例如,前面的示例可以
也写成如下: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)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
以下示例显示如何访问表单数据:
Mono<MultiValueMap<String, String>> map = request.formData();
val map = request.awaitFormData()
以下示例显示如何以 map 形式访问多部分数据:
Mono<MultiValueMap<String, Part>> map = request.multipartData();
val map = request.awaitMultipartData()
以下示例显示了如何以流式处理方式一次访问多个部分:
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
服务器响应
ServerResponse
提供对 HTTP 响应的访问,并且由于它是不可变的,因此您可以使用
一个创建它的方法。您可以使用生成器设置响应状态,以添加响应
headers 或提供正文。以下示例使用 JSON 创建 200 (OK) 响应
内容:build
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
以下示例显示如何构建带有标头但无正文的 201 (CREATED) 响应:Location
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
根据所使用的编解码器,可以传递 hint 参数来自定义 body 是序列化的或反序列化的。例如,要指定 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 是一个处理程序函数,它将在存储库中找到的所有对象作为
JSON 的 JSON 格式。Person |
2 | createPerson 是一个处理程序函数,用于存储请求正文中包含的 new。
请注意,返回 : 一个发出
从请求中读取并存储人员时的完成信号。因此,我们使用该方法在收到该完成信号时发送响应(即
当 已保存时)。Person PersonRepository.savePerson(Person) Mono<Void> Mono build(Publisher<Void>) Person |
3 | getPerson 是一个处理程序函数,它返回一个 person,由路径
变量。我们从存储库中检索该响应并创建一个 JSON 响应(如果是
发现。如果未找到,我们将返回 404 Not Found 响应。id Person switchIfEmpty(Mono<T>) |
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 是一个处理程序函数,它将在存储库中找到的所有对象作为
JSON 的 JSON 格式。Person |
2 | createPerson 是一个处理程序函数,用于存储请求正文中包含的 new。
请注意,这是一个没有返回类型的 suspending 函数。Person PersonRepository.savePerson(Person) |
3 | getPerson 是一个处理程序函数,它返回一个 person,由路径
变量。我们从存储库中检索该响应并创建一个 JSON 响应(如果是
发现。如果未找到,我们将返回 404 Not Found 响应。id Person |
验证
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 | Create instance 创建实例。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 | Create instance 创建实例。Validator |
2 | 应用验证。 |
3 | 引发 400 响应的异常。 |
处理程序还可以通过创建和注入来使用标准 bean 验证 API (JSR-303)
基于 的全局实例。
参见 Spring Validation。Validator
LocalValidatorFactoryBean
1.5.3.RouterFunction
Router 函数用于将请求路由到相应的 .
通常,您不会自己编写 router 函数,而是使用 Utility 类上的方法创建一个。 (无参数)为您提供用于创建路由器的 Fluent 构建器
函数,而提供直接的
创建路由器。HandlerFunction
RouterFunctions
RouterFunctions.route()
RouterFunctions.route(RequestPredicate, HandlerFunction)
通常,建议使用构建器,因为它提供了
适用于典型映射场景的便捷捷径,无需难以发现
static imports。
例如,router 函数构建器提供了为 GET 请求创建 Map 的方法;和 POST。route()
GET(String, HandlerFunction)
POST(String, HandlerFunction)
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种引入其他
谓词。
对于每个 HTTP 方法,都有一个重载的变体,它将 a 作为
参数,但可以表示其他约束。RequestPredicate
谓词
您可以编写自己的 ,但 utility 类
提供常用的实现,基于请求路径、HTTP 方法、内容类型、
等等。
以下示例使用请求谓词创建基于 header 的约束:RequestPredicate
RequestPredicates
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
路线
路由器功能按顺序评估:如果第一个路由不匹配,则 second 被评估,依此类推。 因此,在一般路由之前声明更具体的路由是有意义的。 在将路由器函数注册为 Spring bean 时,这一点也很重要,也是如此 稍后描述。 请注意,此行为与基于 Comments 的编程模型不同,其中 “最具体”控制器方法会自动选取。
使用 router 函数构建器时,所有定义的路由都组合成一个路由,该路由从 返回。
还有其他方法可以将多个 router 功能组合在一起: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 是在其他地方创建并添加到 Route built 的 router 函数。 |
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 是在其他地方创建并添加到 Route built 的 router 函数。 |
嵌套路由
一组 router 函数通常具有共享谓词,例如
共享路径。在上面的示例中,共享谓词将是一个路径谓词,该
matches ,由三个路由使用。使用注释时,您需要删除
此复制通过使用映射到 .在 WebFlux.fn 中,路径谓词可以通过
router 函数构建器。例如,上面示例的最后几行可以是
通过使用嵌套路由,通过以下方式进行了改进:/person
@RequestMapping
/person
path
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
1 | 请注意,第二个参数 of 是采用路由器构建器的使用者。path |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以使用
构建器上的方法。
上面仍然包含一些共享 -header 谓词形式的重复。
我们可以通过将该方法与以下方法结合使用来进一步改进:nest
Accept
nest
accept
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
POST(handler::createPerson)
}
}
}
1.5.4. 运行服务器
如何在 HTTP 服务器中运行路由器功能?一个简单的选择是将路由器
函数设置为:HttpHandler
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
然后,您可以按照 HttpHandler 获取特定于服务器的说明,将 return 与许多服务器适配器一起使用。HttpHandler
Spring Boot 也使用一个更典型的选项,是通过 WebFlux Config 使用基于DispatcherHandler
的设置运行,它使用 Spring 配置来声明
处理请求所需的组件。WebFlux Java 配置声明以下内容
支持功能终端节点的基础设施组件:
-
RouterFunctionMapping
:在 Spring 中检测到一个或多个 bean 配置,对它们进行排序,通过 组合它们,并将请求路由到生成的 composed 。RouterFunction<?>
RouterFunction.andOther
RouterFunction
-
HandlerFunctionAdapter
:允许调用 a 映射到请求。DispatcherHandler
HandlerFunction
-
ServerResponseResultHandler
:通过调用 .HandlerFunction
writeTo
ServerResponse
前面的组件允许功能端点适应请求
处理生命周期,并且(可能)与带注释的控制器并行运行,如果
任何 (any) 都已声明。这也是 Spring Boot WebFlux 如何启用功能端点
起动机。DispatcherHandler
下面的示例显示了一个 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. 过滤处理程序函数
您可以通过在路由上使用 、 或 方法来筛选处理程序函数
函数构建器。
使用注释,您可以通过使用 、 a 或两者来实现类似的功能。
该筛选条件将应用于构建器构建的所有路由。
这意味着嵌套路由中定义的筛选条件不适用于 “top-level” 路由。
例如,请考虑以下示例: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(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 添加自定义请求标头的筛选条件仅适用于两个 GET 路由。before |
2 | 记录响应的筛选条件将应用于所有路由,包括嵌套路由。after |
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST(handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
}
1 | 添加自定义请求标头的筛选条件仅适用于两个 GET 路由。before |
2 | 记录响应的筛选条件将应用于所有路由,包括嵌套路由。after |
路由器构建器上的方法采用 : 一个
函数,该函数采用 AND 并返回 .
handler 函数参数表示链中的下一个元素。
这通常是路由到的处理程序,但也可以是另一个
filter (如果应用了多个)。filter
HandlerFilterFunction
ServerRequest
HandlerFunction
ServerResponse
现在我们可以向路由添加一个简单的安全过滤器,假设我们有一个
可以确定是否允许特定路径。
以下示例显示了如何执行此操作:SecurityManager
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用 is optional。
我们只允许在允许访问时运行处理程序函数。next.handle(ServerRequest)
除了在路由器函数构建器上使用该方法外,还可以应用
通过 Filter 筛选到现有路由器函数。filter
RouterFunction.filter(HandlerFilterFunction)
对功能终端节点的 CORS 支持通过专用的 CorsWebFilter 提供。 |
1.6. URI 链接
本节描述了 Spring Framework 中可用于准备 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. Uri生成器
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
实现 .您可以依次使用 .一起提供一种可插入的机制,以基于
共享配置,例如基本 URL、编码首选项和其他详细信息。UriBuilder
UriBuilder
UriBuilderFactory
UriBuilderFactory
UriBuilder
您可以配置 和 来自定义 URI 的准备。 是默认值
实现 that uses internal,
公开共享配置选项。RestTemplate
WebClient
UriBuilderFactory
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()
此外,您也可以直接使用。它类似于 using ,但它不是静态工厂方法,而是一个实际的实例
,其中包含 configuration 和 preferences,如下例所示:DefaultUriBuilderFactory
UriComponentsBuilder
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.6.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode() 中: 首先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。
-
UriComponents#encode() 中: 在 URI 变量展开后对 URI 组件进行编码。
这两个选项都用转义的八位字节替换非 ASCII 字符和非法字符。但是,第一个选项 还会替换 URI 变量中出现的具有保留含义的字符。
请考虑 “;”,它在 path 中是合法的,但具有保留的含义。第一个选项将 “;” 在 URI 变量中带有 “%3B”,但在 URI 模板中没有。相比之下,第二个选项永远不会 替换 “;”,因为它是路径中的合法字符。 |
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量作为不透明数据进行完全编码,而第二个选项在 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")
和 扩展和编码 URI 模板在 内部通过
策略。两者都可以使用自定义策略
如下例所示:WebClient
RestTemplate
UriBuilderFactory
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()
该实现在内部使用
展开并编码 URI 模板。作为工厂,它提供了一个配置位置
编码方法,基于以下编码模式之一:DefaultUriBuilderFactory
UriComponentsBuilder
-
TEMPLATE_AND_VALUES
: 使用 ,对应于 前面列表中的第一个选项,用于预编码 URI 模板并在 扩大。UriComponentsBuilder#encode()
-
VALUES_ONLY
:不对 URI 模板进行编码,而是应用严格编码 到 URI 变量中,然后再将它们扩展到 模板。UriUtils#encodeUriVariables
-
URI_COMPONENT
:使用对应于前面列表中的第二个选项的 ,以 在 URI 变量展开后对 URI 组件值进行编码。UriComponents#encode()
-
NONE
:不应用编码。
设置为 historic
原因和向后兼容性。依赖于默认值
in ,该 URL 已从 in 更改
5.0.x 复制到 5.1 中。RestTemplate
EncodingMode.URI_COMPONENT
WebClient
DefaultUriBuilderFactory
EncodingMode.URI_COMPONENT
EncodingMode.TEMPLATE_AND_VALUES
1.7. CORS
Spring WebFlux 允许您处理 CORS(跨域资源共享)。本节 介绍如何执行此操作。
1.7.1. 简介
出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。 例如,您可以将银行账户放在一个选项卡中,而 evil.com 放在另一个选项卡中。脚本 evil.com 中,应该无法使用 凭证 — 例如,从您的账户取款!
1.7.2. 处理
CORS 规范区分了印前检查请求、简单请求和实际请求。 要了解 CORS 的工作原理,您可以阅读这篇文章,其中 许多其他方法,或者参见 规范 了解更多详情。
Spring WebFlux 实现提供了对 CORS 的内置支持。成功后
将请求映射到处理程序,则 a 会检查
given request 和 handler 并采取进一步的操作。处理印前检查请求
直接,而简单和实际的 CORS 请求被拦截、验证,并具有
所需的 CORS 响应标头集。HandlerMapping
HandlerMapping
为了启用跨域请求(即,标头存在且
与请求的主机不同),您需要有一些显式声明的 CORS
配置。如果未找到匹配的 CORS 配置,则印前 Backup 请求为
拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中
因此,浏览器会拒绝它们。Origin
每个都可以使用基于 URL 模式的映射单独配置。在大多数情况下,应用程序
使用 WebFlux Java 配置来声明此类映射,这将产生单个
global map 传递给所有 implementation。HandlerMapping
CorsConfiguration
HandlerMapping
您可以将 级别的全局 CORS 配置与更多
精细的处理程序级 CORS 配置。例如,带注解的控制器可以使用
类或方法级注释(其他处理程序可以实现)。HandlerMapping
@CrossOrigin
CorsConfigurationSource
组合全局配置和本地配置的规则通常是累加的 — 例如,
所有全球和所有本地源。对于只能使用单个值
accepted,例如 和 ,local 将覆盖 global 值。有关更多详细信息,请参见 CorsConfiguration#combine(CorsConfiguration)。
allowCredentials
maxAge
要从源中了解更多信息或进行高级自定义,请参阅:
|
1.7.3. 凭证请求
将 CORS 与凭证请求一起使用需要启用 。请注意,
此选项与配置的域建立高级别的信任,并且还会增加
通过公开敏感的用户特定信息,Web 应用程序的攻击面
例如 Cookie 和 CSRF 令牌。allowedCredentials
启用凭证还会影响已配置 CORS 通配符的处理方式:"*"
-
通配符在 中未获得授权,但可以选择 该属性可用于匹配一组动态的 Origins。
allowOrigins
allowOriginPatterns
-
当设置为 or 时,和响应标头通过复制相关的 标头和方法。
allowedHeaders
allowedMethods
Access-Control-Allow-Headers
Access-Control-Allow-Methods
-
设置为 on 时,将设置响应标头 添加到配置的标头列表或通配符。虽然 CORS 规范 不允许使用通配符 when 设置为 ,大多数浏览器都支持通配符,并且响应标头在 CORS 处理,因此通配符是 无论属性的值如何,都指定。
exposedHeaders
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
true
allowCredentials
虽然这种通配符配置可能很方便,但建议在可能的情况下配置 一组有限的值,以提供更高级的安全性。 |
1.7.4.@CrossOrigin
@CrossOrigin
注解支持对带注解的控制器方法进行跨域请求,因为
以下示例显示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,允许:@CrossOrigin
-
所有来源。
-
所有标头。
-
控制器方法映射到的所有 HTTP 方法。
allowCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。启用后,必须
设置为一个或多个特定域(但不是 Special Value )或
该属性可用于匹配一组动态的 Origins。allowOrigins
"*"
allowOriginPatterns
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.5. 全局配置
除了细粒度的 controller 方法级配置之外,您可能还希望
也定义一些全局 CORS 配置。您可以在任何 .但是,大多数应用程序都使用
WebFlux Java 配置来执行此操作。CorsConfiguration
HandlerMapping
默认情况下,全局配置会启用以下内容:
-
所有来源。
-
所有标头。
-
GET
、 和 方法。HEAD
POST
allowedCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。启用后,必须
设置为一个或多个特定域(但不是 Special Value )或
该属性可用于匹配一组动态的 Origins。allowOrigins
"*"
allowOriginPatterns
maxAge
设置为 30 分钟。
要在 WebFlux Java 配置中启用 CORS,您可以使用回调
如下例所示:CorsRegistry
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// 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.6. CORSWebFilter
您可以通过内置的 CorsWebFilter 应用 CORS
支持,它是一个
与功能端点拟合良好。
如果您尝试将 与 Spring Security 一起使用,请记住 Spring
Security 内置了对
CORS 的。CorsFilter |
要配置过滤器,可以声明一个 bean 并将 a 传递给其构造函数,如下例所示:CorsWebFilter
CorsConfigurationSource
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
1.8. Web 安全
Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。请参阅 Spring Security 参考文档,包括:
1.9. 查看技术
Spring WebFlux 中视图技术的使用是可插拔的。无论您决定 使用 Lymeleaf、FreeMarker 或其他一些视图技术主要是 配置更改。本章介绍了与 Spring 集成的视图技术 WebFlux 的 Web Flux 中。我们假设您已经熟悉 View Resolution。
1.9.1. 百里香叶
Thymeleaf 是一个现代的服务器端 Java 模板引擎,强调自然 HTML 可以通过双击在浏览器中预览的模板,这非常 有助于独立处理 UI 模板(例如,由设计人员),而无需 正在运行的服务器。Thymeleaf 提供了一组广泛的功能,并且正在积极开发 并维持。有关更完整的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目管理。这
配置涉及一些 bean 声明,例如 、 和 。有关详细信息,请参阅 Thymeleaf+Spring 和 WebFlux 集成公告。SpringResourceTemplateResolver
SpringWebFluxTemplateEngine
ThymeleafReactiveViewResolver
1.9.2. 自由标记
Apache FreeMarker 是一个模板引擎,用于生成任何 从 HTML 到电子邮件等的文本输出类型。Spring Framework 内置了 用于将 Spring WebFlux 与 FreeMarker 模板一起使用的集成。
View 配置
以下示例显示了如何将 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 配置
你可以通过设置适当的 bean 将 FreeMarker 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 对象(由 Spring 管理)。
属性。该属性需要
对象,并且该属性需要 .以下示例演示如何使用 :Configuration
FreeMarkerConfigurer
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,其中包括一个元素。此元素主要允许表单显示来自
表单支持对象,并显示
Web 或业务层。Spring 在 FreeMarker 中也支持相同的功能,
具有用于生成表单输入元素本身的附加便捷宏。<spring:bind/>
Validator
Bind 宏
文件中维护了一组标准的宏
FreeMarker,因此它们始终可用于适当配置的应用程序。spring-webflux.jar
Spring 模板库中定义的一些宏被认为是内部的
(私有),但宏定义中不存在此类范围,因此所有宏都可见
调用代码和用户模板。以下各节仅重点介绍宏
您需要直接从模板中调用。如果您想查看宏代码
直接调用该文件,并位于包中。spring.ftl
org.springframework.web.reactive.result.view.freemarker
有关绑定支持的更多详细信息,请参阅简单 Spring MVC 的绑定。
1.9.3. 脚本视图
Spring Framework 有一个内置的集成,用于将 Spring WebFlux 与任何 可以在 JSR-223 Java 脚本引擎上运行的模板库。 下表显示了我们在不同脚本引擎上测试的模板库:
脚本库 | 脚本引擎 |
---|---|
集成任何其他脚本引擎的基本规则是它必须实现 and 接口。ScriptEngine Invocable |
要求
您需要在 Classpath 上具有脚本引擎,其详细信息因脚本引擎而异:
-
Nashorn JavaScript 引擎随 Java 8+ 的强烈建议使用可用的最新更新版本。
-
应该将 JRuby 添加为 Ruby 支持的依赖项。
-
应将 Jython 添加为 Python 支持的依赖项。
-
org.jetbrains.kotlin:kotlin-script-util
dependency 并添加包含一行的文件以支持 Kotlin 脚本。有关更多详细信息,请参阅此示例。META-INF/services/javax.script.ScriptEngineFactory
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
您需要有脚本模板库。对 JavaScript 执行此操作的一种方法是 通过 WebJars 进行。
脚本模板
您可以声明一个 bean 来指定要使用的脚本引擎,
要加载的脚本文件、要调用的函数来渲染模板,等等。
以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:ScriptTemplateConfigurer
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
使用以下参数调用该函数:render
-
String template
:模板内容 -
Map model
:视图模型 -
RenderingContext renderingContext
:RenderingContext
,用于访问应用程序上下文、区域设置、模板加载程序和 URL(自 5.0 起)
Mustache.render()
与此签名本机兼容,因此您可以直接调用它。
如果您的模板技术需要一些自定义,则可以提供一个脚本,该脚本 实现自定义 render 函数。例如,Handlerbars 需要在使用模板之前编译模板,并且需要一个 polyfill 来模拟一些 浏览器工具在服务器端脚本引擎中不可用。 以下示例显示如何设置自定义 render 函数:
@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
}
}
使用非线程安全时,需要将属性设置为
具有非并发性模板库的脚本引擎,例如 Handlebars 或
React 在 Nashorn 上运行。在这种情况下,由于此错误,需要 Java SE 8 Update 60,但通常是
建议在任何情况下使用最新的 Java SE 补丁版本。sharedEngine false |
polyfill.js
仅定义 Handlebars 正常运行所需的对象,
如以下代码段所示:window
var window = {};
此基本实现在使用模板之前对其进行编译。A 生产
ready 实现还应存储和重用缓存的模板或预编译的模板。
这可以在脚本端完成,也可以完成您需要的任何自定义(管理
模板引擎配置)。
以下示例显示了如何编译模板:render.js
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.9.4. JSON 和 XML
出于 Content Negotiation 目的,能够交替使用
使用 HTML 模板或其他格式(如 JSON 或 XML)渲染模型之间,
取决于客户端请求的内容类型。为了支持这样做,Spring WebFlux
提供 ,您可以使用它从 中插入任何可用的编解码器,例如 、 、
或。HttpMessageWriterView
spring-web
Jackson2JsonEncoder
Jackson2SmileEncoder
Jaxb2XmlEncoder
与其他视图技术不同,它不需要 a,而是配置为默认视图。您可以
配置一个或多个这样的默认视图,包装不同的实例
或 instances 的 S 。在运行时使用与请求的内容类型匹配的 URL。HttpMessageWriterView
ViewResolver
HttpMessageWriter
Encoder
在大多数情况下,一个模型包含多个属性。要确定要序列化的 Cookie,
您可以使用 model 属性的名称进行配置,以用于
渲染。如果模型仅包含一个属性,则使用该属性。HttpMessageWriterView
1.10. HTTP 缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存
围绕响应头和后续条件请求
标头,例如 和 . 建议私有(例如,浏览器)
和 public (例如 proxy) caches,了解如何缓存和重用响应。使用标头
要发出可能导致 304 (NOT_MODIFIED) 没有正文的条件请求,
如果内容未更改。 可以看作是
标题。Cache-Control
Last-Modified
ETag
Cache-Control
ETag
ETag
Last-Modified
本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。
1.10.1.CacheControl
CacheControl
支持
配置与标头相关的设置,并接受为参数
在许多地方:Cache-Control
虽然 RFC 7234 描述了所有可能的
directives 的 Response,该类型采用
面向用例的方法,侧重于常见场景,如下例所示:Cache-Control
CacheControl
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
1.10.2. 控制器
控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为需要先计算资源的 or 值,然后才能进行比较
针对条件请求标头。控制器可以将 和 设置添加到 ,如下例所示: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) 响应,如果比较
添加到条件请求标头中,表示内容未更改。否则,将 and 标头添加到响应中。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 | 继续进行请求处理。 |
有三种变体可用于根据值和/或值检查条件请求。对于 conditional 和 requests,您可以将响应设置为
304 (NOT_MODIFIED)。对于条件 、 、 和 ,您可以改为设置响应
设置为 412 (PRECONDITION_FAILED) 以防止并发修改。eTag
lastModified
GET
HEAD
POST
PUT
DELETE
1.11. WebFlux 配置
WebFlux Java 配置声明了处理
请求,并且它提供了一个 API 来
自定义配置。这意味着您不需要了解底层
由 Java 配置创建的 bean。但是,如果您想了解它们,
您可以在 Views them in 或 read more about them are
在 Special Bean Types 中。WebFluxConfigurationSupport
对于配置 API 中不可用的更高级自定义,您可以 通过高级配置模式获得对配置的完全控制。
1.11.1. 启用 WebFlux 配置
您可以在 Java 配置中使用 Comments,如下例所示:@EnableWebFlux
@Configuration
@EnableWebFlux
public class WebConfig {
}
@Configuration
@EnableWebFlux
class WebConfig
前面的示例注册了许多 Spring WebFlux 基础结构 bean 并适应依赖项 available on the classpath — 用于 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. 转换、格式化
默认情况下,会安装各种数字和日期类型的格式化程序,以及支持
用于自定义 via 和 on 字段。@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 在解析和格式化日期时会考虑请求 Locale 值。这适用于日期表示为带有 “input” 形式的字符串的表单 领域。但是,对于“日期”和“时间”表单字段,浏览器使用定义的固定格式 在 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 和有关何时
use 实现。FormattingConversionServiceFactoryBean FormatterRegistrar |
1.11.4. 验证
默认情况下,如果存在 Bean Validation
在 Classpath(例如,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 {
// ...
}
}
请注意,你也可以在本地注册 implementations,
如下例所示: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())
}
}
如果你需要在某个地方注入一个 Bean,创建一个 bean 并
将其标记为 ,以避免与 MVC 配置中声明的冲突。LocalValidatorFactoryBean @Primary |
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 的默认属性:
如果在 Classpath 中检测到以下众所周知的模块,它还会自动注册它们:
-
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) {
// ...
}
}
具有视图技术的快捷方式,Spring Framework 使用这些
集成。以下示例使用 FreeMarker(还需要配置
基础 FreeMarker 视图技术):ViewResolverRegistry
@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
}
}
支持 Content Negotiation 和呈现其他格式
通过视图分辨率(除了 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 集成的视图技术的更多信息,请参见 View Technologies。
1.11.8. 静态资源
此选项提供了一种从基于 Resource
的位置列表提供静态资源的便捷方法。
在下一个示例中,给定一个以 开头的请求,相对路径为
用于查找和提供相对于 Classpath 上的静态资源。资源
提供一年的未来到期时间,以确保最大限度地使用浏览器缓存
以及减少浏览器发出的 HTTP 请求。标头也是
已评估,如果存在,则返回状态代码。以下清单显示了
示例:/resources
/static
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))
}
}
另请参阅静态资源的 HTTP 缓存支持。
资源处理程序还支持一系列 ResourceResolver
实现和 ResourceTransformer
实现。
可用于创建用于处理优化资源的工具链。
您可以使用基于 MD5 哈希的版本控制资源 URL
根据内容、固定的应用程序版本或其他信息计算得出。A (MD5 哈希) 是一个不错的选择,但有一些值得注意的例外(例如
JavaScript 资源)。VersionResourceResolver
ContentVersionStrategy
以下示例显示了如何在 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("/**"))
}
}
可用于重写 URL 并应用解析程序和
转换器(例如,插入版本)。WebFlux 配置提供了一个,以便可以将其注入到其他配置中。ResourceUrlProvider
ResourceUrlProvider
与 Spring MVC 不同,目前在 WebFlux 中,没有办法透明地重写 static
资源 URL,因为没有可以使用非阻塞链的视图技术
的旋转转换器。当仅提供本地资源时,解决方法是直接使用(例如,通过自定义元素)和 block。ResourceUrlProvider
请注意,当同时使用(例如,Gzip、Brotli 编码)和 时,必须按该顺序注册它们,以确保基于内容的
版本始终基于未编码的文件进行可靠计算。EncodedResourceResolver
VersionedResourceResolver
对于 WebJar,版本化 URL (如)是推荐且最有效的使用方式。
相关资源位置使用 Spring Boot 开箱即用地配置(也可以配置
手动 ) 并且不需要添加依赖项。/webjars/jquery/1.2.0/jquery.min.js
ResourceHandlerRegistry
org.webjars:webjars-locator-core
无版本的 URL (如 )通过 类路径中存在库时自动注册的 来支持,但代价是
类路径扫描可能会减慢应用程序启动速度。解析器可以将 URL 重写为
包括 jar 的版本,还可以与没有版本的传入 URL 匹配 — 例如, from to 。/webjars/jquery/jquery.min.js
WebJarsResourceResolver
org.webjars:webjars-locator-core
/webjars/jquery/jquery.min.js
/webjars/jquery/1.2.0/jquery.min.js
基于 的 Java 配置提供了更多选项
进行精细控制,例如上次修改的行为和优化的资源解析。ResourceHandlerRegistry |
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 依赖于请求路径的解析表示,该请求调用用于访问解码的路径段值,并删除了分号内容
(即 path 或 matrix 变量)。这意味着,与 Spring MVC 不同,您无需指明
是否解码请求路径,是否删除
路径匹配目的。 Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,在 Spring MVC 中,我们 也建议远离 依赖它。 |
1.11.10. WebSocket服务
WebFlux Java 配置声明了一个 bean,它提供
支持调用 WebSocket 处理程序。这意味着剩下的所有工作都要做
处理 WebSocket 握手请求的 order 是将 映射到 URL
通过。WebSocketHandlerAdapter
WebSocketHandler
SimpleUrlHandlerMapping
在某些情况下,可能需要使用
提供了允许配置 WebSocket 服务器属性的服务。
例如:WebSocketHandlerAdapter
WebSocketService
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public WebSocketService getWebSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
1.11.11. 高级配置模式
@EnableWebFlux
导入:DelegatingWebFluxConfiguration
-
为 WebFlux 应用程序提供默认的 Spring 配置
-
检测并委托给 implementations 来自定义该配置。
WebFluxConfigurer
对于高级模式,您可以直接从 中删除和扩展,而不是实现 ,
如下例所示:@EnableWebFlux
DelegatingWebFluxConfiguration
WebFluxConfigurer
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {
// ...
}
您可以将现有方法保留在 中,但现在也可以覆盖 bean 声明
从基类中,并且仍然有任意数量的其他实现
类路径。WebConfig
WebMvcConfigurer
1.12. HTTP/2 协议
Reactor Netty、Tomcat、Jetty 和 Undertow 支持 HTTP/2。但是,有 与 Server 配置相关的注意事项。有关更多详细信息,请参阅 HTTP/2 wiki 页面。
2. 网页客户端
Spring WebFlux 包括一个用于执行 HTTP 请求的客户端。 具有
基于 Reactor 的功能性、流畅的 API,参见 反应式库,
它支持异步逻辑的声明性组合,而无需处理
threads 或 concurrency。它是完全非阻塞的,它支持流式处理,并依赖于
同样用于编码和
在服务器端解码请求和响应内容。WebClient
WebClient
需要一个 HTTP 客户端库来执行请求。有内置的
支持以下内容:
-
其他可以通过 插入。
ClientHttpConnector
2.1. 配置
创建 a 的最简单方法是通过 static 工厂方法之一:WebClient
-
WebClient.create()
-
WebClient.create(String baseUrl)
您还可以与更多选项一起使用:WebClient.builder()
-
uriBuilderFactory
:自定义以用作基本 URL。UriBuilderFactory
-
defaultUriVariables
:展开 URI 模板时使用的默认值。 -
defaultHeader
:每个请求的标头。 -
defaultCookie
:每个请求的 Cookie。 -
defaultRequest
:自定义每个请求。Consumer
-
filter
:每个请求的客户端筛选器。 -
exchangeStrategies
:HTTP 消息读取器/写入器自定义。 -
clientConnector
:HTTP 客户端库设置。
例如:
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()
构建后, a 是不可变的。但是,您可以克隆它并构建一个
修改副本如下: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. MaxInMemorySize (最大内存大小)
编解码器对 memory 以避免应用程序内存问题。默认情况下,这些设置为 256KB。 如果这还不够,您将收到以下错误:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
要更改默认编解码器的限制,请使用以下内容:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()
2.1.2. 反应堆 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()
资源
默认情况下,参与 中保存的全局 Reactor Netty 资源,包括事件循环线程和连接池。
这是推荐的模式,因为固定的共享资源是事件循环的首选
并发。在此模式下,全局资源将保持活动状态,直到进程退出。HttpClient
reactor.netty.http.HttpResources
如果服务器与进程定时,则通常不需要显式
关闭。但是,如果服务器可以在进程内启动或停止(例如,Spring MVC
应用程序部署为 WAR),你可以声明一个类型为 with(默认值)的 Spring 管理的 bean,以确保 Reactor
当 Spring 关闭时,netty 全局资源会关闭,
如下例所示:ReactorResourceFactory
globalResources=true
ApplicationContext
@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()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build();
要配置读取或写入超时:
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
.addHandlerLast(ReadTimeoutHandler(10))
.addHandlerLast(WriteTimeoutHandler(10))
}
// Create WebClient...
要为所有请求配置响应超时,请执行以下操作:
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
要为特定请求配置响应超时,请执行以下操作:
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest { httpRequest: ClientHttpRequest ->
val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
reactorRequest.responseTimeout(Duration.ofSeconds(2))
}
.retrieve()
.bodyToMono(String::class.java)
2.1.3. Jetty
以下示例显示如何自定义 Jetty 设置:HttpClient
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(JettyClientHttpConnector(httpClient))
.build();
默认情况下,创建自己的资源 (、 、 )、
它们将保持活动状态,直到进程退出或被调用。HttpClient
Executor
ByteBufferPool
Scheduler
stop()
您可以在 Jetty 客户端(和服务器)的多个实例之间共享资源,并且
确保在 Spring 关闭时关闭资源
声明 Spring 管理的 bean 类型为 ,如下例所示
显示:ApplicationContext
JettyResourceFactory
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory()); (1)
return WebClient.builder().clientConnector(connector).build(); (2)
}
1 | 将构造函数与资源工厂一起使用。JettyClientHttpConnector |
2 | 将连接器插入 。WebClient.Builder |
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further customizations...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)
return WebClient.builder().clientConnector(connector).build() (2)
}
1 | 将构造函数与资源工厂一起使用。JettyClientHttpConnector |
2 | 将连接器插入 。WebClient.Builder |
2.1.4. HttpComponents
以下示例显示如何自定义 Apache HttpComponents 设置:HttpClient
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()
2.2.retrieve()
该方法可用于声明如何提取响应。例如:retrieve()
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()
或者只获取正文:
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 响应会导致 ,包括
特定 HTTP 状态代码的子类。自定义错误的处理
responses,使用处理程序,如下所示:WebClientResponseException
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>()
2.3. 交换
and 方法(在 Kotlin 中为 and)
对于需要更多控制的更高级的情况非常有用,例如以不同的方式解码响应
取决于响应状态:exchangeToMono()
exchangeToFlux()
awaitExchange { }
exchangeToFlow { }
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Person>()
}
else {
throw response.createExceptionAndAwait()
}
}
使用上述内容时,在 returned 或 complete 之后,响应正文
被选中,如果未使用,则释放它以防止内存和连接泄漏。
因此,响应不能进一步在下游解码。这取决于提供的
函数来声明如何在需要时解码响应。Mono
Flux
2.4. 请求体
请求正文可以从 处理的任何异步类型进行编码。
like 或 Kotlin 协程,如下例所示:ReactiveAdapterRegistry
Mono
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>()
或者,如果你有实际值,你可以使用 shortcut 方法
如下例所示: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. 表单数据
要发送表单数据,您可以提供 a 作为正文。请注意,
content 会自动设置为 。以下示例演示如何使用:MultiValueMap<String, String>
application/x-www-form-urlencoded
FormHttpMessageWriter
MultiValueMap<String, String>
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()
您还可以使用 内联提供表单数据,如下例所示:BodyInserters
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()
2.4.2. Multipart 数据
要发送多部分数据,您需要提供其值为
表示部件内容的实例或表示内容的实例,以及
Headers 的 Headers 进行分配。 提供了一个方便的 API 来准备一个
multipart 请求。以下示例演示如何创建 :MultiValueMap<String, ?>
Object
HttpEntity
MultipartBodyBuilder
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", FileSystemResource("...logo.png"))
part("jsonPart", Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
在大多数情况下,您不必为每个部分指定 。内容
type 根据选择的序列化对象自动确定
或者,如果是 ,则基于文件扩展名。如有必要,您可以
通过重载的
builder 方法。Content-Type
HttpMessageWriter
Resource
MediaType
part
准备好 a 后,将其传递给 的最简单方法是
通过该方法,如下例所示: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
作为 的替代方法,您还可以提供多部分内容
inline-style,通过内置的 ,如下例所示:MultipartBodyBuilder
BodyInserters
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
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()
可以通过更改现有实例来添加或删除过滤器,从而
在不影响原始实例的新实例中。例如:WebClient
WebClient
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
WebClient
是围绕过滤器链的薄立面,后跟 .它提供了一个工作流来发出请求、编码到更高级别或从更高级别编码
level 对象,它有助于确保始终使用响应内容。
当 filter 以某种方式处理响应时,必须格外小心地始终使用
其内容或以其他方式将其传播到下游,这将确保
一样。下面是一个处理状态代码的筛选器,但确保
任何响应内容(无论是否预期)都会被释放:ExchangeFunction
WebClient
UNAUTHORIZED
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}
fun renewTokenFilter(): ExchangeFilterFunction? {
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
next.exchange(request!!).flatMap { response: ClientResponse ->
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return@flatMap response.releaseBody()
.then(renewToken())
.flatMap { token: String? ->
val newRequest = ClientRequest.from(request).build()
next.exchange(newRequest)
}
} else {
return@flatMap Mono.just(response)
}
}
}
}
2.6. 属性
您可以向请求添加属性。如果您想传递信息 通过过滤器链,并影响给定请求的过滤器行为。 例如:
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>()
请注意,您可以在 level 上全局配置一个 callback,以便将属性插入到所有请求中。
例如,可以在 Spring MVC 应用程序中使用它来填充
请求属性。defaultRequest
WebClient.Builder
ThreadLocal
2.7. 背景信息
属性提供了一种将信息传递给过滤器的便捷方法
chain,但它们只影响当前请求。如果要传递
传播到嵌套的其他请求,例如 via ,或在 之后执行,
例如,通过 ,则需要使用 Reactor 。flatMap
concatMap
Context
Reactor 需要在 Reactive 链的末尾填充,以便
应用于所有操作。例如:Context
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));
2.8. 同步使用
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()
}
但是,如果需要进行多个调用,则避免在每个调用上阻塞会更有效 response 中,而是等待组合的结果:
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())
}
以上只是一个例子。还有许多其他 pattern 和运算符可用于放置 一起进行许多远程调用(可能是一些嵌套的 相互依赖,直到最后都没有阻塞。
使用 或 ,你永远不必在 Spring MVC 或 Spring WebFlux 控制器中阻塞。
只需从 controller 方法返回结果响应式类型。同样的原则也适用于
Kotlin 协程和 Spring WebFlux 中,只需在
controller 方法 。 |
2.9. 测试
要测试使用 的代码,您可以使用模拟 Web 服务器,例如 OkHttp MockWebServer。查看示例
查看 Spring Framework 测试套件中的 WebClientIntegrationTests
或 OkHttp 存储库中的 static-server
示例。WebClient
3. 网络套接字
参考文档的这一部分涵盖了对反应式堆栈 WebSocket 的支持 消息。
3.1. WebSocket 简介
WebSocket 协议 RFC 6455 提供了标准化的 在 Client 端和 Server 之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与 HTTP 不同的 TCP 协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重复使用现有防火墙规则。
WebSocket 交互以使用 HTTP 标头的 HTTP 请求开始
进行升级,或者在本例中切换到 WebSocket 协议。以下示例
显示了这样的交互:Upgrade
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 |
支持 WebSocket 的服务器返回输出,而不是通常的 200 状态代码 类似于以下内容:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 | 协议切换 |
握手成功后,HTTP 升级请求的基础 TCP 套接字将保留 open 以继续发送和接收消息。
有关 WebSockets 工作原理的完整介绍超出了本文档的范围。 请参阅 RFC 6455、HTML5 的 WebSocket 章节或许多介绍中的任何一个 Web 上的教程。
请注意,如果 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 客户端和服务器可以协商使用更高级别的消息传递协议
(例如,STOMP),通过 HTTP 握手请求上的标头。
如果没有这些,他们需要提出自己的惯例。Sec-WebSocket-Protocol
3.1.2. 何时使用 WebSockets
WebSockets 可以使网页具有动态和交互性。但是,在许多情况下, Ajax 和 HTTP 流式处理或长轮询的组合可以提供简单且 有效的解决方案。
例如,新闻、邮件和社交源需要动态更新,但可能需要 每隔几分钟这样做一次完全可以。协作、游戏和金融应用程序 另一方面,需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如, 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 低延迟、高频率和高容量的组合造就了最好的 case 来使用 WebSocket。
另请记住,在 Internet 上,您无法控制的限制性代理
可能会排除 WebSocket 交互,因为它们未配置为传递 Headers,或者因为它们关闭了看起来空闲的长期连接。这
意味着将 WebSocket 用于防火墙内的内部应用程序是
比面向公众的应用程序更直接的决定。Upgrade
3.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写 client- 和 处理 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:
@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);
}
}
@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
}
如果使用 WebFlux 配置,则没有任何内容
进一步操作,或者如果不使用 WebFlux 配置,则需要声明 a,如下所示:WebSocketHandlerAdapter
@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@Configuration
class WebConfig {
// ...
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
3.2.2.WebSocketHandler
方法 takes 和 returns 指示应用程序对会话的处理何时完成。会话已处理
通过两个流,一个用于入站消息,一个用于出站消息。下表
介绍处理流的两种方法:handle
WebSocketHandler
WebSocketSession
Mono<Void>
WebSocketSession 方法 |
描述 |
---|---|
|
提供对入站消息流的访问,并在连接关闭时完成。 |
|
获取传出消息的源,写入消息,并返回一个
在源完成和写入完成时完成。 |
A 必须将入站和出站流组合成一个统一的流,并且
返回反映该流完成情况的 A。取决于应用
要求,则统一流将在以下情况下完成:WebSocketHandler
Mono<Void>
-
入站或出站消息流完成。
-
入站流完成(即连接关闭),而出站流是无限的。
-
在选定的点,通过 的方法。
close
WebSocketSession
当入站和出站消息流组合在一起时,无需 检查连接是否打开,因为 Reactive Streams 向 end activity 发出信号。 入站流接收到完成或错误信号,出站流 接收取消信号。
处理程序的最基本实现是处理入站流的处理程序。这 以下示例显示了此类实现:
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 | 返回 a ,该 a 在接收完成时完成。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 | 返回 a ,该 a 在接收完成时完成。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 | 返回未完成的 a,而我们继续接收。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 | 返回未完成的 a,而我们继续接收。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 | 加入流并返回 a ,该流在任一流结束时完成。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 | 加入流并返回 a ,该流在任一流结束时完成。Mono<Void> |
3.2.3.DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。Spring Core 的
该参考在 Data Buffers and Codecs 一节中有更多关于这方面的内容。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区是池化的和引用计数的,并且必须释放
当使用以避免内存泄漏时。
在 Netty 上运行时,应用程序必须使用 if
希望保留 input 数据缓冲区以确保它们不会被释放,并且
随后在消耗缓冲区时使用。DataBufferUtils.retain(dataBuffer)
DataBufferUtils.release(dataBuffer)
3.2.4. 握手
WebSocketHandlerAdapter
委托给 .默认情况下,这是一个实例
of 执行基本检查,它对 WebSocket 请求执行基本检查,而
然后用于正在使用的服务器。目前,有内置的
支持 Reactor Netty、Tomcat、Jetty 和 Undertow。WebSocketService
HandshakeWebSocketService
RequestUpgradeStrategy
HandshakeWebSocketService
公开一个属性,该属性允许
设置 a 以从 中提取属性并插入它们
转换为 的属性中。sessionAttributePredicate
Predicate<String>
WebSession
WebSocketSession
3.2.5. 服务器配置
的 for each server 公开特定于
底层 WebSocket 服务器引擎。使用 WebFlux Java 配置时,您可以自定义
如 WebFlux Config 的相应部分所示的属性,或者如果
不使用 WebFlux 配置,请使用以下命令:RequestUpgradeStrategy
@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 终端节点的访问的最简单方法是
实现并返回包含允许的来源、标头和其他详细信息的 a。如果你做不到
,您还可以将
通过 URL 模式指定 CORS 设置。如果同时指定了这两者,则使用 on 上的方法将它们组合在一起。WebSocketHandler
CorsConfigurationSource
CorsConfiguration
corsConfigurations
SimpleUrlHandler
combine
CorsConfiguration
3.2.7. 客户端
Spring WebFlux 提供了一个抽象,其中包含
Reactor Netty、Tomcat、Jetty、Undertow 和标准 Java(即 JSR-356)。WebSocketClient
Tomcat 客户端实际上是标准 Java 客户端的扩展,带有一些额外的
功能,以利用特定于 Tomcat 的
用于暂停接收背压消息的 API。WebSocketSession |
要启动 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)实施并需要停止和启动
在您可以使用它们之前。所有客户端都有与配置相关的构造函数选项
底层 WebSocket 客户端的 Web 节点。Lifecycle
4. 测试
该模块提供了 、 和 的模拟实现。
请参阅 Spring Web Reactive 以获取
对 mock 对象的讨论。spring-test
ServerHttpRequest
ServerHttpResponse
ServerWebExchange
WebTestClient
基于这些模拟请求和
response 对象,以支持在没有 HTTP 的情况下测试 WebFlux 应用程序
服务器。您也可以将 用于端到端集成测试。WebTestClient
5. RS锁
本节描述了 Spring 框架对 RSocket 协议的支持。
5.1. 概述
RSocket 是一种用于通过 TCP 进行多路复用、双工通信的应用程序协议, WebSocket 和其他字节流传输,使用以下交互之一 模型:
-
Request-Response
— 发送一条消息,然后接收一条消息。 -
Request-Stream
— 发送一条消息并接收回消息流。 -
Channel
— 双向发送消息流。 -
Fire-and-Forget
— 发送单向消息。
建立初始连接后,“client” 与 “server” 的区别将丢失,因为 双方都变得对称,并且每一方都可以发起上述交互之一。 这就是为什么在协议中将参与方称为 “requester” 和 “responder” 的原因 而上述交互称为 “请求流” 或简称为 “请求”。
以下是 RSocket 协议的主要功能和优势:
-
跨网络边界的 Reactive Streams 语义 — 用于流式请求,例如 和 、背压信号 在请求者和响应者之间移动,允许请求者在 源,从而减少对网络层拥塞控制的依赖,以及需求 用于网络级别或任何级别的缓冲。
Request-Stream
Channel
-
请求限制 — 此功能以 “Leasing” 命名,以 可以从每一端发送,以限制另一端允许的请求总数 在给定的时间内。租约会定期续订。
LEASE
-
会话恢复 — 这是为连接丢失而设计的,需要一些状态 待维护。状态管理对应用程序是透明的,并且运行良好 结合背压,可以在可能的情况下停止生产者并减少 所需的状态量。
-
大型消息的分片和重新汇编。
-
Keepalive (检测信号)。
RSocket 具有多种语言的实现。Java 库构建在 Project Reactor 之上, 和 Reactor Netty 用于运输。这意味着 应用程序中来自 Reactive Streams Publishers 的信号以透明方式传播 通过 RSocket 跨网络。
5.1.1. 协议
RSocket 的好处之一是它在 wire 上具有明确定义的行为,并且 易于阅读的规范以及一些协议扩展。因此它是 阅读规范是个好主意,独立于语言实现和更高级别 框架 API 的 API 中。本节提供了简洁的概述,以建立一些上下文。
连接
最初,客户端通过一些低级流传输(如
作为 TCP 或 WebSocket 进行设置,并向服务器发送一个帧,为
连接。SETUP
服务器可能会拒绝帧,但通常在发送帧后(对于 client)
和 received(对于服务器),双方都可以开始发出请求,除非指示使用 leasing 语义来限制请求的数量,在这种情况下
双方必须等待来自另一端的帧才能允许发出请求。SETUP
SETUP
LEASE
发出请求
建立连接后,双方都可以通过
帧 、 、 或 .每个
这些帧将一条消息从请求者传送到响应者。REQUEST_RESPONSE
REQUEST_STREAM
REQUEST_CHANNEL
REQUEST_FNF
然后,响应方可以返回包含响应消息的帧,在这种情况下
的请求者也可以发送具有更多请求的帧
消息。PAYLOAD
REQUEST_CHANNEL
PAYLOAD
当请求涉及 和 等消息流时,
响应方必须遵循来自请求方的需求信号。Demand 表示为
消息数。初始需求在 和 帧 中指定。后续需求通过帧发出信号。Request-Stream
Channel
REQUEST_STREAM
REQUEST_CHANNEL
REQUEST_N
每一方还可以通过框架发送元数据通知,这些通知不会
与任何单个请求有关,但与整个连接有关。METADATA_PUSH
消息格式
RSocket 消息包含数据和元数据。元数据可用于发送路由、
证券令牌等数据和元数据的格式可以不同。每个 Mime 类型
在框架中声明,并应用于给定连接上的所有请求。SETUP
虽然所有消息都可以包含元数据,但通常元数据(如路由)是按请求进行的
因此仅包含在请求的第一条消息中,即包含帧 、 、 或 。REQUEST_RESPONSE
REQUEST_STREAM
REQUEST_CHANNEL
REQUEST_FNF
协议扩展定义用于应用程序的常见元数据格式:
5.1.2. Java 实现
RSocket 的 Java 实现构建在 Project Reactor 之上。TCP 和 WebSocket 的传输方式是
基于 Reactor Netty 构建。作为反应式流
库,Reactor 简化了实现协议的工作。对于应用程序,它是
天生适合使用,带有声明式运算符和透明背面
压力支持。Flux
Mono
RSocket Java 中的 API 有意做到最小和基本。它侧重于协议 功能,并将应用程序编程模型(例如 RPC codegen 与其他模型)保留为 更高层次,独立关注。
主合约 io.rsocket.RSocket 对四种请求交互类型进行建模,表示
single message 中,消息流和实际的
message 中访问数据和元数据作为字节缓冲区。使用 Contract
对称。对于请求,应用程序被赋予一个 to perform
请求与。为了响应,应用程序实现了处理请求。Mono
Flux
io.rsocket.Payload
RSocket
RSocket
RSocket
这并不是一个详尽的介绍。在大多数情况下,Spring 应用程序 不必直接使用其 API。但是,观察或试验可能很重要 使用 RSocket 独立于 Spring。RSocket Java 存储库包含许多示例应用程序,这些应用程序 演示其 API 和协议功能。
5.1.3. Spring 支持
该模块包含以下内容:spring-messaging
-
RSocketRequester — 流畅的 API,用于通过数据和元数据编码/解码发出请求。
io.rsocket.RSocket
-
Annotated Responders — 带注释的处理程序方法 响应。
@MessageMapping
该模块包含和实现,例如 Jackson
CBOR/JSON 和 Protobuf 的 RSocket 应用程序可能需要。它还包含可插入以实现高效路由匹配的 。spring-web
Encoder
Decoder
PathPatternParser
Spring Boot 2.2 支持通过 TCP 或 WebSocket 建立 RSocket 服务器,包括
在 WebFlux 服务器中通过 WebSocket 公开 RSocket 的选项。还有 Client
支持和自动配置 和 .
有关更多详细信息,请参阅 Spring Boot 参考中的 RSocket 部分。RSocketRequester.Builder
RSocketStrategies
Spring Security 5.2 提供了 RSocket 支持。
Spring 集成 5.2 提供了入站和出站网关来与 RSocket 交互 客户端和服务器。有关更多详细信息,请参见 Spring 集成参考手册。
Spring Cloud 网关支持 RSocket 连接。
5.2. RSocketRequester
RSocketRequester
提供 Fluent API 来执行 RSocket 请求、接受和
返回 data 和 metadata 的对象,而不是低级数据缓冲区。可以使用
对称地,从 Client 端发出请求,以及从 Server 发出请求。
5.2.1. 客户端请求者
要在客户端获取 an 就是连接到一个服务器,它涉及
发送带有连接设置的 RSocket 帧。 提供
构建器,帮助准备 Include 连接
框架的设置。RSocketRequester
SETUP
RSocketRequester
io.rsocket.core.RSocketConnector
SETUP
这是使用默认设置进行连接的最基本方法:
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);
URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);
val requester = RSocketRequester.builder().tcp("localhost", 7000)
URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)
以上不会立即连接。发出请求时,共享连接为 透明地建立并使用。
连接设置
RSocketRequester.Builder
提供以下内容以自定义初始帧:SETUP
-
dataMimeType(MimeType)
— 设置连接上数据的 MIME 类型。 -
metadataMimeType(MimeType)
— 设置连接上元数据的 MIME 类型。 -
setupData(Object)
— 要包含在 .SETUP
-
setupRoute(String, Object…)
— 路由到包含在元数据中。SETUP
-
setupMetadata(Object, MimeType)
— 要包含在 .SETUP
对于数据,默认 mime 类型派生自第一个配置的 .为
metadata,则默认的 MIME 类型是 composite metadata,它允许多个
每个请求的元数据值和 MIME 类型对。通常,两者都不需要更改。Decoder
框架中的数据和元数据是可选的。在服务器端,可以使用@ConnectMapping方法处理
connection 和 frame 的内容。元数据可用于连接
级别安全性。SETUP
SETUP
策略
RSocketRequester.Builder
accepts 来配置请求者。
您需要使用它来提供编码器和解码器,用于数据的 (de) 序列化和
metadata 值。默认情况下,仅注册 for 、 和 的基本编解码器。添加 (Added) 可以访问更多
可以按如下方式注册:RSocketStrategies
spring-core
String
byte[]
ByteBuffer
spring-web
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
RSocketRequester requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000);
val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000)
RSocketStrategies
专为重复使用而设计。在某些情况下,例如客户端和服务器在
相同的应用程序,最好在 Spring 配置中声明它。
客户端响应方
RSocketRequester.Builder
可用于配置对来自
服务器。
您可以使用带注释的处理程序进行基于相同的客户端响应 在服务器上使用但以编程方式注册的基础结构,如下所示:
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();
SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.tcp("localhost", 7000);
1 | 如果存在,请使用 ,以提高效率
路由匹配。PathPatternRouteMatcher spring-web |
2 | 使用 and/or 方法从类创建响应方。@MessageMapping @ConnectMapping |
3 | 注册响应方。 |
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)
.tcp("localhost", 7000)
1 | 如果存在,请使用 ,以提高效率
路由匹配。PathPatternRouteMatcher spring-web |
2 | 使用 and/or 方法从类创建响应方。@MessageMapping @ConnectMapping |
3 | 注册响应方。 |
请注意,以上只是专为 client 的编程注册而设计的快捷方式
反应。对于客户端响应者处于 Spring 配置中的替代场景,
你仍然可以声明为 Spring bean,然后按如下方式应用:RSocketMessageHandler
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.tcp("localhost", 7000);
import org.springframework.beans.factory.getBean
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.tcp("localhost", 7000)
对于上述内容,您可能还需要在 中使用
切换到不同的策略来检测客户端响应者,例如基于自定义
注释(例如 VS 默认的 .这
在客户端和服务器或多个客户端位于同一
应用。setHandlerPredicate
RSocketMessageHandler
@RSocketClientResponder
@Controller
另请参阅 Annotated Responders ,以了解有关编程模型的更多信息。
高深
RSocketRequesterBuilder
提供回调以公开 keepalive 的进一步配置选项的基础
intervals、session resumption、interceptor 等。您可以配置选项
在该级别,如下所示:io.rsocket.core.RSocketConnector
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.tcp("localhost", 7000);
val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}
.tcp("localhost", 7000)
5.2.2. 服务器请求者
要从服务器向连接的客户端发出请求,只需获取 来自服务器的已连接客户端的 requester。
在 Annotated Responders 中,方法支持参数。使用它来访问连接的请求者。保留
请注意,方法本质上是 Frame 的处理程序,它
必须在请求开始之前处理。因此,请求在一开始就必须是
与处理分离。例如:@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 | 在 suspending 函数中执行处理。 |
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 | 声明预期的响应。 |
交互类型由输入的基数隐式确定,而
输出。上面的示例是一个 because 一个 value 被发送和一个 stream
of values 的值。在大多数情况下,您不需要考虑这个问题,只要
输入和输出的选择与 RSocket 交互类型以及 input 和
响应方所需的输出。无效组合的唯一示例是多对一。Request-Stream
该方法还接受任何 Reactive Streams ,包括 和 ,以及在 .对于多值(例如 which 会生成
相同类型的值,请考虑使用重载方法之一来避免
类型检查并查找每个元素:data(Object)
Publisher
Flux
Mono
ReactiveAdapterRegistry
Publisher
Flux
data
Encoder
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
该步骤是可选的。对于不发送数据的请求,请跳过它:data(Object)
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
.retrieveMono(AirportLocation.class);
import org.springframework.messaging.rsocket.retrieveAndAwait
val location = requester.route("find.radar.EWR")
.retrieveAndAwait<AirportLocation>()
如果使用复合元数据(默认),并且可以添加额外的元数据值
值由已注册的 .例如:Encoder
String securityToken = ... ;
ViewBox viewBox = ... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");
Flux<AirportLocation> locations = requester.route("locate.radars.within")
.metadata(securityToken, mimeType)
.data(viewBox)
.retrieveFlux(AirportLocation.class);
import org.springframework.messaging.rsocket.retrieveFlow
val requester: RSocketRequester = ...
val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")
val locations = requester.route("locate.radars.within")
.metadata(securityToken, mimeType)
.data(viewBox)
.retrieveFlow<AirportLocation>()
对于使用返回 .请注意,这仅表示消息已成功发送,而不表示消息已处理。Fire-and-Forget
send()
Mono<Void>
Mono
对于使用带有返回值的方法。Metadata-Push
sendMetadata()
Mono<Void>
5.3. 带注释的响应者
RSocket 响应程序可以作为 and 方法实现。 方法处理单个请求,而方法处理
连接级事件 (Setup 和 Metadata Push)。支持带注释的响应者
对称地,用于从服务器端响应和从客户端响应。@MessageMapping
@ConnectMapping
@MessageMapping
@ConnectMapping
5.3.1. 服务器响应器
要在服务器端使用带注释的响应者,请将 Spring 添加到您的 Spring
配置来检测带有 AND 方法的 bean:RSocketMessageHandler
@Controller
@MessageMapping
@ConnectMapping
@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))
.awaitSingle()
您需要设置元数据和数据所需的 和 实例
格式。您可能需要该模块来实现编解码器。Encoder
Decoder
spring-web
默认情况下,用于通过 匹配路由。
我们建议将 from for
高效的路由匹配。RSocket 路由可以是分层的,但不是 URL 路径。
默认情况下,两个路由匹配器都配置为使用 “.” 作为分隔符,并且没有 URL
解码方式与 HTTP URL 一样。SimpleRouteMatcher
AntPathMatcher
PathPatternRouteMatcher
spring-web
RSocketMessageHandler
可以通过以下方式进行配置,如果满足以下条件,则可能很有用
您需要在同一进程中在 Client 端和 Server 之间共享配置: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> {
// ...
}
}
上述方法响应具有
路由 “locate.radars.within”。它支持灵活的方法签名,并可选择
使用以下方法参数:@MessageMapping
方法参数 | 描述 |
---|---|
|
请求的有效负载。这可以是异步类型的具体值,如 或 。 注意:使用注释是可选的。不是简单类型的方法参数 并且不是任何其他受支持的参数,则假定为预期的有效负载。 |
|
请求者,用于向远程端发出请求。 |
|
根据映射模式中的变量从路由中提取的值,例如 . |
|
注册用于提取的元数据值,如 MetadataExtractor 中所述。 |
|
注册用于提取的所有元数据值,如 MetadataExtractor 中所述。 |
返回值应为一个或多个要序列化为响应的 Object
负载。这可以是异步类型,如 or、具体值或
或者一个无值异步类型,例如 .Mono
Flux
void
Mono<Void>
方法支持的 RSocket 交互类型是根据
input 的基数(即 参数)和输出,其中
cardinality 的含义如下:@MessageMapping
@Payload
基数 | 描述 |
---|---|
1 |
显式值或单值异步类型,如 . |
多 |
多值异步类型,如 . |
0 |
对于 input,这意味着该方法没有参数。 对于输出,这是 or 无值异步类型,例如 . |
下表显示了所有输入和输出基数组合以及相应的 交互类型:
输入基数 | 输出基数 | 交互类型 |
---|---|---|
0, 1 |
0 |
Fire-and-Forget, Request-Response |
0, 1 |
1 |
请求-响应 |
0, 1 |
多 |
请求流 |
多 |
0、1、多 |
请求通道 |
5.3.4. @ConnectMapping
@ConnectMapping
在 RSocket 连接开始时处理帧,并且
通过框架的任何后续元数据推送通知,即 在。SETUP
METADATA_PUSH
metadataPush(Payload)
io.rsocket.RSocket
@ConnectMapping
方法支持与 @MessageMapping 相同的参数,但基于来自 AND 帧的元数据和数据。 可以有一个模式来缩小处理范围
在元数据中具有路由的特定连接,或者如果未声明任何模式
则所有连接都匹配。SETUP
METADATA_PUSH
@ConnectMapping
@ConnectMapping
方法不能返回数据,必须用 OR 作为返回值声明。如果处理返回新的
connection,则连接将被拒绝。处理不得搁置以使
请求 。有关详细信息,请参阅 Server Requester 。void
Mono<Void>
RSocketRequester
5.4. 元数据提取器
响应方必须解释元数据。复合元数据允许独立 格式化的元数据值(例如,用于路由、安全、跟踪),每个值都有自己的 MIME 类型。应用程序需要一种方法来配置元数据 MIME 类型以支持,以及一种方法 以访问提取的值。
MetadataExtractor
是一个合约,用于获取序列化元数据并返回解码
名称-值对,然后可以像 Headers 一样按名称访问,例如 via 在带注释的处理程序方法中。@Header
DefaultMetadataExtractor
可以为其提供实例来解码元数据。出
该盒子内置了对 “message/x.rsocket.routing.v0” 的支持,它将其解码并保存在 “route” 键下。对于任何其他 mime 类型,您需要提供
a 并注册 MIME 类型,如下所示:Decoder
String
Decoder
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")
复合元数据可以很好地组合独立的元数据值。但是,
请求者可能不支持复合元数据,或者可能选择不使用它。为此,可能需要自定义逻辑将解码值映射到输出
地图。以下是将 JSON 用于元数据的示例:DefaultMetadataExtractor
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
依赖并在内部使用它来编写异步
logic 并提供 Reactive Streams 支持。通常, WebFlux API 返回或(因为这些是内部使用的)并宽容地接受任何 Reactive Streams 实现作为 Importing。使用 versus 很重要,因为
它有助于表达基数 — 例如,无论是单个还是多个异步
值,这对于做出决策可能是必不可少的(例如,当
编码或解码 HTTP 消息)。reactor-core
Flux
Mono
Publisher
Flux
Mono
对于带注释的控制器,WebFlux 透明地适应所选的响应式库
由应用程序。这是在 ReactiveAdapterRegistry
的帮助下完成的,它为反应式库和其他异步类型提供可插拔的支持。
该注册表内置了对 RxJava 3、Kotlin 协程和 SmallRye Mutiny 的支持。
但您也可以注册其他第三方适配器。
从 Spring Framework 5.3.11 开始,对 RxJava 1 和 2 的支持已被弃用,如下所示 RxJava 自己的 EOL 建议和针对 RxJava 3 的升级建议。 |
对于功能性 API(例如 Functional Endpoints、 等),常规的
WebFlux API 的规则适用 — 并作为返回值和 Reactive Streams 作为输入。当 ,无论是自定义的还是来自另一个响应式库的,
,则只能将其视为语义未知 (0..N) 的流。但是,如果
语义是已知的,你可以用 OR 代替它
传递原始的 .WebClient
Flux
Mono
Publisher
Publisher
Flux
Mono.from(Publisher)
Publisher
例如,给定 a 不是 ,则 Jackson JSON 消息编写器
需要多个值。如果媒体类型意味着无限流(例如, ),则会单独写入和刷新值。否则
值缓冲到列表中并呈现为 JSON 数组。Publisher
Mono
application/json+stream