Web 响应式
1. 春季 WebFlux
Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是 专为 Servlet API 和 Servlet 容器构建。响应式堆栈 Web 框架 Spring WebFlux,后来在 5.0 版本中添加。它是完全无阻塞的,支持反应流背压,并在以下服务器上运行 Netty、Undertow 和 Servlet 3.1+ 容器。
两个 Web 框架都镜像其源模块的名称
(spring-webmvc 和 spring-webflux)并并存在
Spring 框架。每个模块都是可选的。应用程序可以使用一个模块或另一个模块,
在某些情况下,两者兼而有之——例如,具有响应式WebClient
.
1.1. 概述
为什么要创建 Spring WebFlux?
部分答案是需要一个非阻塞 Web 堆栈来处理并发性
线程数少,以更少的硬件资源进行扩展。Servlet 3.1 确实提供了
用于非阻塞 I/O 的 API。但是,使用它会导致远离 Servlet API 的其余部分,
其中合约是同步的 (Filter
,Servlet
) 或阻塞 (getParameter
,getPart
).这就是新的通用 API 作为基础的动机
任何非阻塞运行时。这很重要,因为服务器(例如 Netty)是
在异步、非阻塞领域建立良好。
答案的另一部分是函数式编程。就像添加注释一样
在 Java 5 中创造了机会(例如带注释的 REST 控制器或单元测试),添加
的 lambda 表达式为 Java 中的函数式 API 创造了机会 8。
这对于非阻塞应用程序和延续式 API(如普及的那样)来说是一个福音
由CompletableFuture
和 ReactiveX),允许声明式
异步逻辑的组成。在编程模型级别,Java 8 启用了 Spring
WebFlux 提供功能性 Web 端点以及带注释的控制器。
1.1.1. 定义“响应式”
我们谈到了“非阻塞”和“功能性”,但响应式是什么意思?
术语“响应式”是指围绕对变化做出反应而构建的编程模型——对 I/O 事件做出反应的网络组件、对鼠标事件做出反应的 UI 控制器等等。 从这个意义上说,非阻塞是被动的,因为我们现在不是被阻塞,而是处于模式 在作完成或数据可用时对通知做出反应。
我们还 Spring 团队将另一个重要机制与“响应式”联系在一起 这就是非阻塞背压。在同步命令式代码中,阻止调用 作为一种自然形式的背压,迫使呼叫者等待。在非阻塞中 代码中,控制事件的速率变得很重要,这样快速生产者就不会 压倒了它的目的地。
Reactive Streams 是一个小规范(在 Java 9 中也采用) 这定义了异步组件与背压之间的交互。 例如,数据存储库(充当发布者) 可以生成 HTTP 服务器(充当订阅者)的数据 然后可以写入响应。Reactive Streams 的主要目的是让 订阅者控制发布者生成数据的速度或速度。
常见问题:如果发布商不能放慢速度怎么办? 反应流的目的只是建立机制和边界。 如果发布者无法减慢速度,则必须决定是缓冲、丢弃还是失败。 |
1.1.2. 响应式 API
反应流在互作性方面发挥着重要作用。图书馆对此感兴趣
和基础设施组件,但作为应用程序 API 不太有用,因为它太有用了
低级。应用程序需要更高级别、更丰富的功能 API 来
compose 异步逻辑 — 类似于 Java 8Stream
API,但不仅适用于集合。
这就是响应式库所扮演的角色。
Reactor 是首选的反应性库
Spring WebFlux 的 WebFlux。它提供了Mono
和Flux
API 类型
处理 0..1 (Mono
) 和 0..N (Flux
)通过一组丰富的运算符与
ReactiveX 运算符词汇表。
Reactor 是一个 Reactive Streams 库,因此,它的所有运算符都支持非阻塞背压。
Reactor 非常注重服务器端 Java。它是在密切合作下开发的
与Spring。
WebFlux 需要 Reactor 作为核心依赖项,但它可以与其他响应式互作
通过反应流的库。作为一般规则,WebFlux API 接受普通Publisher
作为输入,在内部将其适配为 Reactor 类型,使用它,并返回Flux
或Mono
作为输出。因此,您可以传递任何Publisher
作为输入,您可以应用
作,但您需要调整输出以与另一个响应式库一起使用。
只要可行(例如,带注释的控制器),WebFlux 就会透明地适应使用
RxJava 或其他响应式库。有关更多详细信息,请参阅响应式库。
除了响应式 API 之外,WebFlux 还可以与 Kotlin 中的协程 API 一起使用,从而提供更命令性的编程风格。 协程 API 将提供以下 Kotlin 代码示例。 |
1.1.3. 编程模型
这spring-web
模块包含作为 Spring WebFlux 基础的响应式基础,
包括 HTTP 抽象、支持的响应式流适配器
服务器、编解码器和核心WebHandler
应用程序接口可与
Servlet API,但具有非阻塞合约。
在此基础上,Spring WebFlux 提供了两种编程模型的选择:
1.1.4. 适用性
Spring MVC 还是 WebFlux?
这是一个很自然的问题,但却设置了一个不健全的二分法。实际上,两者 共同努力,扩大可用选项的范围。两者专为 彼此的连续性和一致性,它们并排可用,并且反馈 双方都受益。下图显示了两者的关系,它们是什么 有共同点,以及每个共同点支持的内容:

我们建议您考虑以下具体要点:
-
如果你有一个运行良好的 Spring MVC 应用程序,则无需更改。 命令式编程是编写、理解和调试代码的最简单方法。 您可以最大限度地选择库,因为从历史上看,大多数库都是阻塞的。
-
如果您已经在购买非阻塞 Web 堆栈,Spring WebFlux 提供相同的功能 执行模型与该领域的其他模型一样受益,并且还提供了服务器的选择 (Netty、Tomcat、Jetty、Undertow 和 Servlet 3.1+ 容器),可选择编程模型 (带注释的控制器和功能性 Web 端点),以及响应式库的选择 (Reactor、RxJava 或其他)。
-
如果您对与 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 应用程序,请尝试响应式
WebClient
. 您可以返回响应式类型(Reactor、RxJava 或其他) 直接来自 Spring MVC 控制器方法。每次调用的延迟越大,或者 呼叫之间的相互依赖性,好处就越显着。Spring MVC 控制器 也可以调用其他响应式组件。 -
如果您有一个庞大的团队,请记住向非阻塞转变的陡峭学习曲线, 函数式编程和声明式编程。无需全开关即可启动的实用方法 就是用响应式
WebClient
.除此之外,从小处着手并衡量收益。 我们预计,对于广泛的应用,这种转变是不必要的。如果你是 不确定要寻找什么好处,请从了解非阻塞 I/O 的工作原理开始 (例如,单线程Node.js上的并发性)及其效果。
1.1.5. 服务器
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(以及一般的非阻塞服务器)中,假设应用程序 不要阻塞。因此,非阻塞服务器使用小型、固定大小的线程池 (事件循环工作程序)来处理请求。
“缩放”和“线程数量少”听起来可能很矛盾,但永远不要阻止 当前线程(并依赖回调)意味着您不需要额外的线程,因为 没有要吸收的阻止呼叫。 |
如果您确实需要使用阻止库怎么办?Reactor 和 RxJava 都提供了publishOn
运算符以继续在不同的线程上进行处理。这意味着有一个
轻松逃生舱口。但是请记住,阻止 API 并不适合
这种并发模型。
在 Reactor 和 RxJava 中,你通过运算符声明逻辑。在运行时,响应式 管道是在不同阶段按顺序处理数据的地方形成的。一个主要优势 其中,它使应用程序不必保护可变状态,因为 该管道中的应用程序代码永远不会同时调用。
您应该期望在运行 Spring WebFlux 的服务器上看到哪些线程?
-
在“普通”Spring WebFlux 服务器上(例如,没有数据访问或其他可选的 dependencies),您可以期望服务器有一个线程,请求需要其他几个线程 处理(通常与 CPU 内核数一样多)。然而,Servlet 容器 可以从更多线程开始(例如,Tomcat 上的 10 个线程),以支持 servlet(阻塞)I/O 和 servlet 3.1(非阻塞)I/O 使用。
-
响应式
WebClient
以事件循环样式运行。所以你可以看到一个小的,固定的 与之相关的处理线程数(例如,reactor-http-nio-
与反应堆 Netty 连接器)。但是,如果 Reactor Netty 同时用于客户端和服务器,则 默认共享事件循环资源。 -
Reactor 和 RxJava 提供了线程池抽象,称为调度器,可与
publishOn
运算符,用于将处理切换到不同的线程池。 调度程序的名称建议特定的并发策略,例如,“parallel” (对于线程数量有限的 CPU 绑定工作)或“弹性”(用于 I/O 绑定工作 大量线程)。如果您看到此类线程,则表示某些代码正在使用 特定线程池Scheduler
策略。 -
数据访问库和其他第三方依赖项也可以创建和使用线程 他们自己的。
1.2. 响应式内核
这spring-web
模块包含以下对响应式 Web 的基础支持
应用:
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:HTTP 请求处理的基本合约 非阻塞 I/O 和反应流背压,以及用于 Reactor Netty 的适配器, Undertow、Tomcat、Jetty 和任何 Servlet 3.1+ 容器。
-
WebHandler
应用程序接口:稍高级别的通用 Web API 请求处理,在此基础上有具体的编程模型,例如 Commentted 构建控制器和功能端点。
-
-
对于客户端,有一个基本的
ClientHttpConnector
执行 HTTP 的合约 具有非阻塞 I/O 和响应式流背压的请求,以及 Reactor Netty、响应式 Jetty HttpClient 和 Apache HttpComponents 的适配器。 应用程序中使用的更高级别的 WebClient 建立在这个基本契约的基础上。 -
对于客户端和服务器,用于序列化的编解码器和 HTTP 请求和响应内容的反序列化。
1.2.1.HttpHandler
HttpHandler 是一个简单的合约,具有处理请求和响应的单一方法。是的 故意最小化,其主要和唯一目的是成为最小抽象 通过不同的 HTTP 服务器 API。
下表介绍了支持的服务器 API:
服务器名称 | 使用的服务器 API | 响应式流支持 |
---|---|---|
网 |
Netty API |
|
Undertow |
暗流 API |
spring-web:Undertow 到 Reactive Streams 桥接器 |
Tomcat |
Servlet 3.1 非阻塞 I/O;用于读写 ByteBuffers 的 Tomcat API 与 byte[] |
spring-web:Servlet 3.1 非阻塞 I/O 到 Reactive Streams 桥接器 |
Jetty |
Servlet 3.1 非阻塞 I/O;用于编写 ByteBuffers 的 Jetty API 与 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 | 神器名称 |
---|---|---|
反应器网 |
io.projectreactor.netty |
反应器网 |
Undertow |
io.undertow |
暗拖核心 |
Tomcat |
org.apache.tomcat.embed |
tomcat-嵌入核心 |
Jetty |
org.eclipse.Jetty |
jetty-server, jetty-servlet |
下面的代码片段显示了使用HttpHandler
适配器与每个服务器 API 一起使用:
反应器网
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
Tomcat
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
Jetty
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
Servlet 3.1+ 容器
要作为 WAR 部署到任何 Servlet 3.1+ 容器,您可以扩展并包含AbstractReactiveWebInitializer
在战争中。该类包装了HttpHandler
跟ServletHttpHandlerAdapter
和寄存器
作为Servlet
.
1.2.2.WebHandler
应用程序接口
这org.springframework.web.server
包构建在HttpHandler
合同
提供通用 Web API,用于通过多个链处理请求WebExceptionHandler
倍数WebFilter
,并且单个WebHandler
元件。链条可以
与WebHttpHandlerBuilder
只需指向弹簧ApplicationContext
其中组件是自动检测的,和/或通过注册组件
与架构商。
而HttpHandler
有一个简单的目标,即抽象使用不同的 HTTP 服务器,WebHandler
API 旨在提供 Web 应用程序中常用的更广泛的功能集
如:
-
具有属性的用户会话。
-
请求属性。
-
解决
Locale
或Principal
对于请求。 -
访问解析和缓存的表单数据。
-
多部分数据的抽象。
-
以及更多..
特殊Beans
下表列出了以下组件:WebHttpHandlerBuilder
可以在Spring ApplicationContext 中自动检测,或者可以直接向其注册:
豆子名称 | Beans | 计数 | 描述 |
---|---|---|---|
<任何> |
|
0..N |
提供对来自 |
<任何> |
|
0..N |
将拦截样式逻辑应用于过滤器链的其余部分之前和之后,以及目标 |
|
|
1 |
请求的处理程序。 |
|
|
0..1 |
经理 |
|
|
0..1 |
用于访问 |
|
|
0..1 |
的解析器 |
|
|
0..1 |
用于处理转发的类型标头,通过提取和删除它们或仅删除它们。 默认情况下不使用。 |
表单数据
ServerWebExchange
公开了以下访问表单数据的方法:
Mono<MultiValueMap<String, String>> getFormData();
suspend fun getFormData(): MultiValueMap<String, String>
这DefaultServerWebExchange
使用配置的HttpMessageReader
解析表单数据
(application/x-www-form-urlencoded
) 转换为MultiValueMap
.默认情况下,FormHttpMessageReader
配置为ServerCodecConfigurer
豆
(请参阅 Web 处理程序 API)。
多部分数据
ServerWebExchange
公开了以下用于访问多部分数据的方法:
Mono<MultiValueMap<String, Part>> getMultipartData();
suspend fun getMultipartData(): MultiValueMap<String, Part>
这DefaultServerWebExchange
使用配置的HttpMessageReader<MultiValueMap<String, Part>>
解析multipart/form-data
内容
变成一个MultiValueMap
.
默认情况下,这是DefaultPartHttpMessageReader
,没有任何第三方
依赖。
或者,SynchronossPartHttpMessageReader
可以使用,它基于 Synchronoss NIO Multipart 库。
两者都是通过ServerCodecConfigurer
豆
(请参阅 Web 处理程序 API)。
要以流式方式解析多部分数据,您可以使用Flux<Part>
从HttpMessageReader<Part>
相反。例如,在带注释的控制器中,使用@RequestPart
意味 着Map
-like 按名称访问单个部件,因此需要
完整解析多部分数据。相比之下,您可以使用@RequestBody
解码
内容设置为Flux<Part>
而不收集到MultiValueMap
.
请求头转发
当请求通过代理(例如负载均衡器)时,主机、端口和 方案可能会改变。从客户端的角度来看,这使得创建指向正确链接成为一项挑战 主机、端口和方案。
RFC 7239 定义了Forwarded
HTTP 标头
代理可以使用该信息来提供有关原始请求的信息。还有其他的
非标准标头,包括X-Forwarded-Host
,X-Forwarded-Port
,X-Forwarded-Proto
,X-Forwarded-Ssl
和X-Forwarded-Prefix
.
ForwardedHeaderTransformer
是修改主机、端口和方案的组件
请求,然后删除这些标头。如果您声明
它作为一个豆子,名字forwardedHeaderTransformer
,它将被检测和使用。
请求头转发存在安全注意事项,因为应用程序无法知道
如果标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么
应配置信任边界处的代理,以删除传入的不受信任的转发流量
从外面看。您还可以配置ForwardedHeaderTransformer
跟removeOnly=true
,在这种情况下,它会删除但不使用标头。
在 5.1 中ForwardedHeaderFilter 已被弃用并被ForwardedHeaderTransformer 因此,请求头转发可以更早地处理,在
交换被创建。如果仍然配置了过滤器,则它将从
过滤器,以及ForwardedHeaderTransformer 而是使用。 |
1.2.3. 过滤器
在WebHandler
应用程序接口,您可以使用WebFilter
应用拦截样式
过滤器和目标处理链其余部分之前和之后的逻辑WebHandler
.使用 WebFlux Config 时,注册WebFilter
就这么简单
将其声明为 Spring bean 并(可选)通过使用@Order
上
bean 声明或通过实现Ordered
.
CORS
Spring WebFlux 通过对 CORS 配置的注释提供细粒度的支持
控制器。但是,当您将其与 Spring Security 一起使用时,我们建议依赖内置的CorsFilter
,必须在 Spring Security 的过滤器链之前订购。
请参阅有关 CORS 和CORSWebFilter
了解更多详情。
1.2.4. 例外
在WebHandler
应用程序接口,您可以使用WebExceptionHandler
处理
链中的例外WebFilter
实例和目标WebHandler
.使用 WebFlux Config 时,注册WebExceptionHandler
就像将其声明为
Spring bean 和(可选)通过使用@Order
在 bean 声明上或
通过实现Ordered
.
下表描述了可用的WebExceptionHandler
实现:
异常处理程序 | 描述 |
---|---|
|
提供对类型 |
|
扩展 此处理程序在 WebFlux 配置中声明。 |
1.2.5. 编解码器
这spring-web
和spring-core
模块支持序列化和
通过非阻塞 I/O 将字节内容反序列化到更高级别对象或从更高级别对象反序列化
反应流背压。下面介绍了此支持:
-
HttpMessageReader
和HttpMessageWriter
是合同 对 HTTP 消息内容进行编码和解码。 -
一
Encoder
可以用EncoderHttpMessageWriter
使其适应在 Web 中使用 应用程序,而Decoder
可以用DecoderHttpMessageReader
. -
DataBuffer
摘要不同 字节缓冲区表示(例如 NettyByteBuf
,java.nio.ByteBuffer
等)并且是 所有编解码器的工作原理。请参阅 数据缓冲区和编解码器 有关此主题的更多信息,请参阅“Spring Core”部分。
这spring-core
模块提供byte[]
,ByteBuffer
,DataBuffer
,Resource
和String
编码器和解码器实现。这spring-web
模块提供 Jackson
JSON、Jackson Smile、JAXB2、协议缓冲区和其他编码器和解码器以及
表单数据、多部分内容、
服务器发送的事件等。
ClientCodecConfigurer
和ServerCodecConfigurer
通常用于配置和自定义要在应用程序中使用的编解码器。请参阅有关配置 HTTP 消息编解码器的部分。
Jackson JSON
JSON 和二进制 JSON(微笑)是 当 Jackson 库存在时,两者都受支持。
这Jackson2Decoder
工作原理如下:
-
Jackson 的异步、非阻塞解析器用于聚合字节块流 到
TokenBuffer
的每个代表一个 JSON 对象。 -
每
TokenBuffer
传给Jackson的ObjectMapper
以创建更高级别的对象。 -
当解码到单值发布者(例如
Mono
),有一个TokenBuffer
. -
解码到多值发布者时(例如
Flux
),每个TokenBuffer
被传递给 这ObjectMapper
一旦为完全格式的对象接收到足够的字节。 这 输入内容可以是 JSON 数组,也可以是任何以行分隔的 JSON 格式,例如 NDJSON、JSON 行或 JSON 文本序列。
这Jackson2Encoder
工作原理如下:
-
对于单一价值发布商(例如
Mono
),只需通过ObjectMapper
. -
对于具有
application/json
,默认情况下使用Flux#collectToList()
,然后序列化生成的集合。 -
对于具有流媒体类型(如
application/x-ndjson
或application/stream+x-jackson-smile
、编码、写入和使用以行分隔的 JSON 格式单独刷新每个值。 其他 流媒体类型可以注册到编码器。 -
对于 SSE,
Jackson2Encoder
每个事件调用并刷新输出以确保立即交付。
默认情况下,两者 |
表单数据
FormHttpMessageReader
和FormHttpMessageWriter
支持解码和编码application/x-www-form-urlencoded
内容。
在经常需要从多个地方访问表单内容的服务器端,ServerWebExchange
提供专用的getFormData()
解析内容的方法 通过FormHttpMessageReader
然后缓存结果以供重复访问。请参阅 表单数据 中的WebHandler
应用程序接口部分。
一次getFormData()
原始原始内容,则无法再从请求正文中读取原始内容。因此,应用程序应通过ServerWebExchange
一致地访问缓存的表单数据,而不是从原始请求正文中读取。
多部分
MultipartHttpMessageReader
和MultipartHttpMessageWriter
支持解码和对“multipart/form-data”内容进行编码。 挨次MultipartHttpMessageReader
委托给 另一个HttpMessageReader
用于实际解析为Flux<Part>
然后简单地将零件收集到MultiValueMap
.
默认情况下,DefaultPartHttpMessageReader
,但这可以通过ServerCodecConfigurer
.
有关DefaultPartHttpMessageReader
,请参阅javadoc 的DefaultPartHttpMessageReader
.
在服务器端,可能需要从多个
地方ServerWebExchange
提供专用的getMultipartData()
解析的方法
内容通过MultipartHttpMessageReader
然后缓存结果以供重复访问。
请参阅WebHandler
应用程序接口部分。
一次getMultipartData()
使用时,无法再从
请求正文。因此,应用程序必须始终使用getMultipartData()
用于重复、类似地图的部件访问,或以其他方式依赖SynchronossPartHttpMessageReader
一次性访问Flux<Part>
.
限制
Decoder
和HttpMessageReader
缓冲部分或全部输入的实现
stream 可以配置对内存中缓冲的最大字节数进行限制。
在某些情况下,缓冲是因为输入被聚合并表示为单个
object — 例如,具有@RequestBody byte[]
,x-www-form-urlencoded
数据,依此类推。缓冲也可能发生在流式处理中,当
拆分输入流 - 例如,分隔文本、JSON 对象流和
依此类推。对于这些流式处理情况,限制适用于关联的字节数
在流中有一个对象。
要配置缓冲区大小,您可以检查给定的Decoder
或HttpMessageReader
公开一个maxInMemorySize
属性,如果是这样,Javadoc 将包含有关默认值的详细信息
值。在服务器端,ServerCodecConfigurer
提供从哪里到
设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,限制
所有编解码器都可以在 WebClient.Builder 中更改。
对于多部分解析maxInMemorySize
属性限制
非文件部件的大小。对于文件部件,它确定部件的阈值
写入磁盘。对于写入磁盘的文件部分,还有一个额外的maxDiskUsagePerPart
属性来限制每个部分的磁盘空间量。还有
一个maxParts
属性来限制多部分请求中的部分总数。
要在 WebFlux 中配置所有三个,您需要提供一个预配置的实例MultipartHttpMessageReader
自ServerCodecConfigurer
.
流
流式传输到 HTTP 响应时(例如text/event-stream
,application/x-ndjson
),定期发送数据非常重要,以便
尽早可靠地检测断开连接的客户端。这样的发送可能是
仅注释、空 SSE 事件或任何其他“无作”数据,这些数据将有效地充当
心跳。
DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。的 Spring Core 部分
此参考在数据缓冲区和编解码器部分中有更多相关内容。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区被池化并对引用进行计数,并且必须释放
使用时以避免内存泄漏。
WebFlux 应用程序通常不需要关注此类问题,除非它们 直接使用或生成数据缓冲区,而不是依赖编解码器进行转换 以及来自更高级别的对象,或者除非他们选择创建自定义编解码器。对于这样的 情况请查看数据缓冲区和编解码器中的信息, 尤其是关于使用 DataBuffer 的部分。
1.2.6. 日志记录
DEBUG
Spring WebFlux 中的级别日志记录被设计为紧凑、最小和
人性化。它侧重于对
与其他仅在调试特定问题时有用的其他问题相比。
TRACE
关卡日志记录通常遵循与DEBUG
(例如,还有不应是消防水带),但可用于调试任何问题。此外,一些日志消息可能会显示不同级别的详细信息TRACE
与DEBUG
.
良好的日志记录来自使用日志的经验。如果您发现任何不符合既定目标的内容,请告诉我们。
日志 ID
在 WebFlux 中,单个请求可以在多个线程上运行,并且线程 ID 对于关联属于特定请求的日志消息没有用处。这就是为什么 默认情况下,WebFlux 日志消息以特定于请求的 ID 为前缀。
在服务器端,日志 ID 存储在ServerWebExchange
属性
(LOG_ID_ATTRIBUTE
),
而基于该 ID 的完整格式化前缀可从ServerWebExchange#getLogPrefix()
.在WebClient
side,日志 ID 存储在ClientRequest
属性
(LOG_ID_ATTRIBUTE
)
,而完全格式化的前缀可从ClientRequest#logPrefix()
.
敏感数据
DEBUG
和TRACE
日志记录可以记录敏感信息。这就是为什么 form 参数和
默认情况下,标头是屏蔽的,您必须显式启用其完全日志记录。
以下示例显示了如何对服务器端请求执行此作:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}
以下示例演示如何对客户端请求执行此作:
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()
自定义编解码器
应用程序可以注册自定义编解码器以支持其他媒体类型, 或默认编解码器不支持的特定行为。
以下示例演示如何对客户端请求执行此作:
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
1.3.DispatcherHandler
Spring WebFlux 与 Spring MVC 类似,是围绕前端控制器模式设计的,
其中中心WebHandler
这DispatcherHandler
,为
请求处理,而实际工作由可配置的委托组件执行。
该模型灵活并支持多种工作流程。
DispatcherHandler
从 Spring 配置中发现它需要的委托组件。
它还被设计成 Spring bean 本身和工具ApplicationContextAware
以访问它运行的上下文。如果DispatcherHandler
用 bean 声明
名称webHandler
,反过来,它又被发现WebHttpHandlerBuilder
,
它将请求处理链放在一起,如WebHandler
应用程序接口.
WebFlux 应用程序中的 Spring 配置通常包含:
-
DispatcherHandler
与 bean 名称webHandler
-
WebFilter
和WebExceptionHandler
豆 -
别人
配置被赋予WebHttpHandlerBuilder
构建加工链,
如以下示例所示:
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
由此产生的HttpHandler
已准备好与服务器适配器一起使用。
1.3.1. 特殊Beans类型
这DispatcherHandler
委托给特殊 bean 来处理请求并渲染
适当的回应。我们所说的“特殊豆”是指 Spring 管理的Object
实例
实现 WebFlux 框架合约。这些通常带有内置合同,但是
您可以自定义它们的属性、扩展它们或替换它们。
下表列出了DispatcherHandler
.请注意
在较低级别还检测到一些其他 Bean(请参阅 Web 处理程序 API 中的特殊 Bean 类型)。
Beans | 解释 |
---|---|
|
将请求映射到处理程序。映射基于一些标准,详细信息
差异为 主要 |
|
帮助 |
|
处理处理程序调用的结果并完成响应。 请参阅结果处理。 |
1.3.2. WebFlux 配置
应用程序可以声明基础架构 Bean(列在 Web 处理程序 API 和DispatcherHandler
)来处理请求。但是,在大多数情况下,WebFlux Config 是最好的起点。它声明了required bean,并提供了更高级别的配置回调 API 来自定义它。
Spring Boot 依靠 WebFlux 配置来配置 Spring WebFlux,并且还提供许多额外的方便选项。 |
1.3.3. 处理
DispatcherHandler
按如下方式处理请求:
-
每
HandlerMapping
被要求查找匹配的处理程序,并使用第一个匹配项。 -
如果找到处理程序,则通过适当的
HandlerAdapter
哪 将执行的返回值公开为HandlerResult
. -
这
HandlerResult
被给予适当的HandlerResultHandler
完成通过直接写入响应或使用视图进行渲染来处理。
1.3.4. 结果处理
调用处理程序的返回值,通过HandlerAdapter
,被包装作为HandlerResult
,以及一些额外的上下文,并传递给第一个HandlerResultHandler
声称支持它。下表显示了可用的HandlerResultHandler
实现,所有这些都在 WebFlux Config 中声明:
结果处理程序类型 | 返回值 | 默认顺序 |
---|---|---|
|
|
0 |
|
|
0 |
|
处理来自 |
100 |
|
另请参见视图分辨率。 |
|
1.3.5. 例外
这HandlerResult
从HandlerAdapter
可以公开错误函数
基于某些特定于处理程序的机制进行处理。如果出现以下情况,则调用此错误函数:
-
处理程序(例如,
@Controller
) 调用失败。 -
通过
HandlerResultHandler
失败。
错误函数可以更改响应(例如,更改为错误状态),只要错误 信号发生在从处理程序返回的响应式类型生成任何数据项之前。
就是这样@ExceptionHandler
方法@Controller
支持类。
相比之下,Spring MVC 中对相同内容的支持是建立在HandlerExceptionResolver
.
这通常无关紧要。但是,请记住,在 WebFlux 中,您不能使用@ControllerAdvice
处理在选择处理程序之前发生的异常。
1.3.6. 视图分辨率
视图分辨率允许渲染到具有 HTML 模板的浏览器和没有 HTML 模板的模型
将您绑定到特定的视图技术。在 Spring WebFlux 中,视图分辨率为
通过专用的 HandlerResultHandler 支持,该 HandlerResultHandler 使用ViewResolver
实例将 String(表示逻辑视图名称)映射到View
实例。这View
然后用于呈现响应。
处理
这HandlerResult
传递给ViewResolutionResultHandler
包含返回值
从处理程序和包含请求期间添加的属性的模型
处理。返回值按以下方式之一进行处理:
-
String
,CharSequence
:要解析为View
通过 已配置的列表ViewResolver
实现。 -
void
:根据请求路径选择默认视图名称,减去前导和 尾随斜杠,并将其解析为View
.当视图名称 未提供(例如,返回 model 属性)或异步返回值 (例如,Mono
已完成空)。 -
渲染:API 查看分辨率方案。通过代码补全探索 IDE 中的选项。
-
Model
,Map
:要为请求添加到模型中的额外模型属性。 -
任何其他:任何其他返回值(简单类型除外,由 BeanUtils#isSimpleProperty 确定)被视为要添加到模型中的模型属性。属性名称派生使用约定从类名,除非处理程序方法
@ModelAttribute
注释存在。
该模型可以包含异步响应式类型(例如,来自 Reactor 或 RxJava)。 事先 渲染,AbstractView
将此类模型属性解析为具体值并更新模型。单值响应式类型解析为单个值或无值(如果为空),而多值响应式类型(例如Flux<T>
) 是
收集并解析为List<T>
.
要配置视图分辨率,只需添加一个ViewResolutionResultHandler
豆
到你的 Spring 配置。WebFlux Config 提供了一个
用于视图解析的专用配置 API。
有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术。
重 定向
特别的redirect:
视图名称中的前缀允许您执行重定向。这UrlBasedViewResolver
(和子类)将此识别为指令,即
需要重定向。视图名称的其余部分是重定向 URL。
净效果与控制器返回RedirectView
或Rendering.redirectTo("abc").build()
,但现在控制器本身可以
根据逻辑视图名称进行作。视图名称,例如redirect:/some/resource
是相对于当前应用程序的,而视图名称(例如redirect:https://example.com/arbitrary/path
重定向到绝对 URL。
内容协商
ViewResolutionResultHandler
支持内容协商。它比较请求
媒体类型,其中包含每个所选媒体支持的媒体类型View
.第一个View
使用支持请求的媒体类型 () 。
为了支持 JSON 和 XML 等媒体类型,Spring WebFlux 提供了HttpMessageWriterView
,这是一个特殊的View
通过 HttpMessageWriter 呈现。通常,您会将这些配置为默认值
通过 WebFlux 配置进行视图。默认视图为
如果它们与请求的媒体类型匹配,则始终选择并使用。
1.4. 带注释的控制器
Spring WebFlux 提供了一个基于注释的编程模型,其中@Controller
和@RestController
组件使用注释来表达请求映射、请求输入、
处理异常等。带注释的控制器具有灵活的方法签名和
不必扩展基类,也不必实现特定的接口。
以下列表显示了一个基本示例:
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
@RestController
class HelloController {
@GetMapping("/hello")
fun handle() = "Hello WebFlux"
}
在前面的示例中,该方法返回一个String
写入响应正文。
1.4.1.@Controller
您可以使用标准 Spring Bean 定义来定义控制器 Bean。 这@Controller
stereotype 允许自动检测,并与 Spring 通用支持用于检测@Component
类路径中的类和自动注册 Bean 定义。它还充当带注释的类的构造型,表明它作为Web 组件的角色。
启用此类自动检测@Controller
bean,您可以将组件扫描添加到您的 Java 配置中,如下例所示:
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {
// ...
}
1 | 扫描org.example.web 包。 |
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {
// ...
}
1 | 扫描org.example.web 包。 |
@RestController
是一个组合的注释,它是本身用元注释@Controller
和@ResponseBody
,表示其每个方法都继承类型级@ResponseBody
注释,因此写入直接到响应正文,而不是使用 HTML 模板进行视图解析和渲染。
1.4.2. 请求映射
这@RequestMapping
注释用于将请求映射到控制器方法。它有通过 URL、HTTP 方法、请求参数、标头和媒体匹配的各种属性 类型。 您可以在类级别使用它来表达共享映射,也可以在方法级别将其缩小到特定的端点映射。
还有特定于 HTTP 方法的快捷方式变体@RequestMapping
:
-
@GetMapping
-
@PostMapping
-
@PutMapping
-
@DeleteMapping
-
@PatchMapping
前面的注释是提供的自定义注释
因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是
用@RequestMapping
,默认情况下,它与所有 HTTP 方法匹配。同时,一个@RequestMapping
在类级别仍然需要来表达共享映射。
以下示例使用类型和方法级别映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式
您可以使用 glob 模式和通配符来映射请求:
模式 | 描述 | 示例 |
---|---|---|
|
匹配一个字符 |
|
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径结束 |
|
|
匹配路径段并将其捕获为名为“name”的变量 |
|
|
匹配正则表达式 |
|
|
匹配零个或多个路径段,直到路径末尾,并将其捕获为名为“path”的变量 |
|
捕获的 URI 变量可以通过@PathVariable
,如以下示例所示:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
您可以在类和方法级别声明 URI 变量,如以下示例所示:
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {
@GetMapping("/pets/{petId}") (2)
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
1 | 类级 URI 映射。 |
2 | 方法级 URI 映射。 |
@Controller
@RequestMapping("/owners/{ownerId}") (1)
class OwnerController {
@GetMapping("/pets/{petId}") (2)
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
1 | 类级 URI 映射。 |
2 | 方法级 URI 映射。 |
URI 变量会自动转换为适当的类型或TypeMismatchException
被提高。简单类型 (int
,long
,Date
,依此类推)默认支持
寄存器支持任何其他数据类型。
请参阅类型转换和DataBinder
.
URI 变量可以显式命名(例如,@PathVariable("customId")
),但你可以
如果名称相同并且您通过调试编译代码,请省略该详细信息
信息或使用-parameters
Java 8 上的编译器标志。
语法{*varName}
声明一个与零个或多个剩余路径匹配的 URI 变量
段。例如/resources/{*path}
匹配下的所有文件/resources/
和"path"
变量捕获/resources
.
语法{varName:regex}
声明一个 URI 变量,其正则表达式具有
语法:{varName:regex}
.例如,给定 URL 为/spring-web-3.0.5.jar
,则采用以下方法
提取名称、版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式也可以嵌入${…}
在启动时解析的占位符
通过PropertySourcesPlaceholderConfigurer
针对本地、系统、环境和
其他属性来源。例如,您可以使用它来参数化基于
一些外部配置。
Spring WebFlux 使用PathPattern 和PathPatternParser 用于 URI 路径匹配支持。
这两个类都位于spring-web 并且专门设计用于 HTTP URL
Web 应用程序中的路径,其中运行时匹配大量 URI 路径模式。 |
Spring WebFlux 不支持后缀模式匹配——与 Spring MVC 不同,其中
映射,例如/person
也匹配为/person.*
.对于基于 URL 的内容
协商,如果需要,我们建议使用查询参数,这样更简单,更多
显式的,并且不易受到基于 URL 路径的漏洞的攻击。
模式比较
当多个模式与 URL 匹配时,必须比较它们才能找到最佳匹配。这已经完成了
跟PathPattern.SPECIFICITY_COMPARATOR
,它会查找更具体的模式。
对于每种模式,都会根据 URI 变量和通配符的数量计算一个分数, 其中 URI 变量的得分低于通配符。总分较低的模式 获胜。如果两个模式具有相同的分数,则选择较长的模式。
包罗万象的模式(例如,**
{*varName}
) 被排除在评分之外,并且始终是
而是最后排序。如果两个模式都是包罗万象的,则选择较长的模式。
耗材类型
您可以根据Content-Type
的请求,
如以下示例所示:
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
// ...
}
consumes 属性还支持否定表达式,例如!text/plain
表示任何
内容类型以外的text/plain
.
您可以声明共享的consumes
属性。与大多数其他请求不同
但是,当在类级别使用时,映射属性是方法级别的consumes
属性
覆盖而不是扩展类级声明。
MediaType 为常用媒体类型提供常量,例如APPLICATION_JSON_VALUE 和APPLICATION_XML_VALUE . |
可生产的培养基类型
您可以根据Accept
request 标头和
控制器方法生成的内容类型,如以下示例所示:
@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
.
您可以声明共享的produces
属性。与大多数其他请求不同
但是,当在类级别使用时,映射属性是方法级别的produces
属性
覆盖而不是扩展类级声明。
MediaType 为常用媒体类型提供常量——例如APPLICATION_JSON_VALUE ,APPLICATION_XML_VALUE . |
参数和标头
您可以根据查询参数条件缩小请求映射范围。您可以测试
存在查询参数 (myParam
),因为它没有 (!myParam
),或对于
具体值 (myParam=myValue
).以下示例测试具有值的参数:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查myParam 等于myValue . |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查myParam 等于myValue . |
您还可以将相同的方法用于请求标头条件,如以下示例所示:
@GetMapping(path = "/pets/{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
和@RequestMapping(method=HttpMethod.GET)
支持 HTTP HEAD
透明地用于请求映射目的。控制器方法无需更改。
响应包装器,应用于HttpHandler
服务器适配器,确保Content-Length
header 设置为写入的字节数,但实际上没有写入响应。
默认情况下,HTTP OPTIONS 是通过设置Allow
response 标头添加到 HTTP 列表中
所有@RequestMapping
具有匹配 URL 模式的方法。
对于一个@RequestMapping
如果没有 HTTP 方法声明,则Allow
header 设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
.控制器方法应始终声明
支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体 —@GetMapping
,@PostMapping
等)。
您可以显式映射@RequestMapping
方法设置为 HTTP HEAD 和 HTTP OPTIONS,但该
在常见情况下没有必要。
自定义注释
Spring WebFlux 支持使用组合注释进行请求映射。这些注释本身是元注释的@RequestMapping
并组成以重新声明@RequestMapping
具有更窄、更具体用途的属性。
@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
和@PatchMapping
是
组合注释的示例。它们被提供,因为可以说,大多数
控制器方法应映射到特定的 HTTP 方法,而不是使用@RequestMapping
,
默认情况下,它与所有 HTTP 方法匹配。如果您需要一个组合的示例
注释,看看这些是如何声明的。
Spring WebFlux 还支持具有自定义请求匹配的自定义请求映射属性
逻辑。这是一个更高级的选项,需要子类化RequestMappingHandlerMapping
并覆盖getCustomMethodCondition
方法,其中
您可以检查自定义属性并返回您自己的RequestCondition
.
显式注册
您可以以编程方式注册 Handler 方法,该方法可用于动态 注册或高级情况,例如同一处理程序的不同实例 在不同的 URL 下。以下示例显示了如何执行此作:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
1 | 注入目标处理程序和控制器的处理程序映射。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 注入目标处理程序和控制器的处理程序映射。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
1.4.3. 处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列
支持的控制器方法参数和返回值。
方法参数
下表显示了支持的控制器方法参数。
响应式类型(Reactor、RxJava 或其他)是 在需要阻止 I/O(例如,读取请求正文)的参数上支持 被解决。这在“描述”列中标记。不预期响应类型 在不需要阻止的参数上。
JDK 1.8 的java.util.Optional
支持作为方法参数与
具有required
属性(例如,@RequestParam
,@RequestHeader
,
等)等,相当于required=false
.
控制器方法参数 | 描述 |
---|---|
|
访问完整的 |
|
访问 HTTP 请求或响应。 |
|
访问会话。这不会强制启动新会话,除非属性 被添加。支持响应式类型。 |
|
当前经过身份验证的用户 — 可能是特定的 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的名称-值对。请参阅矩阵变量。 |
|
用于访问查询参数。参数值转换为声明的方法参数
类型。看 请注意,使用 |
|
用于访问请求标头。标头值转换为声明的方法参数
类型。看 |
|
用于访问 cookie。Cookie 值将转换为声明的方法参数类型。
看 |
|
用于访问 HTTP 请求正文。正文内容转换为声明的方法
参数类型,使用 |
|
用于访问请求标头和正文。身体被转换成 |
|
|
|
用于访问 HTML 控制器中使用的模型,并公开给模板作为 视图渲染的一部分。 |
|
用于访问模型中的现有属性(如果不存在则实例化),使用
应用数据绑定和验证。看 请注意,使用 |
|
用于访问命令对象的验证和数据绑定中的错误,即 |
|
用于标记表单处理完成,这会触发会话属性的清理
通过类级声明 |
|
用于准备相对于当前请求的主机、端口、方案和 上下文路径。请参阅 URI 链接。 |
|
用于访问任何会话属性 — 与存储在会话中的模型属性相反
由于类级 |
|
用于访问请求属性。看 |
任何其他参数 |
如果方法参数与上述任何参数不匹配,则默认情况下,它被解析为
一个 |
返回值
下表显示了支持的控制器方法返回值。请注意,响应式 Reactor、RxJava 或其他库中的类型是 通常支持所有返回值。
控制器方法返回值 | 描述 |
---|---|
|
返回值通过 |
|
返回值指定完整的响应,包括 HTTP 标头,并且正文被编码
通过 |
|
用于返回带有标头且没有正文的响应。 |
|
要解析的视图名称 |
|
一个 |
|
要添加到隐式模型的属性,其中视图名称是隐式确定的 基于请求路径。 |
|
要添加到模型的属性,视图名称隐式确定 在请求路径上。 请注意 |
|
用于模型和视图呈现方案的 API。 |
|
具有 如果以上都不是真的,则 |
|
发出服务器发送的事件。这 |
其他返回值 |
如果返回值以任何其他方式仍未解析,则将其视为模型 属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型, 在这种情况下,它仍然没有解决。 |
类型转换
一些带注释的控制器方法参数表示基于字符串的请求输入(例如,@RequestParam
,@RequestHeader
,@PathVariable
,@MatrixVariable
和@CookieValue
)
如果参数声明为String
.
对于这种情况,将根据配置的转换器自动应用类型转换。
默认情况下,简单类型(例如int
,long
,Date
等)都得到了支持。类型转换
可以通过WebDataBinder
(参见DataBinder
)或通过注册Formatters
使用FormattingConversionService
(参见 Spring 字段格式)。
类型转换中的一个实际问题是处理空的 String 源值。
如果这样的值变成null
类型转换的结果。
这可能是Long
,UUID
和其他目标类型。如果要允许null
要注入,请使用required
标志,或声明
argument 作为@Nullable
.
矩阵变量
矩阵变量可以出现在任何路径段中,每个变量都用分号和
用逗号分隔的多个值 — 例如"/cars;color=red,green;year=2012"
.倍数
也可以通过重复的变量名称来指定值,例如,"color=red;color=green;color=blue"
.
与 Spring MVC 不同,在 WebFlux 中,URL 中矩阵变量的存在与否确实如此 不影响请求映射。换句话说,您不需要使用 URI 变量 以掩盖可变内容。也就是说,如果你想从 controller 方法,需要在路径段中添加一个 URI 变量,其中矩阵 变量是预期的。以下示例显示了如何执行此作:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有路径段都可以包含矩阵变量,有时可能需要 消除矩阵变量预期位于哪个路径变量的歧义, 如以下示例所示:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
您可以定义矩阵变量,可以定义为可选变量并指定默认值 如以下示例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,请使用MultiValueMap
,如以下示例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
您可以使用@RequestParam
注释,将查询参数绑定到方法参数
控制器。以下代码片段显示了用法:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | 用@RequestParam . |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 用@RequestParam . |
Servlet API“请求参数”概念将查询参数、形式
数据,并多部分合二为一。但是,在 WebFlux 中,每个都可以通过ServerWebExchange .而@RequestParam 绑定到查询参数,您可以使用
数据绑定,将查询参数、表单数据和多部分应用于命令对象。 |
使用@RequestParam
注释是默认必需的,但
您可以通过设置@RequestParam
自false
或者通过使用java.util.Optional
包装纸。
如果目标方法参数类型不是String
.请参阅类型转换。
当@RequestParam
注释在Map<String, String>
或MultiValueMap<String, String>
参数,则映射将填充所有查询参数。
请注意,使用@RequestParam
是可选的——例如,设置其属性。由
default,任何简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且不被任何其他参数解析器视为已注释
跟@RequestParam
.
@RequestHeader
您可以使用@RequestHeader
注释,将请求标头绑定到一个
控制器。
以下示例显示了带有标头的请求:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
以下示例获取Accept-Encoding
和Keep-Alive
头:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取Accept-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 页眉。 |
如果目标方法参数类型不是String
.请参阅类型转换。
当@RequestHeader
注释用于Map<String, String>
,MultiValueMap<String, String>
或HttpHeaders
参数,则填充地图
替换为所有标头值。
内置支持可用于将逗号分隔的字符串转换为数组或字符串的集合或类型转换系统已知的其他类型。 为 例如,一个方法参数用@RequestHeader("Accept") 可能是类型String 但也String[] 或List<String> . |
@CookieValue
您可以使用@CookieValue
注释,将 HTTP cookie 的值绑定到方法参数在控制器中。
以下示例显示了带有 cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下代码示例演示了如何获取 cookie 值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 | 获取 cookie 值。 |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
1 | 获取 cookie 值。 |
如果目标方法参数类型不是String
.请参阅类型转换。
@ModelAttribute
您可以使用@ModelAttribute
方法参数上的注释以从
模型或实例化它(如果不存在)。model 属性也覆盖了
名称与字段名称匹配的查询参数和表单字段的值。这是
称为数据绑定,它使您不必处理解析和
转换单个查询参数和表单字段。以下示例绑定Pet
:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 | 绑定Pet . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 | 绑定Pet . |
这Pet
实例的解析方法如下:
-
如果已通过
Model
. -
从 HTTP 会话到
@SessionAttributes
. -
从调用默认构造函数。
-
从调用具有匹配查询的参数的“主构造函数” 参数或表单字段。参数名称通过 JavaBeans 确定
@ConstructorProperties
或通过字节码中运行时保留的参数名称。
获取模型属性实例后,应用数据绑定。这WebExchangeDataBinder
类将查询参数的名称和表单字段与字段匹配
目标上的名称Object
.应用类型转换后填充匹配字段
必要时。有关数据绑定(和验证)的更多信息,请参阅验证。有关自定义数据绑定的更多信息,请参阅DataBinder
.
数据绑定可能会导致错误。默认情况下,一个WebExchangeBindException
被抬高,但是,
要检查控制器方法中的此类错误,您可以添加一个BindingResult
论点
紧邻@ModelAttribute
,如以下示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 添加一个BindingResult . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 添加一个BindingResult . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 用@Valid 在模型属性参数上。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 用@Valid 在模型属性参数上。 |
与 Spring MVC 不同,Spring WebFlux 支持模型中的响应式类型——例如Mono<Account>
或io.reactivex.Single<Account>
.您可以声明一个@ModelAttribute
论点
无论是否带有响应式包装器,它都会被相应地解析,
如有必要,设置为实际值。但是,请注意,要使用BindingResult
参数,则必须声明@ModelAttribute
没有响应式的参数
类型 wrapper,如前所示。或者,您可以通过
reactive 类型,如以下示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
return petMono
.flatMap { pet ->
// ...
}
.onErrorResume{ ex ->
// ...
}
}
请注意,使用@ModelAttribute
是可选的 — 例如,设置其属性。默认情况下,任何不是简单值类型(由 BeanUtils#isSimpleProperty 确定)并且不被任何其他参数解析器的参数解析器都被视为已注释 跟@ModelAttribute
.
@SessionAttributes
@SessionAttributes
用于将模型属性存储在WebSession
之间 请求。 它是一个类型级注释,用于声明特定的控制器。这通常列出模型属性或类型的名称模型属性应透明地存储在会话中以供后续访问请求。
请考虑以下示例:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注解。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注解。 |
在第一个请求中,当名称为pet
,被添加到模型中,它会自动提升并保存在WebSession
. 它一直留在那里,直到另一个控制器方法使用SessionStatus
method 参数来清除存储,如以下示例所示:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete();
// ...
}
}
}
1 | 使用@SessionAttributes 注解。 |
2 | 使用SessionStatus 变量。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete()
// ...
}
}
1 | 使用@SessionAttributes 注解。 |
2 | 使用SessionStatus 变量。 |
@SessionAttribute
如果您需要访问全局管理的预先存在的会话属性
(即,在控制器外部 - 例如,通过过滤器),并且可能存在也可能不存在,
您可以使用@SessionAttribute
方法参数上的注释,如以下示例所示:
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | 用@SessionAttribute . |
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
1 | 用@SessionAttribute . |
对于需要添加或删除会话属性的用例,请考虑注入WebSession
进入控制器方法。
用于在会话中临时存储模型属性作为控制器的一部分
工作流,请考虑使用SessionAttributes
,如@SessionAttributes
.
@RequestAttribute
类似于@SessionAttribute
,您可以使用@RequestAttribute
注释到
访问之前创建的预先存在的请求属性(例如,通过WebFilter
),
如以下示例所示:
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 用@RequestAttribute . |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 用@RequestAttribute . |
多部分内容
如 Multipart Data 中所述,ServerWebExchange
提供对 Multipart 的访问
内容。在控制器中处理文件上传表单(例如,从浏览器)的最佳方式
是通过与命令对象的数据绑定,
如以下示例所示:
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 - 类似于@RequestBody
),
您可以声明一个具体的目标Object
而不是Part
,如以下示例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
1 | 用@RequestPart 以获取元数据。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
1 | 用@RequestPart 以获取元数据。 |
您可以使用@RequestPart
结合使用javax.validation.Valid
或 Spring 的@Validated
注释,这会导致应用标准 Bean 验证。验证
错误导致WebExchangeBindException
这会导致 400 (BAD_REQUEST) 的响应。
异常包含一个BindingResult
带有错误详细信息,也可以处理
在控制器方法中,使用异步包装器声明参数,然后使用
错误相关运算符:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}
要将所有多部分数据作为MultiValueMap
,您可以使用@RequestBody
,
如以下示例所示:
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
1 | 用@RequestBody . |
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
1 | 用@RequestBody . |
要以流式方式按顺序访问多部分数据,您可以使用@RequestBody
跟Flux<Part>
(或Flow<Part>
在 Kotlin 中),如以下示例所示:
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
1 | 用@RequestBody . |
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
// ...
}
1 | 用@RequestBody . |
@RequestBody
您可以使用@RequestBody
注释,让请求正文读取并反序列化为Object
通过 HttpMessageReader。
以下示例使用@RequestBody
论点:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
与 Spring MVC 不同,在 WebFlux 中,@RequestBody
method 参数支持响应式类型以及完全非阻塞读取和(客户端到服务器)流式传输。
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
您可以使用 WebFlux Config 的 HTTP 消息编解码器选项来配置或自定义消息阅读器。
您可以使用@RequestBody
结合使用javax.validation.Valid
或 Spring 的@Validated
注释,这会导致应用标准 Bean 验证。 验证 错误会导致WebExchangeBindException
,这会导致 400 (BAD_REQUEST) 响应。异常包含一个BindingResult
带有错误详细信息,并且可以在controller 方法中处理,方法是使用异步包装器声明参数,然后使用 error相关运算符:
@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
您可以使用@ResponseBody
将返回序列化的方法上的注释
通过 HttpMessageWriter 发送到响应正文。以下内容
示例显示了如何执行此作:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别也受支持,在这种情况下,它由
所有控制器方法。这就是@RestController
,仅此而已
而不是标记为@Controller
和@ResponseBody
.
您可以组合@ResponseBody
方法与 JSON 序列化视图。
有关详细信息,请参阅 Jackson JSON。
您可以使用 WebFlux Config 的 HTTP 消息编解码器选项来 配置或自定义消息写入。
ResponseEntity
ResponseEntity
就像@ResponseBody
但带有状态和标题。例如:
@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>>
或ResponseEntity<Flux<T>>
使响应状态和 标头立即已知,而正文稍后以异步方式提供。 用Mono
如果正文由 0..1 个值组成或Flux
如果它可以产生多个值。 -
Mono<ResponseEntity<T>>
提供所有三个 — 响应状态、标头和正文, 稍后异步。这允许响应状态和标头变化 取决于异步请求处理的结果。 -
Mono<ResponseEntity<Mono<T>>>
或Mono<ResponseEntity<Flux<T>>>
是另一个 可能的,尽管不太常见的替代方案。它们提供响应状态和标头 首先异步,然后是响应体,其次也是异步的。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring WebFlux 为 Jackson 的序列化视图提供了内置支持,
它只允许渲染Object
.要将其与@ResponseBody
或ResponseEntity
controller 方法,可以使用 Jackson 的@JsonView
Comments 激活序列化视图类,如以下示例所示:
@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
注解:
本节讨论@ModelAttribute
方法,或上一个列表中的第二项。
控制器可以有任意数量的@ModelAttribute
方法。所有这些方法都是
之前调用@RequestMapping
方法。一个@ModelAttribute
方法也可以通过@ControllerAdvice
.有关更多详细信息,请参阅控制器建议部分。
@ModelAttribute
方法具有灵活的方法签名。他们支持许多相同的
arguments 作为@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);
}
如果未显式指定名称,则根据类型选择默认名称
如 Javadoc 中所述Conventions .
始终可以使用重载的addAttribute method 或
通过 name 属性@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 {
// ...
}
此外,任何具有响应式类型包装器的模型属性都将解析为其 实际值(和模型更新)在视图渲染之前。
您还可以使用@ModelAttribute
作为方法级注释@RequestMapping
方法,在这种情况下,返回值@RequestMapping
方法被解释为
model 属性。这通常不是必需的,因为它是 HTML 中的默认行为
控制器,除非返回值是String
否则会被解释
作为视图名称。@ModelAttribute
还可以帮助自定义模型属性名称,
如以下示例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.4.5.DataBinder
@Controller
或@ControllerAdvice
类可以有@InitBinder
方法,设置为
初始化WebDataBinder
.反过来,这些用于:
-
将请求参数(即表单数据或查询)绑定到模型对象。
-
转换
String
基于请求值(例如请求参数、路径变量、 标头、cookie 等)添加到控制器方法参数的目标类型。 -
将模型对象值格式化为
String
呈现 HTML 表单时的值。
@InitBinder
方法可以注册特定于控制器java.beans.PropertyEditor
或
SpringConverter
和Formatter
组件。此外,您可以使用 WebFlux Java 配置进行注册Converter
和Formatter
全局共享的类型FormattingConversionService
.
@InitBinder
方法支持许多相同的参数@RequestMapping
方法
do,但@ModelAttribute
(命令对象)参数。通常,它们被声明为
使用WebDataBinder
参数,用于注册,以及void
返回值。以下示例使用@InitBinder
注解:
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
1 | 使用@InitBinder 注解。 |
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
或者,当使用Formatter
通过共享的基于设置FormattingConversionService
,您可以重复使用相同的方法并注册特定于控制器Formatter
实例,如以下示例所示:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
1 | 添加自定义格式化程序(DateFormatter ,在本例中)。 |
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
1 | 添加自定义格式化程序(DateFormatter ,在本例中)。 |
模型设计
在 Web 应用程序的上下文中,数据绑定涉及 HTTP 请求的绑定 参数(即表单数据或查询参数)添加到模型对象中的属性,以及 其嵌套对象。
只public
遵循 JavaBeans 命名约定的属性公开用于数据绑定 — 例如,public String getFirstName()
和public void setFirstName(String)
方法firstName
财产。
模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO(普通旧 Java 对象)。 |
默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为 客户端可以面向任何公共属性路径,甚至是一些不预期的路径 针对给定用例。
例如,给定 HTTP 表单数据终结点,恶意客户端可以提供 存在于模型对象图中但不属于 HTML 表单的属性 在浏览器中显示。这可能导致在模型对象和任何 其嵌套对象的,预计不会更新。
建议的方法是使用仅公开的专用模型对象
与表单提交相关的属性。例如,在用于更改的表单上
用户的电子邮件地址,模型对象应声明一组最小的属性,例如
如下所示ChangeEmailForm
.
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
如果您不能或不想为每个数据使用专用模型对象
绑定用例,您必须限制数据绑定允许的属性。
理想情况下,您可以通过setAllowedFields()
方法WebDataBinder
.
例如,要在应用程序中注册允许的字段模式,您可以实现@InitBinder
方法@Controller
或@ControllerAdvice
组件如下图所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以注册不允许的模式
字段模式通过setDisallowedFields()
方法DataBinder
及其子类。
但请注意,“允许列表”比“拒绝列表”更安全。因此setAllowedFields()
应该被青睐setDisallowedFields()
.
请注意,与允许的字段模式进行匹配区分大小写;而匹配 针对不允许的字段模式不区分大小写。此外,与 不允许的模式将不会被接受,即使它也恰好与 允许列表。
正确配置允许和不允许的字段模式非常重要 直接公开域模型以进行数据绑定时。否则,它是 巨大的安全风险。 此外,强烈建议您不要使用域中的类型 模型,例如 JPA 或 Hibernate 实体作为数据绑定场景中的模型对象。 |
1.4.6. 管理异常
@Controller
并且@ControllerAdvice类可以有@ExceptionHandler
处理来自控制器方法的异常的方法。以下内容
示例包括这样的处理程序方法:
@Controller
public class SimpleController {
// ...
@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
1 | 声明@ExceptionHandler . |
@Controller
class SimpleController {
// ...
@ExceptionHandler (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
1 | 声明@ExceptionHandler . |
异常可以与正在传播的顶级异常(即直接IOException
被抛出)或针对顶级包装器中的直接原因
异常(例如,一个IOException
包裹在一个IllegalStateException
).
对于匹配的异常类型,最好将目标异常声明为方法参数,
如前面的示例所示。或者,注释声明可以缩小
异常类型。我们通常建议在
参数签名,并在@ControllerAdvice
以相应的顺序优先。
有关详细信息,请参阅 MVC 部分。
一@ExceptionHandler 方法支持相同的方法参数和
返回值作为@RequestMapping 方法,但请求正文除外-
和@ModelAttribute -related 方法参数。 |
对@ExceptionHandler
Spring WebFlux 中的方法由HandlerAdapter
为@RequestMapping
方法。看DispatcherHandler
了解更多详情。
REST API 异常
REST 服务的一个常见要求是在
响应。Spring Framework 不会自动这样做,因为表示
响应正文中的错误详细信息特定于应用程序。但是,一个@RestController
可以使用@ExceptionHandler
方法ResponseEntity
返回
值来设置响应的状态和正文。也可以声明此类方法
在@ControllerAdvice
类以在全球范围内应用它们。
请注意,Spring WebFlux 没有 Spring MVC 的等效项ResponseEntityExceptionHandler ,因为 WebFlux 仅ResponseStatusException (或其子类),并且不需要将其转换为
HTTP 状态代码。 |
1.4.7. 控制器建议
通常,@ExceptionHandler
,@InitBinder
和@ModelAttribute
方法适用
在@Controller
class(或类层次结构),其中声明了它们。如果你
希望此类方法更全局(跨控制器)应用,您可以在
类用@ControllerAdvice
或@RestControllerAdvice
.
@ControllerAdvice
注释为@Component
,这意味着此类类可以是
通过组件扫描注册为 Spring bean。@RestControllerAdvice
是带有注释的组合注释
两者兼而有之@ControllerAdvice
和@ResponseBody
,这本质上意味着@ExceptionHandler
方法通过消息转换呈现到响应正文
(与视图分辨率或模板渲染相比)。
启动时,的基础设施类@RequestMapping
和@ExceptionHandler
方法检测带有@ControllerAdvice
然后应用他们的
方法。全球@ExceptionHandler
方法(从@ControllerAdvice
) 是
在本地之后应用(从@Controller
).相比之下,全球@ModelAttribute
和@InitBinder
方法在本地方法之前应用。
默认情况下,@ControllerAdvice
方法适用于每个请求(即所有控制器),
但是,您可以使用
注释,如以下示例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}
前面示例中的选择器在运行时进行评估,可能会产生负面影响
如果广泛使用,性能。请参阅@ControllerAdvice
javadoc 了解更多详情。
1.5. 功能端点
Spring WebFlux 包括 WebFlux.fn,这是一个轻量级函数式编程模型,其中函数 用于路由和处理请求,合约设计为不可变性。 它是基于注释的编程模型的替代方案,但在其他方面运行在 相同的响应式核心基础。
1.5.1. 概述
在 WebFlux.fn 中,HTTP 请求使用HandlerFunction
:一个接受ServerRequest
并返回延迟的ServerResponse
(即Mono<ServerResponse>
).
请求和响应对象都有不可变的合约,提供 JDK 8 友好
访问 HTTP 请求和响应。HandlerFunction
相当于一个@RequestMapping
方法
基于注释的编程模型。
传入请求被路由到带有RouterFunction
:一个函数
需要ServerRequest
并返回延迟的HandlerFunction
(即Mono<HandlerFunction>
).
当路由器函数匹配时,返回一个处理程序函数;否则是空的单声道。RouterFunction
相当于@RequestMapping
注释,但使用 major
路由器功能不仅提供数据,还提供行为。
RouterFunctions.route()
提供了一个路由器构建器,有助于创建路由器,
如以下示例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
suspend fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
suspend fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
suspend fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
1 | 使用协程路由器 DSL 创建路由器,也可以通过以下方式获得响应式替代方案router { } . |
运行RouterFunction
就是把它变成一个HttpHandler
并安装它
通过内置服务器适配器之一:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多数应用程序都可以通过 WebFlux Java 配置运行,请参阅运行服务器。
1.5.2. 处理程序函数
ServerRequest
和ServerResponse
是不可变接口,提供 JDK 8 友好访问 HTTP 请求和响应。请求和响应都提供反应流背压针对正文流。请求正文用 Reactor 表示Flux
或Mono
. 响应正文用任何响应式流表示Publisher
包括Flux
和Mono
. 有关更多信息,请参阅响应式库。
服务器请求
ServerRequest
提供对 HTTP 方法、URI、标头和查询参数的访问,
虽然对身体的访问是通过body
方法。
以下示例将请求正文提取到Mono<String>
:
Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()
以下示例将正文提取为Flux<Person>
(或Flow<Person>
在 Kotlin 中),
哪里Person
对象从某种序列化形式解码,例如 JSON 或 XML:
Flux<Person> people = request.bodyToFlux(Person.class);
val people = request.bodyToFlow<Person>()
前面的示例是使用更通用的ServerRequest.body(BodyExtractor)
,
它接受BodyExtractor
功能策略界面。实用程序类BodyExtractors
提供对多个实例的访问。例如,前面的示例可以
也写如下:
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
以下示例演示如何访问表单数据:
Mono<MultiValueMap<String, String>> map = request.formData();
val map = request.awaitFormData()
以下示例演示如何以地图形式访问多部分数据:
Mono<MultiValueMap<String, Part>> map = request.multipartData();
val map = request.awaitMultipartData()
以下示例演示如何以流式处理方式一次访问一个多部分:
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
服务器响应
ServerResponse
提供对 HTTP 响应的访问,并且由于它是不可变的,因此您可以使用
一个build
创建它的方法。您可以使用构建器设置响应状态,添加响应
标头,或提供正文。以下示例使用 JSON 创建 200 (OK) 响应
内容:
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
以下示例演示如何使用Location
header 且无正文:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
根据使用的编解码器,可以传递提示参数来自定义 正文是序列化或反序列化的。例如,要指定 Jackson JSON 视图,请执行以下作:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
处理程序类
我们可以将处理程序函数编写为 lambda,如以下示例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
这很方便,但在应用程序中我们需要多个函数和多个内联
lambda 可能会变得混乱。
因此,将相关的处理程序函数组合到一个处理程序类中是有用的,该类
具有与@Controller
在基于注释的应用程序中。
例如,以下类公开响应式Person
存储 库:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
1 | listPeople 是一个处理程序函数,它返回所有Person 在存储库中找到的对象作为
JSON的。 |
2 | createPerson 是一个处理程序函数,用于存储新的Person 包含在请求正文中。
请注意PersonRepository.savePerson(Person) 返回Mono<Void> :一个空的Mono 发出的
从请求中读取并存储此人时的完成信号。所以我们使用build(Publisher<Void>) 方法在收到该完成信号时发送响应(即,
当Person 已被保存)。 |
3 | getPerson 是一个处理程序函数,返回单个人,由id 路径
变量。我们检索Person 并创建 JSON 响应(如果是)
发现。如果找不到,我们使用switchIfEmpty(Mono<T>) 返回 404 Not Found 响应。 |
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
1 | listPeople 是一个处理程序函数,它返回所有Person 在存储库中找到的对象作为
JSON的。 |
2 | createPerson 是一个处理程序函数,用于存储新的Person 包含在请求正文中。
请注意PersonRepository.savePerson(Person) 是一个没有返回类型的挂起函数。 |
3 | getPerson 是一个处理程序函数,返回单个人,由id 路径
变量。我们检索Person 并创建 JSON 响应(如果是)
发现。如果未找到,我们将返回 404 Not Found 响应。 |
验证
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
1 | 创造Validator 实例。 |
2 | 应用验证。 |
3 | 为 400 响应引发异常。 |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
1 | 创造Validator 实例。 |
2 | 应用验证。 |
3 | 为 400 响应引发异常。 |
处理程序还可以通过创建和注入来使用标准 bean 验证 API (JSR-303)
全局Validator
实例基于LocalValidatorFactoryBean
.
请参阅 Spring Validation。
1.5.3.RouterFunction
路由器函数用于将请求路由到相应的HandlerFunction
. 通常,您不会自己编写路由器函数,而是使用RouterFunctions
实用程序类创建一个。RouterFunctions.route()
(无参数)为您提供了一个流畅的构建器来创建路由器函数,而RouterFunctions.route(RequestPredicate, HandlerFunction)
提供直接方式
以创建路由器。
一般建议使用route()
builder,因为它提供了
典型映射场景的便捷捷径,无需难以发现
静态导入。
例如,路由器函数构建器提供了GET(String, HandlerFunction)
为 GET 请求创建映射;和POST(String, HandlerFunction)
对于 POST。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种引入其他
谓词。
对于每个 HTTP 方法,都有一个重载变体,它采用RequestPredicate
作为
参数,但可以表达其他约束。
谓词
你可以自己写RequestPredicate
,但RequestPredicates
实用程序类
提供常用的实现,基于请求路径、HTTP 方法、内容类型、
等等。
以下示例使用请求谓词创建基于Accept
页眉:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
您可以使用以下命令将多个请求谓词组合在一起:
-
RequestPredicate.and(RequestPredicate)
— 两者必须匹配。 -
RequestPredicate.or(RequestPredicate)
——两者都可以匹配。
许多谓词来自RequestPredicates
组成。
例如RequestPredicates.GET(String)
由RequestPredicates.method(HttpMethod)
和RequestPredicates.path(String)
.
上面显示的示例还使用两个请求谓词,因为构建器使用RequestPredicates.GET
内部,并使用accept
谓语。
路线
路由器功能按顺序计算:如果第一个路由不匹配,则 second 被评估,依此类推。 因此,在一般路线之前声明更具体的路线是有意义的。 当将路由器函数注册为 Spring Bean 时,这一点也很重要,同样 稍后会描述。 请注意,此行为与基于注释的编程模型不同,其中 自动选择“最具体”的控制器方法。
使用路由器函数构建器时,所有定义的路由都组合成一个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} 使用Accept 标头将被路由到PersonHandler.getPerson |
2 | GET /person 使用Accept 标头将被路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到构建的路由中的路由器函数。 |
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | GET /person/{id} 使用Accept 标头将被路由到PersonHandler.getPerson |
2 | GET /person 使用Accept 标头将被路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到构建的路由中的路由器函数。 |
嵌套路由
一组路由器函数通常具有共享谓词,例如
共享路径。在上面的示例中,共享谓词将是一个路径谓词,其中
比赛/person
,其中三条路线使用。使用注释时,您可以删除
这种重复使用类型级别@RequestMapping
映射到/person
.在 WebFlux.fn 中,可以通过path
方法
路由器函数构建器。例如,上面示例的最后几行可以是
通过使用嵌套路由,以以下方式进行了改进:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
1 | 请注意,第二个参数path 是采用路由器构建器的消费者。 |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但您可以使用
这nest
方法。
上面仍然包含一些共享形式的重复Accept
-header 谓词。
我们可以通过使用nest
方法和accept
:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(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 服务器中运行路由器功能?一个简单的选择是转换路由器
function 设置为HttpHandler
通过使用以下选项之一:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
然后,您可以使用返回的HttpHandler
使用多个服务器适配器,请按照 HttpHandler 获取特定于服务器的说明。
Spring Boot 也使用一个更典型的选项是使用DispatcherHandler
基于 WebFlux Config 的设置,该设置使用 Spring 配置来声明
处理请求所需的组件。WebFlux Java 配置声明以下内容
支持功能端点的基础结构组件:
-
RouterFunctionMapping
:检测一个或多个RouterFunction<?>
Spring的豆子 配置,对它们进行排序,通过RouterFunction.andOther
,并将请求路由到生成的组合RouterFunction
. -
HandlerFunctionAdapter
:简单的适配器,让DispatcherHandler
调用 一个HandlerFunction
该映射到请求。 -
ServerResponseResultHandler
:处理调用HandlerFunction
通过调用writeTo
方法ServerResponse
.
上述组件允许功能端点适合DispatcherHandler
请求
处理生命周期,并且(可能)与带注释的控制器并行运行,如果
any 被声明。这也是 Spring Boot WebFlux 启用功能端点的方式
起动机。
以下示例显示了 WebFlux Java 配置(有关如何运行它,请参阅 DispatcherHandler):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
1.5.5. 过滤处理程序函数
您可以使用before
,after
或filter
路由上的方法
函数生成器。
使用注释,您可以使用@ControllerAdvice
一个ServletFilter
,或两者兼而有之。
该过滤器将应用于构建者构建的所有路径。
这意味着嵌套路由中定义的过滤器不适用于“顶级”路由。
例如,考虑以下示例:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 这before 添加自定义请求标头的过滤器仅应用于两个 GET 路由。 |
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 | 这before 添加自定义请求标头的过滤器仅应用于两个 GET 路由。 |
2 | 这after 记录响应的过滤器将应用于所有路由,包括嵌套路由。 |
这filter
方法采用HandlerFilterFunction
:一个
采用ServerRequest
和HandlerFunction
并返回一个ServerResponse
.
处理程序函数参数表示链中的下一个元素。
这通常是路由到的处理程序,但也可以是另一个
如果应用了多个,则进行筛选。
现在我们可以向路由添加一个简单的安全过滤器,假设我们有一个SecurityManager
那
可以确定是否允许特定路径。
以下示例显示了如何执行此作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(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();
}
}
}
}
前面的示例演示了调用next.handle(ServerRequest)
是可选的。
我们只允许在允许访问时运行处理程序函数。
除了使用filter
方法,可以在路由器函数构建器上应用
通过以下方式过滤到现有路由器功能RouterFunction.filter(HandlerFilterFunction)
.
对功能终结点的 CORS 支持通过专用的CorsWebFilter . |
1.6. URI 链接
本节介绍 Spring Framework 中可用于准备 URI 的各种选项。
1.6.1. Uri组件
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
有助于从带有变量的 URI 模板构建 URI,如以下示例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 | 带有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
1 | 带有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
前面的示例可以合并为一个链,并使用buildAndExpand
,
如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()
您可以通过直接转到 URI(这意味着编码)来进一步缩短它, 如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.6.2. UriBuilder
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
实现UriBuilder
.您可以创建一个UriBuilder
,反过来,使用UriBuilderFactory
.一起UriBuilderFactory
和UriBuilder
提供一种可插拔的机制,用于从 URI 模板构建 URI,基于
共享配置,例如基本 URL、编码首选项和其他详细信息。
您可以配置RestTemplate
和WebClient
使用UriBuilderFactory
自定义 URI 的准备。DefaultUriBuilderFactory
是默认的
实现UriBuilderFactory
使用UriComponentsBuilder
内部和
公开共享配置选项。
以下示例演示如何配置RestTemplate
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory
以下示例配置WebClient
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val client = WebClient.builder().uriBuilderFactory(factory).build()
此外,您还可以使用DefaultUriBuilderFactory
径直。这类似于使用UriComponentsBuilder
但是,它不是静态工厂方法,而是一个实际实例
,其中包含配置和首选项,如以下示例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.6.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode(): 首先对 URI 模板进行预编码,然后在展开时严格编码 URI 变量。
-
UriComponents#encode(): 在 URI 变量展开后对 URI 组件进行编码。
这两个选项都将非 ASCII 和非法字符替换为转义八位字节。但是,第一个选项 还会替换 URI 变量中出现的具有保留含义的字符。
考虑“;”,它在路径中是合法的,但具有保留的含义。第一个选项将 “;”在 URI 变量中与“%3B”一起使用,但在 URI 模板中没有。相比之下,第二种选择从来不会 替换 “;”,因为它是路径中的法定字符。 |
在大多数情况下,第一个选项可能会给出预期的结果,因为它处理 URI 变量作为要完全编码的不透明数据,而第二个选项在 URI 变量确实有意包含保留字符。第二个选项也很有用 当根本不扩展 URI 变量时,因为这也会对任何 顺便说一句,看起来像一个 URI 变量。
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到 URI(这意味着编码) 来缩短前面的示例, 如以下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
这WebClient
和RestTemplate
通过内部扩展和编码 URI 模板
这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()
这DefaultUriBuilderFactory
实现用途UriComponentsBuilder
内部设置为
展开和编码 URI 模板。作为工厂,它提供了一个配置位置
编码方法,基于以下编码模式之一:
-
TEMPLATE_AND_VALUES
:使用UriComponentsBuilder#encode()
,对应于 前面列表中的第一个选项,用于预编码 URI 模板并在 扩大。 -
VALUES_ONLY
:不对 URI 模板进行编码,而是应用严格的编码 到 URI 变量UriUtils#encodeUriVariables
在将它们扩展到 模板。 -
URI_COMPONENT
:使用UriComponents#encode()
,对应于前面列表中的第二个选项,设置为 在 URI 变量展开后对 URI 组件值进行编码。 -
NONE
:不应用编码。
这RestTemplate
设置为EncodingMode.URI_COMPONENT
对于历史
原因和向后兼容性。这WebClient
依赖于默认值
在DefaultUriBuilderFactory
,从EncodingMode.URI_COMPONENT
在
5.0.x 到EncodingMode.TEMPLATE_AND_VALUES
在 5.1 中。
1.7. CORS
Spring WebFlux 允许您处理 CORS(跨域资源共享)。本节 描述了如何执行此作。
1.7.1. 简介
出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。 例如,您的银行账户可能位于一个选项卡中,而 evil.com 位于另一个选项卡中。脚本 从 evil.com 应该无法使用 凭据——例如,从您的账户中取款!
1.7.2. 处理
CORS 规范区分了预检请求、简单请求和实际请求。 要了解 CORS 的工作原理,您可以阅读这篇文章,其中 许多其他,或有关更多详细信息,请参阅规范。
Spring WebFluxHandlerMapping
实现为 CORS 提供内置支持。成功后
将请求映射到处理程序,则HandlerMapping
检查 CORS 配置中的
给定的请求和处理程序,并采取进一步的作。处理印前检查请求
直接,而简单和实际的 CORS 请求被拦截、验证,并具有
设置了必需的 CORS 响应标头。
为了启用跨域请求(即Origin
header 存在,并且
与请求的主机不同),您需要有一些显式声明的 CORS
配置。如果未找到匹配的 CORS 配置,则预检请求为
拒绝。不会将 CORS 标头添加到简单和实际 CORS 请求的响应中
因此,浏览器拒绝它们。
每HandlerMapping
可以使用基于 URL 模式的单独配置CorsConfiguration
映射。在大多数情况下,应用程序
使用 WebFlux Java 配置声明此类映射,这会导致单个
全局地图传递给所有HandlerMapping
实现。
您可以在HandlerMapping
与更多水平
细粒度的处理程序级 CORS 配置。例如,带注释的控制器可以使用
类级或方法级@CrossOrigin
注释(其他处理程序可以实现CorsConfigurationSource
).
组合全局和本地配置的规则通常是累加的,例如,
所有全局和所有本地原点。对于那些只能使用单个值的属性
accepted,例如allowCredentials
和maxAge
,则局部将覆盖全局值。看CorsConfiguration#combine(CorsConfiguration)
了解更多详情。
若要从源代码中了解详细信息或进行高级自定义,请参阅:
|
1.7.3. 凭证请求
将 CORS 与凭证请求一起使用需要启用allowedCredentials
.请注意
此选项与配置的域建立了高度信任,并且还增加了
通过暴露敏感的用户特定信息对 Web 应用程序进行攻击的表面
例如 cookie 和 CSRF Tokens。
启用凭据还会影响配置的 CORS 通配符的处理方式:"*"
-
通配符未授权
allowOrigins
,但也可以 这allowOriginPatterns
属性可用于匹配一组动态源。 -
当设置为
allowedHeaders
或allowedMethods
这Access-Control-Allow-Headers
和Access-Control-Allow-Methods
响应标头是通过复制相关的 CORS 预检请求中指定的标头和方法。 -
当设置为
exposedHeaders
,Access-Control-Expose-Headers
响应标头已设置 配置的标头列表或通配符。虽然 CORS 规范 不允许通配符Access-Control-Allow-Credentials
设置为true
,大多数浏览器都支持它,并且响应标头在 CORS 处理,因此通配符是当 指定,而不管allowCredentials
财产。
虽然这种通配符配置很方便,但建议在可能的情况下配置 有限的值集,以提供更高级别的安全性。 |
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 Tokens),以及
应仅在适当的情况下使用。启用时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. 全局配置
除了细粒度的控制器方法级配置之外,您可能还希望
还定义一些全局 CORS 配置。您可以设置基于 URLCorsConfiguration
在任何HandlerMapping
.但是,大多数应用程序都使用
WebFlux Java 配置来做到这一点。
默认情况下,全局配置启用以下功能:
-
所有起源。
-
所有标头。
-
GET
,HEAD
和POST
方法。
allowedCredentials
默认情况下不启用,因为这会建立信任级别
暴露敏感的用户特定信息(例如 cookie 和 CSRF Tokens),以及
应仅在适当的情况下使用。启用时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
,这是一个
与功能端点配合良好。
如果您尝试使用CorsFilter 使用 Spring Security,请记住 Spring
安全性内置了对
科斯。 |
要配置过滤器,您可以声明CorsWebFilter
bean 并传递一个CorsConfigurationSource
到其构造函数,如以下示例所示:
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
1.8. 网络安全
Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。查看 Spring Security性 参考文档,包括:
1.9. 查看技术
在 Spring WebFlux 中使用视图技术是可插拔的。无论您决定使用 Thymeleaf、FreeMarker 或其他一些视图技术,主要是配置更改的问题。本章介绍与 Spring 集成的视图技术WebFlux。我们假设您已经熟悉视图分辨率。
1.9.1. 胸腺叶
Thymeleaf 是一个强调自然 HTML 的现代服务器端 Java 模板引擎 可以通过双击在浏览器中预览的模板,这非常 有助于独立处理 UI 模板(例如,由设计人员)而无需 正在运行的服务器。Thymeleaf 提供了一组广泛的功能,并且正在积极开发中 并维护。如需更完整的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目管理。这
配置涉及一些 bean 声明,例如SpringResourceTemplateResolver
,SpringWebFluxTemplateEngine
和ThymeleafReactiveViewResolver
.有关更多详细信息,请参阅 Thymeleaf+Spring 和 WebFlux 集成公告。
1.9.2. 自由标记
Apache FreeMarker 是一个模板引擎,用于生成任何 从 HTML 到电子邮件等的文本输出。Spring 框架内置了 将 Spring WebFlux 与 FreeMarker 模板一起使用的集成。
查看配置
以下示例展示了如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates/freemarker")
}
}
您的模板需要存储在FreeMarkerConfigurer
, 如前面的示例所示。给定上述配置,如果您的控制器返回视图名称,welcome
,解析器会查找classpath:/templates/freemarker/welcome.ftl
模板。
FreeMarker 配置
您可以将 FreeMarker 的“设置”和“共享变量”直接传递给 FreeMarkerConfiguration
对象(由 Spring 管理)通过设置适当的 bean
属性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 的标签库,其中包含一个<spring:bind/>
元素。此元素主要允许表单显示来自
form-backing 对象,并显示来自Validator
在
Web 或业务层。Spring 也支持 FreeMarker 中的相同功能,
具有用于生成表单输入元素本身的额外便利宏。
绑定宏
一组标准宏在spring-webflux.jar
文件
FreeMarker,因此它们始终可用于适当配置的应用程序。
Spring 模板库中定义的一些宏被认为是内部的
(private),但宏定义中不存在此类作用域,使所有宏都可见
调用代码和用户模板。以下部分仅重点介绍宏
您需要直接从模板中调用。如果您想查看宏代码
直接调用该文件spring.ftl
并且位于org.springframework.web.reactive.result.view.freemarker
包。
有关绑定支持的其他详细信息,请参阅简单 Spring MVC 的绑定。
1.9.3. 脚本视图
Spring Framework 有一个内置的集成,用于将 Spring WebFlux 与任何 可以在 JSR-223 Java 脚本引擎之上运行的模板库。 下表显示了我们在不同脚本引擎上测试的模板库:
脚本库 | 脚本引擎 |
---|---|
集成任何其他脚本引擎的基本规则是它必须实现ScriptEngine 和Invocable 接口。 |
要求
您需要在类路径上安装脚本引擎,其详细信息因脚本引擎而异:
-
Nashorn JavaScript 引擎提供了 Java 8+。强烈建议使用可用的最新更新版本。
-
JRuby 应该添加为 Ruby 支持的依赖项。
-
Jython 应该添加为 Python 支持的依赖项。
-
org.jetbrains.kotlin:kotlin-script-util
dependency 和META-INF/services/javax.script.ScriptEngineFactory
文件包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
应添加行以支持 Kotlin 脚本。有关更多详细信息,请参阅此示例。
您需要有脚本模板库。对于 JavaScript 做到这一点的一种方法是 通过 WebJars。
脚本模板
您可以声明一个ScriptTemplateConfigurer
bean 来指定要使用的脚本引擎,
要加载的脚本文件、要调用的函数来渲染模板等等。
以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
这render
函数使用以下参数调用:
-
String template
:模板内容 -
Map model
:视图模型 -
RenderingContext renderingContext
:这RenderingContext
提供对应用程序上下文、区域设置、模板加载器和URL(自 5.0 起)的访问权限
Mustache.render()
与此签名原生兼容,因此您可以直接调用它。
如果您的模板技术需要一些自定义,您可以提供一个脚本来实现自定义渲染函数。例如,Handlerbars 需要在使用模板之前编译模板,并且需要填充才能模拟一些浏览器工具在服务器端脚本引擎中不可用。以下示例展示了如何设置自定义渲染函数:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
设置sharedEngine 属性设置为false 使用非线程安全时需要具有模板库的脚本引擎,例如在 Nashorn 上运行的 Handlebars 或React。在这种情况下,由于此错误,需要 Java SE 8 更新 60,但通常建议在任何情况下使用最新的 Java SE 补丁版本。 |
polyfill.js
仅定义window
Handlebars 正常运行所需的对象,如以下代码片段所示:
var window = {};
这个基本的render.js
实现在使用模板之前对其进行编译。生产就绪实现还应存储和重用缓存模板或预编译模板。这可以在脚本端完成,也可以在您需要的任何自定义(例如管理模板引擎配置)上完成。以下示例显示了如何编译模板:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.9.4. JSON和 XML
出于内容协商的目的,能够交替在使用 HTML 模板或其他格式(例如 JSON 或 XML)渲染模型之间,取决于客户端请求的内容类型。为了支持这样做,Spring WebFlux提供了HttpMessageWriterView
,您可以使用它来插入任何可用的编解码器spring-web
如Jackson2JsonEncoder
,Jackson2SmileEncoder
, 或Jaxb2XmlEncoder
.
与其他视图技术不同,HttpMessageWriterView
不需要ViewResolver
而是配置为默认视图。 您可以 配置一个或多个此类默认视图,将不同的HttpMessageWriter
实例 或Encoder
实例。 与请求的内容类型匹配的在运行时使用。
在大多数情况下,一个模型包含多个属性。要确定要序列化哪一个,
您可以配置HttpMessageWriterView
替换为要用于的 Model 属性的名称
渲染。如果模型仅包含一个属性,则使用该属性。
1.10. HTTP 缓存
HTTP 缓存可以显着提高 Web 应用程序的性能。HTTP 缓存围绕Cache-Control
response 标头和后续条件请求标头,例如Last-Modified
和ETag
.Cache-Control
建议私有(例如浏览器)和公共(例如代理)缓存如何缓存和重用响应。 一ETag
标头发出条件请求,可能导致没有正文的 304 (NOT_MODIFIED),如果内容没有改变。ETag
可以看作是更复杂的继任者 这Last-Modified
页眉。
本节介绍 Spring WebFlux 中可用的 HTTP 缓存相关选项。
1.10.1.CacheControl
CacheControl
提供支持配置与Cache-Control
标头,并被接受为参数在许多地方:
虽然 RFC 7234 描述了所有可能的指令Cache-Control
response 标头,CacheControl
类型采用面向用例的方法,专注于常见场景,如以下示例所示:
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
1.10.2. 控制器
控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为lastModified
或ETag
需要先计算资源的值,然后才能对其进行比较与条件请求标头。控制器可以添加ETag
和Cache-Control
settings 设置为ResponseEntity
,如以下示例所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {
val book = findBook(id)
val version = book.getVersion()
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book)
}
前面的示例发送带有空体的 304 (NOT_MODIFIED) 响应,如果比较到条件请求标头表示内容未更改。否则,ETag
和Cache-Control
标头将添加到响应中。
您还可以对控制器中的条件请求标头进行检查,如以下示例所示:
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {
long eTag = ... (1)
if (exchange.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
1 | 特定于应用程序的计算。 |
2 | 响应已设置为 304 (NOT_MODIFIED)。没有进一步处理。 |
3 | 继续处理请求。 |
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {
val eTag: Long = ... (1)
if (exchange.checkNotModified(eTag)) {
return null(2)
}
model.addAttribute(...) (3)
return "myViewName"
}
1 | 特定于应用程序的计算。 |
2 | 响应已设置为 304 (NOT_MODIFIED)。没有进一步处理。 |
3 | 继续处理请求。 |
有三种变体用于检查条件请求eTag
值lastModified
值,或两者。对于有条件的GET
和HEAD
请求,您可以将响应设置为
304 (NOT_MODIFIED)。对于有条件的POST
,PUT
和DELETE
,您可以改为设置响应
设置为 412 (PRECONDITION_FAILED) 以防止并发修改。
1.11. WebFlux 配置
WebFlux Java 配置声明需要处理的组件
请求,并提供了一个 API
自定义配置。这意味着您不需要了解底层
由 Java 配置创建的 bean。但是,如果你想了解它们,
您可以在WebFluxConfigurationSupport
或阅读更多关于它们是什么的信息
在特殊Beans中。
对于配置 API 中不可用的更高级自定义项,您可以 通过高级配置模式完全控制配置。
1.11.1. 启用 WebFlux 配置
您可以使用@EnableWebFlux
注释,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig {
}
@Configuration
@EnableWebFlux
class WebConfig
前面的示例注册了许多 Spring WebFlux 基础设施 bean 并适应依赖项在类路径上可用 — 用于 JSON、XML 等。
1.11.2. WebFlux 配置 API
在 Java 配置中,您可以实现WebFluxConfigurer
接口 如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// Implement configuration methods...
}
1.11.3. 转换、格式化
默认情况下,将安装各种数字和日期类型的格式化程序,并支持
用于定制@NumberFormat
和@DateTimeFormat
在田野上。
要在 Java 配置中注册自定义格式化程序和转换器,请使用以下命令:
@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 值。这适用于日期表示为带有“输入”形式的字符串的表单 领域。但是,对于“日期”和“时间”表单字段,浏览器使用定义的固定格式 在 HTML 规范中。对于这种情况,可以按如下方式自定义日期和时间格式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
看FormatterRegistrar SPI和FormattingConversionServiceFactoryBean 有关何时
用FormatterRegistrar 实现。 |
1.11.4. 验证
默认情况下,如果存在 Bean Validation
在类路径(例如,Hibernate Validator)上,该LocalValidatorFactoryBean
注册为全局验证器,用于@Valid
和@Validated
上@Controller
method 参数。
在 Java 配置中,您可以自定义全局Validator
实例
如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun getValidator(): Validator {
// ...
}
}
请注意,您也可以注册Validator
本地实现,
如以下示例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
如果您需要有一个LocalValidatorFactoryBean 注入到某处,创建一个 bean 和
用@Primary 以避免与 MVC 配置中声明的冲突。 |
1.11.5. 内容类型解析器
您可以配置 Spring WebFlux 如何确定请求的媒体类型@Controller
实例。默认情况下,只有Accept
标头被选中,
但您也可以启用基于查询参数的策略。
以下示例演示如何自定义请求的内容类型解析:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
// ...
}
}
1.11.6. HTTP 消息编解码器
以下示例演示如何自定义请求和响应正文的读取和写入方式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(512 * 1024);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
}
}
ServerCodecConfigurer
提供了一组默认的读取器和写入器。您可以使用它来添加
更多的读写器,自定义默认的,或完全替换默认的。
对于 Jackson JSON 和 XML,请考虑使用Jackson2ObjectMapperBuilder
,
它使用以下属性自定义 Jackson 的默认属性:
如果在类路径上检测到以下已知模块,它还会自动注册它们:
-
jackson-datatype-joda
:支持 Joda-Time 类型。 -
jackson-datatype-jsr310
:支持 Java 8 日期和时间 API 类型。 -
jackson-datatype-jdk8
:支持其他 Java 8 类型,例如Optional
. -
jackson-module-kotlin
:支持 Kotlin 类和数据类。
1.11.7. 视图解析器
以下示例演示如何配置视图分辨率:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// ...
}
}
这ViewResolverRegistry
具有 Spring Framework 使用的视图技术的快捷方式
集成。以下示例使用 FreeMarker(这也需要配置
底层 FreeMarker 视图技术):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure Freemarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure Freemarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
}
}
您还可以插入任何ViewResolver
实现,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ViewResolver resolver = ... ;
registry.viewResolver(resolver);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
val resolver: ViewResolver = ...
registry.viewResolver(resolver
}
}
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = Jackson2JsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
// ...
}
有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术。
1.11.8. 静态资源
此选项提供了一种方便的方式来提供列表中的静态资源Resource
基于位置。
在下一个示例中,给定一个以/resources
,相对路径为
用于查找和提供相对于/static
在类路径上。资源
未来到期时间为一年,以确保最大限度地利用浏览器缓存
以及减少浏览器发出的 HTTP 请求。这Last-Modified
header 也是
评估,如果存在,则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
implementations 和ResourceTransformer
实现
可用于创建用于处理优化资源的工具链。
您可以使用VersionResourceResolver
对于基于 MD5 哈希的版本化资源 URL
根据内容、固定应用程序版本或其他信息计算。一个ContentVersionStrategy
(MD5 哈希)是一个不错的选择,但有一些值得注意的例外(例如
与模块加载器一起使用的 JavaScript 资源)。
以下示例演示如何使用VersionResourceResolver
在您的 Java 配置中:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
您可以使用ResourceUrlProvider
重写 URL 并应用完整的解析器链和
转换器(例如,插入版本)。WebFlux 配置提供了一个ResourceUrlProvider
这样就可以注射到别人身上。
与 Spring MVC 不同,目前在 WebFlux 中,没有办法透明地重写静态
资源 URL,因为没有可以使用非阻塞链的视图技术
旋转转换器和转换器。仅提供本地资源时,解决方法是使用ResourceUrlProvider
直接(例如,通过自定义元素)和块。
请注意,当同时使用EncodedResourceResolver
(例如,Gzip、Brotli 编码)和VersionedResourceResolver
,它们必须按该顺序注册,以确保基于内容
版本始终根据未编码的文件可靠地计算。
对于 WebJars,版本化 URL (例如/webjars/jquery/1.2.0/jquery.min.js
是推荐且最有效的使用方式。
相关资源位置是使用 Spring Boot 开箱即用的(或者可以配置
手动通过ResourceHandlerRegistry
),并且不需要添加org.webjars:webjars-locator-core
Dependency。
无版本 URL,例如/webjars/jquery/jquery.min.js
通过WebJarsResourceResolver
当org.webjars:webjars-locator-core
库存在于类路径上,但代价是
类路径扫描可能会减慢应用程序启动速度。解析器可以将 URL 重写为
包括 jar 的版本,也可以与没有版本的传入 URL 进行匹配,例如,从/webjars/jquery/jquery.min.js
自/webjars/jquery/1.2.0/jquery.min.js
.
基于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 依赖于名为 Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,在 Spring MVC 中,我们也建议远离依赖它。 |
1.11.10. WebSocket服务
WebFlux Java 配置声明了WebSocketHandlerAdapter
bean 提供支持 WebSocket 处理程序的调用。这意味着剩下要做的就是为了处理 WebSocket 握手请求,只需将WebSocketHandler
到 URL 通过SimpleUrlHandlerMapping
.
在某些情况下,可能需要创建WebSocketHandlerAdapter
bean 与 提供WebSocketService
允许配置 WebSocket 服务器属性的服务。 例如:
@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 配置
-
检测并委托给
WebFluxConfigurer
实现来自定义该配置。
对于高级模式,您可以删除@EnableWebFlux
并直接从DelegatingWebFluxConfiguration
而不是实现WebFluxConfigurer
,
如以下示例所示:
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {
// ...
}
您可以将现有方法保留在WebConfig
,但您现在也可以覆盖 bean 声明
来自基类,并且仍然具有任意数量的其他WebMvcConfigurer
实现
类路径。
1.12. HTTP/2
Reactor Netty、Tomcat、Jetty 和 Undertow 支持 HTTP/2。但是,有 与服务器配置相关的注意事项。有关更多详细信息,请参阅 HTTP/2 wiki 页面。
2. 网络客户端
Spring WebFlux 包括一个用于执行 HTTP 请求的客户端。WebClient
有一个
基于 Reactor 的功能性、流畅的 API,请参阅响应式库,
它支持异步逻辑的声明性组合,而无需处理
线程或并发。它是完全无阻塞的,它支持流媒体,并依赖于
同样用于编码和
在服务器端解码请求和响应内容。
WebClient
需要一个 HTTP 客户端库来执行请求。有内置的
支持以下内容:
-
其他的可以通过
ClientHttpConnector
.
2.1. 配置
创建WebClient
是通过静态工厂方法之一:
-
WebClient.create()
-
WebClient.create(String baseUrl)
您还可以使用WebClient.builder()
还有更多选项:
-
uriBuilderFactory
:定制UriBuilderFactory
用作基本 URL。 -
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()
构建后,一个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. MaxInMemory大小
编解码器对缓冲数据有限制 内存以避免应用程序内存问题。默认情况下,这些设置为 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. 反应器内蒂
要自定义 Reactor Netty 设置,请提供预配置的HttpClient
:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
资源
默认情况下,HttpClient
参与全球Reactor Netty资源reactor.netty.http.HttpResources
,包括事件循环线程和连接池。
这是推荐的模式,因为固定的共享资源是事件循环的首选
并发。在此模式下,全局资源将保持活动状态,直到进程退出。
如果服务器与进程定时,则通常不需要显式
关闭。但是,如果服务器可以在进程内启动或停止(例如,Spring MVC
应用程序部署为 WAR),您可以声明类型为ReactorResourceFactory
跟globalResources=true
(默认值)以确保 Reactor
Netty 全局资源在 Spring 时关闭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
以下示例显示如何自定义 JettyHttpClient
设置:
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 客户端(和服务器)的多个实例之间共享资源,并且
确保在 SpringApplicationContext
由
声明类型为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. Http组件
以下示例演示如何自定义 Apache HttpComponentsHttpClient
设置:
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 响应会导致WebClientResponseException
包括
特定 HTTP 状态代码的子类。自定义错误处理
响应, 使用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. 交换
这exchangeToMono()
和exchangeToFlux()
方法(或awaitExchange { }
和exchangeToFlow { }
在 Kotlin 中)
对于需要更多控制的更高级情况很有用,例如以不同的方式解码响应
根据响应状态:
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()
}
}
使用上述时,返回后Mono
或Flux
completes,响应正文
被选中,如果未消耗,则将其释放以防止内存和连接泄漏。
因此,响应无法进一步在下游解码。这取决于提供的
函数来声明如何根据需要解码响应。
2.4. 请求正文
请求正文可以从由ReactiveAdapterRegistry
,
喜欢Mono
或 Kotlin 协程Deferred
如以下示例所示:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
还可以对对象流进行编码,如以下示例所示:
Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()
或者,如果您有实际值,则可以使用bodyValue
快捷方式,
如以下示例所示:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()
2.4.1. 表单数据
要发送表单数据,您可以提供MultiValueMap<String, String>
作为正文。请注意,content 会自动设置为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. 多部分数据
要发送多部分数据,您需要提供一个MultiValueMap<String, ?>
其值为 也Object
表示部件内容的实例或HttpEntity
表示内容的实例和headers 的组件。MultipartBodyBuilder
提供了一个方便的 API 来准备multipart 请求。以下示例展示了如何创建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()
在大多数情况下,您不必指定Content-Type
。内容类型是根据HttpMessageWriter
选择序列化它或者,在Resource
,基于文件扩展名。如有必要,您可以显式提供MediaType
通过重载的 架构工人part
方法。
一次MultiValueMap
准备好了,最简单的方法就是把它传递给WebClient
是
通过body
方法,如以下示例所示:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()
如果MultiValueMap
包含至少一种非String
value,这也可以
表示常规形式数据(即application/x-www-form-urlencoded
),您不必
将Content-Type
自multipart/form-data
.使用MultipartBodyBuilder
,这确保了HttpEntity
包装纸。
作为替代方案MultipartBodyBuilder
,还可以提供多部分内容,
inline-style,通过内置的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
是围绕过滤器链的薄立面,后跟一个ExchangeFunction
.它提供了一个工作流程来发出请求、与更高级别之间进行编码
level 对象,它有助于确保始终使用响应内容。
当过滤器以某种方式处理响应时,必须格外小心,始终使用
其内容或以其他方式将其传播到下游的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>()
请注意,您可以配置defaultRequest
callback 全局调用WebClient.Builder
级别,允许您将属性插入到所有请求中,
例如,可以在 Spring MVC 应用程序中使用它来填充
请求属性ThreadLocal
数据。
2.7. 上下文
属性提供了一种将信息传递给过滤器的便捷方法
链,但它们只影响当前请求。如果要传递信息
传播到嵌套的其他请求,例如通过flatMap
,或在之后执行,
例如通过concatMap
,那么你需要使用 ReactorContext
.
反应堆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()
}
但是,如果需要进行多个调用,则避免在每个调用上阻塞会更有效 响应,而是等待组合结果:
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
val data = runBlocking {
val personDeferred = async {
client.get().uri("/person/{id}", personId)
.retrieve().awaitBody<Person>()
}
val hobbiesDeferred = async {
client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
}
以上只是一个例子。还有很多其他的模式和运算符可用于放置 一起一个响应式管道,该管道进行许多远程调用,可能有一些嵌套的, 相互依存,直到最后都不会阻塞。
跟 |
2.9. 测试
要测试使用WebClient
,您可以使用模拟 Web 服务器,例如 OkHttp MockWebServer。查看示例
它的用途,请查看WebClientIntegrationTests
在 Spring Framework 测试套件或static-server
示例。
3. Web套接字
参考文档的这一部分涵盖了对响应式堆栈 WebSocket 的支持 消息。
3.1. WebSocket 简介
WebSocket 协议 RFC 6455 提供了一个标准化的 在客户端和服务器之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与 HTTP 不同的 TCP 协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重复使用现有防火墙规则。
WebSocket 交互从使用 HTTP 的 HTTP 请求开始Upgrade
页眉
升级,或者在这种情况下,切换到 WebSocket 协议。以下示例
显示了这样的交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1 | 这Upgrade 页眉。 |
2 | 使用Upgrade 连接。 |
支持 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 套接字将保留 打开,供客户端和服务器继续发送和接收消息。
WebSocket 工作原理的完整介绍超出了本文档的范围。 请参阅 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),通过Sec-WebSocket-Protocol
HTTP 握手请求上的标头。
如果没有这一点,他们需要制定自己的公约。
3.1.2. 何时使用 WebSockets
WebSockets 可以使网页具有动态性和交互性。然而,在许多情况下, Ajax 和 HTTP 流的组合或长轮询可以提供一个简单的 有效的解决方案。
例如,新闻、邮件和社交源需要动态更新,但可能是 每隔几分钟就这样做一次完全没问题。协作、游戏和金融应用程序,在 另一方面,需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 正是低延迟、高频和高音量的结合造就了最好的 使用 WebSocket 的案例。
还要记住,在互联网上,您无法控制的限制性代理
可能会阻止 WebSocket 交互,因为它们未配置为传递Upgrade
标头,或者因为它们关闭了显示为空闲的长期连接。这
意味着将 WebSocket 用于防火墙内的内部应用程序是
与面向公众的应用程序相比,直接做出决定。
3.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写客户端和 处理 WebSocket 消息的服务器端应用程序。
3.2.1. 服务器
要创建 WebSocket 服务器,您可以先创建一个WebSocketHandler
.
以下示例显示了如何执行此作:
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
public class MyWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
// ...
}
}
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}
然后,您可以将其映射到 URL:
@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 Config,则没有任何内容
进一步执行,否则,如果不使用 WebFlux 配置,则需要声明一个WebSocketHandlerAdapter
如下图所示:
@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@Configuration
class WebConfig {
// ...
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
3.2.2.WebSocketHandler
这handle
方法WebSocketHandler
需要WebSocketSession
并返回Mono<Void>
以指示会话的应用程序处理何时完成。会话已处理
通过两个流,一个用于入站消息,一个用于出站消息。下表
描述了处理流的两种方法:
WebSocketSession 方法 |
描述 |
---|---|
|
提供对入站消息流的访问,并在连接关闭时完成。 |
|
获取传出消息的源,写入消息,并返回 |
一个WebSocketHandler
必须将入站和出站流组合成一个统一的流,并且
返回一个Mono<Void>
这反映了该流程的完成。取决于应用
要求时,统一流将在以下情况下完成:
-
入站或出站消息流完成。
-
入站流完成(即连接关闭),而出站流是无限的。
-
在选定的点上,通过
close
方法WebSocketSession
.
当入站和出站消息流组合在一起时,无需 检查连接是否打开,因为反应流表示活动结束。 入站流接收完成或错误信号,出站流 接收取消信号。
处理程序最基本的实现是处理入站流的实现。这 以下示例显示了这样的实现:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() (1)
.doOnNext(message -> {
// ... (2)
})
.concatMap(message -> {
// ... (3)
})
.then(); (4)
}
}
1 | 访问入站消息流。 |
2 | 对每条消息做一些事情。 |
3 | 执行使用消息内容的嵌套异步作。 |
4 | 返回一个Mono<Void> 当接收完成时完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive() (1)
.doOnNext {
// ... (2)
}
.concatMap {
// ... (3)
}
.then() (4)
}
}
1 | 访问入站消息流。 |
2 | 对每条消息做一些事情。 |
3 | 执行使用消息内容的嵌套异步作。 |
4 | 返回一个Mono<Void> 当接收完成时完成。 |
对于嵌套的异步作,您可能需要调用message.retain() 在基础上
使用池数据缓冲区的服务器(例如 Netty)。否则,数据缓冲区可能是
在您有机会读取数据之前发布。有关更多背景信息,请参阅数据缓冲区和编解码器。 |
以下实现结合了入站和出站流:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive() (1)
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.map(value -> session.textMessage("Echo " + value)); (2)
return session.send(output); (3)
}
}
1 | 处理入站消息流。 |
2 | 创建出站消息,生成组合流。 |
3 | 返回一个Mono<Void> 当我们继续接收时,这还没有完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val output = session.receive() (1)
.doOnNext {
// ...
}
.concatMap {
// ...
}
.map { session.textMessage("Echo $it") } (2)
return session.send(output) (3)
}
}
1 | 处理入站消息流。 |
2 | 创建出站消息,生成组合流。 |
3 | 返回一个Mono<Void> 当我们继续接收时,这还没有完成。 |
入站和出站流可以是独立的,并且只能在完成时加入, 如以下示例所示:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
Mono<Void> input = session.receive() (1)
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.then();
Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage)); (2)
return Mono.zip(input, output).then(); (3)
}
}
1 | 处理入站消息流。 |
2 | 发送外发消息。 |
3 | 加入流并返回Mono<Void> 当任一流结束时完成。 |
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val input = session.receive() (1)
.doOnNext {
// ...
}
.concatMap {
// ...
}
.then()
val source: Flux<String> = ...
val output = session.send(source.map(session::textMessage)) (2)
return Mono.zip(input, output).then() (3)
}
}
1 | 处理入站消息流。 |
2 | 发送外发消息。 |
3 | 加入流并返回Mono<Void> 当任一流结束时完成。 |
3.2.3.DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。的 Spring Core 部分
该参考资料在有关数据缓冲区和编解码器的部分中有更多相关内容。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区被池化并对引用进行计数,并且必须释放
使用时以避免内存泄漏。
在 Netty 上运行时,应用程序必须使用DataBufferUtils.retain(dataBuffer)
如果他们
希望保留输入数据缓冲区以确保它们不会被释放,并且
随后使用DataBufferUtils.release(dataBuffer)
当缓冲区被消耗时。
3.2.4. 握手
WebSocketHandlerAdapter
委托给WebSocketService
.默认情况下,这是一个实例
之HandshakeWebSocketService
,它对 WebSocket 请求执行基本检查,并且
然后使用RequestUpgradeStrategy
对于正在使用的服务器。目前,有内置的
支持 Reactor Netty、Tomcat、Jetty 和 Undertow。
HandshakeWebSocketService
公开一个sessionAttributePredicate
允许
设置一个Predicate<String>
从WebSession
并插入它们
转换为WebSocketSession
.
3.2.5. 服务器配置
这RequestUpgradeStrategy
对于每个服务器,会公开特定于
底层 WebSocket 服务器引擎。使用 WebFlux Java 配置时,您可以自定义
WebFlux 配置的相应部分中显示的此类属性,或者如果
不使用 WebFlux 配置,请使用以下内容:
@Configuration
class WebConfig {
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter(webSocketService());
}
@Bean
public WebSocketService webSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
@Configuration
class WebConfig {
@Bean
fun handlerAdapter() =
WebSocketHandlerAdapter(webSocketService())
@Bean
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
检查服务器的升级策略,查看可用的选项。现在 只有 Tomcat 和 Jetty 公开了这样的选项。
3.2.6. CORS
配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是
拥有您的WebSocketHandler
实现CorsConfigurationSource
并返回一个CorsConfiguration
包含允许的来源、标头和其他详细信息。如果你不能
这样,您还可以将corsConfigurations
属性SimpleUrlHandler
自
通过 URL 模式指定 CORS 设置。如果同时指定了两者,则使用combine
方法CorsConfiguration
.
3.2.7. 客户端
Spring WebFlux 提供了一个WebSocketClient
抽象与实现
Reactor Netty、Tomcat、Jetty、Undertow 和标准 Java(即 JSR-356)。
Tomcat 客户端实际上是标准 Java 客户端的扩展,但有一些额外的功能
功能WebSocketSession 处理以利用特定于 Tomcat 的
API 用于暂停接收消息以进行背压。 |
要启动 WebSocket 会话,您可以创建客户端的实例并使用其execute
方法:
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
session.receive()
.doOnNext(::println)
.then()
}
一些客户端,例如 Jetty,实现Lifecycle
需要停止和启动
在您可以使用它们之前。所有客户端都有与配置相关的构造函数选项
底层 WebSocket 客户端的。
4. 测试
这spring-test
模块提供了ServerHttpRequest
,ServerHttpResponse
和ServerWebExchange
.
请参阅 Spring Web Reactive 的
模拟对象的讨论。
WebTestClient
基于这些模拟请求和
响应对象,以支持在没有 HTTP 的情况下测试 WebFlux 应用程序
服务器。您可以使用WebTestClient
也适用于端到端集成测试。
5. RS袜子
本节介绍 Spring Framework 对 RSocket 协议的支持。
5.1. 概述
RSocket 是一种用于通过 TCP 进行多路复用、双工通信的应用协议, WebSocket 和其他字节流传输,使用以下交互之一 模型:
-
Request-Response
— 发送一条消息并回复一条。 -
Request-Stream
— 发送一条消息并接收一连串消息。 -
Channel
— 双向发送消息流。 -
Fire-and-Forget
— 发送单向消息。
建立初始连接后,“客户端”与“服务器”的区别将丢失,因为 双方变得对称,双方都可以发起上述交互之一。 这就是为什么在协议中将参与方称为“请求者”和“响应者” 而上述交互称为“请求流”或简称为“请求”。
以下是 RSocket 协议的主要功能和优点:
-
跨网络边界的响应式流语义 - 用于流式请求,例如
Request-Stream
和Channel
、背压信号 在请求者和响应者之间移动,允许请求者减慢响应者的速度 源,从而减少对网络层拥塞控制的依赖,以及需求 用于网络级别或任何级别的缓冲。 -
请求限制 — 此功能在
LEASE
框架 可以从两端发送,以限制另一端允许的请求总数 在给定的时间内。租约会定期续签。 -
会话恢复 — 这是为失去连接而设计的,需要一些状态 待维护。状态管理对应用程序是透明的,并且运行良好 结合背压,可以在可能的情况下停止生产者并降低 所需的状态量。
-
大消息的碎片化和重组。
-
Keepalive(心跳)。
RSocket 有多种语言的实现。Java 库基于 Project Reactor 构建, 和用于运输的 Reactor Netty。这意味着 来自应用程序中响应式流发布者的信号透明传播 通过网络中的 RSocket。
5.1.1. 协议
RSocket 的好处之一是它在线路上具有明确定义的行为,并且 易于阅读的规范以及一些协议扩展。因此,它是 阅读规范是个好主意,独立于语言实现和更高级别 框架 API。本节提供了一个简洁的概述,以建立一些上下文。
连接
最初,客户端通过一些低级流传输连接到服务器,例如
作为 TCP 或 WebSocket 并发送一个SETUP
frame 设置
连接。
服务器可能会拒绝SETUP
帧,但通常是在发送后(对于客户端)
和 received(对于服务器),双方都可以开始发出请求,除非SETUP
指示使用租赁语义来限制请求数,在这种情况下
双方都必须等待LEASE
框架从另一端允许提出请求。
提出请求
一旦建立连接,双方都可以通过其中一个
框架REQUEST_RESPONSE
,REQUEST_STREAM
,REQUEST_CHANNEL
或REQUEST_FNF
.每个
这些帧将一条消息从请求者传送到响应者。
然后,响应者可能会返回PAYLOAD
带有响应消息的帧,并且在
之REQUEST_CHANNEL
请求者还可以发送PAYLOAD
要求较多的框架
消息。
当请求涉及消息流时,例如Request-Stream
和Channel
,
响应方必须尊重来自请求方的请求信号。需求表示为
消息数。初始需求在REQUEST_STREAM
和REQUEST_CHANNEL
框架。后续需求通过REQUEST_N
框架。
每一方还可以通过METADATA_PUSH
框架,不
与任何个人请求有关,而是与整个连接有关。
消息格式
RSocket 消息包含数据和元数据。元数据可用于发送路由、安全Tokens等。数据和元数据的格式可以不同。每个的 MIME 类型在SETUP
框架并应用于给定连接上的所有请求。
虽然所有消息都可以有元数据,但通常元数据(如路由)是按请求的
因此仅包含在请求的第一条消息中,即其中一个帧REQUEST_RESPONSE
,REQUEST_STREAM
,REQUEST_CHANNEL
或REQUEST_FNF
.
协议扩展定义了用于应用程序的通用元数据格式:
5.1.2. Java 实现
RSocket 的 Java 实现基于 Project Reactor 构建。TCP 和 WebSocket 的传输是
基于 Reactor Netty 构建。作为响应式流
库,Reactor 简化了实现协议的工作。对于应用,它是
使用起来很自然Flux
和Mono
带有声明性运算符和透明背面
压力支持。
RSocket Java 中的 API 是故意最小和基本的。它侧重于协议 功能,并将应用程序编程模型(例如 RPC codegen 与其他 更高层次,独立关注。
主合约 io.rsocket.RSocket 对四种请求交互类型进行建模Mono
表示
单消息,Flux
消息流,以及io.rsocket.Payload
实际的
消息,可以作为字节缓冲区访问数据和元数据。这RSocket
使用合约
对称。对于请求,应用程序会被赋予一个RSocket
执行
请求与。为了响应,应用程序实现RSocket
来处理请求。
这并不是一个彻底的介绍。在大多数情况下,Spring 应用程序 将不必直接使用其 API。但是,查看或尝试可能很重要 RSocket 独立于 Spring。RSocket Java 存储库包含许多示例应用程序,这些应用程序 演示其 API 和协议功能。
5.1.3. 弹簧支撑
这spring-messaging
模块包含以下内容:
-
RSocketRequester — 流畅的 API,用于通过
io.rsocket.RSocket
具有数据和元数据编码/解码功能。 -
带注释的响应者 —
@MessageMapping
带注释的处理程序方法 响应。
这spring-web
模块包含Encoder
和Decoder
Jackson 等实现
RSocket 应用程序可能需要的 CBOR/JSON 和 Protobuf。它还包含PathPatternParser
可以插入以实现高效的路由匹配。
Spring Boot 2.2 支持通过 TCP 或 WebSocket 建立 RSocket 服务器,包括
在 WebFlux 服务器中通过 WebSocket 公开 RSocket 的选项。还有客户端
支持和自动配置RSocketRequester.Builder
和RSocketStrategies
.
有关更多详细信息,请参阅 Spring Boot 参考中的 RSocket 部分。
Spring Security 5.2 提供 RSocket 支持。
Spring Integration 5.2 提供了入站和出站网关来与 RSocket 交互客户端和服务器。有关更多详细信息,请参阅 Spring Integration 参考手册。
Spring Cloud Gateway 支持 RSocket 连接。
5.2. RSocket请求者
RSocketRequester
提供了一个流畅的 API 来执行 RSocket 请求,接受和
返回数据和元数据的对象,而不是低级数据缓冲区。它可以使用
对称地,从客户端发出请求,从服务器发出请求。
5.2.1. 客户端请求者
要获取RSocketRequester
在客户端是连接到一个服务器,其中涉及
发送 RSocketSETUP
带有连接设置的框架。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 类型派生自第一个配置的Decoder
.为
metadata,默认 MIME 类型是复合元数据,它允许多个
元数据值和 MIME 类型对。通常,两者都不需要更改。
数据和元数据中的SETUP
框架是可选的。在服务器端,可以使用@ConnectMapping方法来处理
连接和SETUP
框架。元数据可用于连接
级别安全。
策略
RSocketRequester.Builder
接受RSocketStrategies
以配置请求者。
您需要使用它来提供编码器和解码器,用于数据的 (反) 序列化,并且
元数据值。默认情况下,只有来自spring-core
为String
,byte[]
和ByteBuffer
已注册。添加spring-web
提供对更多
可以按以下方式注册:
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
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 | 从类创建响应者@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 | 从类创建响应者@MessageMapping 和/或@ConnectMapping 方法。 |
3 | 注册响应者。 |
注意,以上只是为客户端编程注册而设计的快捷方式 反应。 对于替代场景,客户端响应器处于 Spring 配置中,您仍然可以声明RSocketMessageHandler
作为 Spring bean,然后按如下方式应用:
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.tcp("localhost", 7000);
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)
对于上述内容,您可能还需要使用setHandlerPredicate
在RSocketMessageHandler
自 切换到不同的策略来检测客户端响应者,例如基于自定义注释,例如@RSocketClientResponder
与默认@Controller
. 这 在客户端和服务器的场景中,或多个客户端在同一个场景中是必需的 应用。
另请参阅带注释的响应器,了解有关编程模型的更多信息。
高深
RSocketRequesterBuilder
提供回调来公开底层io.rsocket.core.RSocketConnector
有关 keepalive 的更多配置选项间隔、会话恢复、拦截器等。您可以配置选项在该级别,如下所示:
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.tcp("localhost", 7000);
val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}
.tcp("localhost", 7000)
5.2.2. 服务器请求者
要从服务器向连接的客户端发出请求,只需获取 来自服务器的连接客户端的请求者。
在带注释的响应器中,@ConnectMapping
和@MessageMapping
方法支持RSocketRequester
论点。使用它来访问连接的请求者。保持在内
请注意@ConnectMapping
方法本质上是SETUP
框架,其中
必须先处理请求,然后才能开始请求。因此,一开始的请求必须是
与处理分离。例如:
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { (1)
// ...
});
return ... (2)
}
1 | 异步启动请求,独立于处理。 |
2 | 执行处理和退货完成Mono<Void> . |
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
// ...
}
}
/// ... (2)
}
1 | 异步启动请求,独立于处理。 |
2 | 在挂起功能中执行处理。 |
5.2.3. 请求
ViewBox viewBox = ... ;
Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlux(AirportLocation.class); (3)
1 | 指定要包含在请求消息元数据中的路由。 |
2 | 为请求消息提供数据。 |
3 | 声明预期响应。 |
val viewBox: ViewBox = ...
val locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlow<AirportLocation>() (3)
1 | 指定要包含在请求消息元数据中的路由。 |
2 | 为请求消息提供数据。 |
3 | 声明预期响应。 |
交互类型是根据输入的基数隐式确定的,并且
输出。上面的例子是一个Request-Stream
因为发送了一个值和一个流
的值被接收。在大多数情况下,只要
输入和输出的选择与 RSocket 交互类型以及输入和
响应者预期的输出。无效组合的唯一示例是多对一。
这data(Object)
方法也接受任何反应流Publisher
包括Flux
和Mono
,以及在ReactiveAdapterRegistry
.对于多值Publisher
如Flux
这会产生
相同类型的值,请考虑使用重载的data
避免有
类型检查和Encoder
查找每个元素:
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
这data(Object)
step 是可选的。对于不发送数据的请求,请跳过它:
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 响应器可以实现为@MessageMapping
和@ConnectMapping
方法。@MessageMapping
方法处理单个请求,而@ConnectMapping
方法 处理
连接级事件(设置和元数据推送)。支持带注释的响应者
对称地,用于从服务器端响应和从客户端响应。
5.3.1. 服务器响应器
要在服务器端使用带注释的响应器,请将RSocketMessageHandler
到你的Spring
要检测的配置@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
用于编解码器实现的模块。
默认情况下SimpleRouteMatcher
用于匹配路由AntPathMatcher
.
我们建议插入PathPatternRouteMatcher
从spring-web
为
高效的路由匹配。RSocket 路由可以是分层的,但不是 URL 路径。
默认情况下,两个路由匹配器都配置为使用“.”作为分隔符,并且没有 URL
与 HTTP URL 一样解码。
RSocketMessageHandler
可以通过RSocketStrategies
如果出现以下情况,这可能会有用
您需要在同一进程中在客户端和服务器之间共享配置:
@Configuration
static class ServerConfig {
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.setRSocketStrategies(rsocketStrategies());
return handler;
}
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.routeMatcher(new PathPatternRouteMatcher())
.build();
}
}
@Configuration
class ServerConfig {
@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
rSocketStrategies = rsocketStrategies()
}
@Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.routeMatcher(PathPatternRouteMatcher())
.build()
}
5.3.2. 客户端响应者
客户端的带注释的响应器需要在RSocketRequester.Builder
.有关详细信息,请参阅客户端响应程序。
5.3.3. @MessageMapping
@Controller
public class RadarsController {
@MessageMapping("locate.radars.within")
public Flux<AirportLocation> radars(MapRequest request) {
// ...
}
}
@Controller
class RadarsController {
@MessageMapping("locate.radars.within")
fun radars(request: MapRequest): Flow<AirportLocation> {
// ...
}
}
以上@MessageMapping
方法响应具有
路由“locate.radars.within”。它支持灵活的方法签名,并可选择
使用以下方法参数:
方法参数 | 描述 |
---|---|
|
请求的有效负载。这可以是异步类型的具体值,例如 注意:注释的使用是可选的。不是简单类型的方法参数 并且不是任何其他受支持的参数,则假定为预期有效负载。 |
|
向远程端发出请求的请求者。 |
|
根据映射模式中的变量从路由中提取的值,例如 |
|
按照 MetadataExtractor 中所述注册为提取的元数据值。 |
|
注册提取的所有元数据值,如 MetadataExtractor 中所述。 |
返回值应是一个或多个要序列化为响应的对象
负载。这可以是异步类型,例如Mono
或Flux
、具体值或
也void
或无值异步类型,例如Mono<Void>
.
RSocket 交互类型,即@MessageMapping
方法支座由
输入的基数(即@Payload
参数)和输出,其中
基数表示以下内容:
基数 | 描述 |
---|---|
1 |
显式值或单值异步类型,例如 |
多 |
多值异步类型,例如 |
0 |
对于输入,这意味着该方法没有 对于输出,这是 |
下表显示了所有输入和输出基数组合以及相应的 交互类型:
输入基数 | 输出基数 | 交互类型 |
---|---|---|
0, 1 |
0 |
即发即忘,请求-响应 |
0, 1 |
1 |
请求-响应 |
0, 1 |
多 |
请求流 |
多 |
0、1、多 |
请求通道 |
5.3.4. @ConnectMapping
@ConnectMapping
处理SETUP
frame,以及 RSocket 连接开始时的帧,以及
任何后续元数据推送通知通过METADATA_PUSH
框架,即metadataPush(Payload)
在io.rsocket.RSocket
.
@ConnectMapping
方法支持与 @MessageMapping 相同的参数,但基于元数据和来自SETUP
和METADATA_PUSH
框架。@ConnectMapping
可以有一个模式来缩小处理范围
元数据中具有路由的特定连接,或者未声明任何模式
则所有连接都匹配。
@ConnectMapping
方法不能返回数据,必须使用void
或Mono<Void>
作为返回值。如果处理返回新
连接,则连接被拒绝。不得将处理搁置以使
请求RSocketRequester
连接。有关详细信息,请参阅服务器请求器。
5.4. 元数据提取器
响应者必须解释元数据。复合元数据允许独立 格式化的元数据值(例如用于路由、安全、跟踪),每个值都有自己的 MIME 类型。应用程序需要一种方法来配置要支持的元数据 MIME 类型,以及一种方法 以访问提取的值。
MetadataExtractor
是一个获取序列化元数据并返回解码的合约
name-value 对,然后可以像标头一样按名称访问,例如通过@Header
在带注释的处理程序方法中。
DefaultMetadataExtractor
可以给Decoder
实例来解码元数据。出
它内置了对“message/x.rsocket.routing.v0”的支持,它解码到String
并保存在“路由”键下。对于您需要提供的任何其他 MIME 类型
一个Decoder
并注册 MIME 类型,如下所示:
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
复合元数据可以很好地组合独立的元数据值。然而,
请求者可能不支持复合元数据,或者可能选择不使用它。为此,DefaultMetadataExtractor
可能需要自定义逻辑来将解码值映射到输出
地图。以下是将 JSON 用于元数据的示例:
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
MimeType.valueOf("application/vnd.myapp.metadata+json"),
new ParameterizedTypeReference<Map<String,String>>() {},
(jsonMap, outputMap) -> {
outputMap.putAll(jsonMap);
});
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
outputMap.putAll(jsonMap)
}
配置时MetadataExtractor
通过RSocketStrategies
,你可以让RSocketStrategies.Builder
使用配置的解码器创建提取器,以及
只需使用回调即可自定义注册,如下所示:
RSocketStrategies strategies = RSocketStrategies.builder()
.metadataExtractorRegistry(registry -> {
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
// ...
})
.build();
import org.springframework.messaging.rsocket.metadataToExtract
val strategies = RSocketStrategies.builder()
.metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
registry.metadataToExtract<Foo>(fooMimeType, "foo")
// ...
}
.build()
6. 响应式库
spring-webflux
取决于reactor-core
并在内部使用它来组合异步
逻辑并提供响应式流支持。通常,WebFlux API 返回Flux
或Mono
(因为这些是在内部使用的)并宽容地接受任何反应流Publisher
实现作为输入。使用Flux
对Mono
很重要,因为
它有助于表达基数——例如,无论是单个还是多个异步
值是预期的,这对于做出决策至关重要(例如,当
编码或解码 HTTP 消息)。
对于带注释的控制器,WebFlux 透明地适应选择的响应式库由应用程序。这是在ReactiveAdapterRegistry
它为响应式库和其他异步类型提供可插拔支持。该注册表内置了对 RxJava 3、Kotlin 协程和 SmallRye Mutiny 的支持,但您也可以注册其他第三方适配器。
从 Spring Framework 5.3.11 开始,对 RxJava 1 和 2 的支持已弃用,遵循RxJava 自己的 EOL 建议和对 RxJava 3 的升级建议。 |
对于功能 API(例如功能端点,则WebClient
等),一般
适用于 WebFlux API 的规则 —Flux
和Mono
作为返回值和响应式流Publisher
作为输入。当Publisher
,无论是自定义的还是来自其他响应式库,
,则只能将其视为语义未知 (0..N) 的流。但是,如果
语义是已知的,你可以用Flux
或Mono.from(Publisher)
相反
传递原始的Publisher
.
例如,给定一个Publisher
这不是Mono
,Jackson JSON 消息编写器
期望多个值。如果媒体类型暗示无限流(例如,application/json+stream
),值被单独写入和刷新。否则
值被缓冲到列表中并呈现为 JSON 数组。