Web 响应式
1. Spring WebFlux
Spring 框架中包含的原始 Web 框架 Spring Web MVC 是 专为 Servlet API 和 Servlet 容器构建。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版后期添加的。它是完全非阻塞的,支持 Reactive Streams 背压,并运行在 Netty、Undertow 和 Servlet 3.1+ 容器。
这两个 Web 框架都镜像其源模块的名称
(spring-webmvc 和 spring-webflux)
并在 Spring 框架中并存。每个模块都是可选的。
应用程序可以使用一个或另一个模块,或者在某些情况下,同时使用两个模块——例如,带有反应式WebClient
.
1.1. 概述
为什么创建 Spring WebFlux?
部分答案是需要一个非阻塞 Web 堆栈来处理并发
线程数量少,硬件资源较少。Servlet 3.1 确实提供了
用于非阻塞 I/O 的 API。但是,使用它会导致 Servlet API 的其余部分
其中 Contract 是同步的 (Filter
,Servlet
) 或阻止 (getParameter
,getPart
).这就是一个新的通用 API 作为
任何非阻塞运行时。这很重要,因为服务器(例如 Netty)是
在异步、非阻塞空间中建立良好。
答案的另一部分是函数式编程。就像添加注释一样
在 Java 5 中创建的机会(例如带注释的 REST 控制器或单元测试)中,添加的
的 lambda 表达式为 Java 中的功能性 API 创造了机会。
这对于非阻塞应用程序和延续式 API(如流行的
由CompletableFuture
和 ReactiveX),它们允许声明式
异步逻辑的组成。在编程模型级别,Java 8 启用了 Spring
WebFlux 提供功能性 Web 端点以及带注释的控制器。
1.1.1. 定义 “Reactive”
我们谈到了“非阻塞”和“函数式”,但响应式是什么意思?
术语“反应式”是指围绕响应变化而构建的编程模型 — 网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。 从这个意义上说,非阻塞是反应性的,因为我们现在处于模式中,而不是被阻塞 在作完成或数据可用时对通知做出反应。
我们 Spring 团队还有另一个重要的机制与 “reactive” 相关联 那就是非阻塞背压。在同步的命令式代码中,阻塞调用 作为一种自然形式的背压,迫使呼叫者等待。在非阻塞 代码中,控制事件的速率变得很重要,这样快速生产者就不会 压倒它的目的地。
Reactive Streams 是一个小规范(在 Java 9 中也采用了) 它定义了异步组件与背压之间的交互。 例如,数据存储库(充当 Publisher) 可以生成 HTTP 服务器(充当订阅者)的数据 然后可以写入响应。Reactive Streams 的主要目的是让 subscriber 控制发布服务器生成数据的速度或速度。
常见问题:如果出版商不能放慢速度怎么办? Reactive Streams 的目的只是建立机制和边界。 如果发布者无法放慢速度,则必须决定是缓冲、丢弃还是失败。 |
1.1.2. 响应式 API
Reactive Streams 在互作性方面起着重要作用。图书馆对此感兴趣
和基础设施组件,但作为应用程序 API 的用处不大,因为它太
低级。应用程序需要更高级别、更丰富的功能 API 来
compose async logic — 类似于 Java 8Stream
API 的 API 中,但不仅仅是集合。
这就是响应式库所扮演的角色。
Reactor 是
Spring WebFlux 的 Web Flux 中。它提供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 或其他反应式库。有关更多详细信息,请参阅 Reactive Libraries 。
除了反应式 API 之外,WebFlux 还可以与 Kotlin 中的协程 API 一起使用,后者提供了更命令式的编程风格。 以下 Kotlin 代码示例将随协程 API 一起提供。 |
1.1.3. 对模型进行编程
这spring-web
模块包含作为 Spring WebFlux 基础的反应式基础,
包括 HTTP 抽象、支持的 Reactive Streams 适配器
服务器、编解码器和内核WebHandler
应用程序接口可比较
Servlet API,但具有非阻塞 Contract。
在此基础上, 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. 服务器
Spring WebFlux 在 Tomcat、Jetty、Servlet 3.1+ 容器以及 非 Servlet 运行时,例如 Netty 和 Undertow。所有服务器都适用于低级通用 API,因此可以跨服务器支持更高级别的编程模型。
Spring WebFlux 没有内置支持来启动或停止服务器。然而,事实确实如此 从 Spring 配置和 WebFlux 基础设施轻松组装应用程序,并使用几个 代码行。
Spring Boot 有一个 WebFlux Starters,可以自动执行这些步骤。默认情况下,Starters使用 Netty,但通过更改您的 Maven 或 Gradle 依赖项。Spring Boot 默认为 Netty,因为它更广泛 在异步、非阻塞空间中使用,并允许客户端和服务器共享资源。
Tomcat 和 Jetty 可以与 Spring MVC 和 WebFlux 一起使用。但是请记住, 它们的使用方式非常不同。Spring MVC 依赖于 Servlet 阻塞 I/O 和 允许应用程序在需要时直接使用 Servlet API。Spring WebFlux 依赖于 Servlet 3.1 非阻塞 I/O,并在底层使用 Servlet API 适配器。它不暴露在外,直接使用。
对于 Undertow,Spring WebFlux 直接使用 Undertow API,而不使用 Servlet API。
1.1.6. 性能
性能具有许多特征和含义。通常是反应式和非阻塞的
不要使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用WebClient
以并行运行远程调用)。总的来说,它需要做更多的工作
事情以非阻塞方式进行,这可能会略微增加所需的处理时间。
响应式和非阻塞性的主要预期好处是能够使用小型 固定线程数和较少的内存。这使得应用程序在负载下更具弹性, 因为它们以更可预测的方式扩展。但是,为了观察这些好处,您需要 需要有一些延迟(包括缓慢且不可预测的网络 I/O 的组合)。 这就是响应式堆栈开始显示其优势的地方,差异可能是 戏剧性的。
1.1.7. 并发模型
Spring MVC 和 Spring WebFlux 都支持带注释的控制器,但有一个键 并发模型以及阻塞和线程的默认假设的差异。
在 Spring MVC(以及一般的 servlet 应用程序)中,假定应用程序可以 阻止当前线程(例如,用于远程调用)。因此,Servlet 容器 使用大型线程池来吸收请求处理过程中的潜在阻塞。
在 Spring WebFlux(以及一般的非阻塞服务器)中,假定应用程序 不要阻止。因此,非阻塞服务器使用小型的固定大小的线程池 (事件循环工作程序)来处理请求。
“To scale” 和 “small number of thread” 听起来可能很矛盾,但永远不要阻止 current thread (并依赖于回调) 意味着您不需要额外的线程,因为 没有阻塞调用需要吸收。 |
如果您确实需要使用阻塞库怎么办?Reactor 和 RxJava 都提供了publishOn
运算符继续在不同的线程上处理。这意味着有一个
轻松逃生舱口。但请记住,阻止 API 并不适合
this concurrency model 的
在 Reactor 和 RxJava 中,您可以通过运算符声明逻辑。在运行时,响应式 在不同的阶段按顺序处理数据的地方形成管道。主要优势 这是因为它使应用程序不必保护可变状态,因为 该管道中的应用程序代码永远不会并发调用。
您应该期望在运行 Spring WebFlux 的服务器上看到哪些线程?
-
在“vanilla”Spring WebFlux 服务器上(例如,没有数据访问或其他可选 dependencies),您可以期望服务器有一个线程,而 request 则有其他几个线程 处理(通常与 CPU 内核的数量一样多)。但是,Servlet 容器 可以从更多线程开始(例如,Tomcat 上的 10 个),以支持 servlet(阻塞)I/O 以及 servlet 3.1(非阻塞)I/O 使用情况。
-
反应式
WebClient
以 Event Loop 样式运行。所以你可以看到一个小的、固定的 与该 ID 相关的处理线程数(例如reactor-http-nio-
使用 Reactor Netty 连接器)。但是,如果 Reactor Netty 同时用于客户端和服务器,则这两个 默认情况下共享事件循环资源。 -
Reactor 和 RxJava 提供了线程池抽象,称为调度程序,可与
publishOn
运算符,用于将处理切换到其他线程池。 计划程序的名称表示特定的并发策略,例如,“parallel” (对于具有有限线程数的 CPU 绑定工作)或 “elastic” (对于 I/O 绑定工作 大量线程)。如果您看到这样的线程,则表示某些代码正在使用 特定线程池Scheduler
策略。 -
数据访问库和其他第三方依赖项也可以创建和使用线程 他们自己的。
1.2. 响应式核心
这spring-web
module 包含对反应式 Web 的以下基本支持
应用:
-
对于服务器请求处理,有两个级别的支持。
-
HttpHandler:HTTP 请求处理的基本协定 非阻塞 I/O 和反应流背压,以及用于 Reactor Netty 的适配器, Undertow、Tomcat、Jetty 和任何 Servlet 3.1+ 容器。
-
WebHandler
应用程序接口:稍高级的通用 Web API 请求处理,除此之外,还有具体的编程模型,如 Annotated 构建控制器和功能端点。
-
-
对于客户端,有一个基本的
ClientHttpConnector
contract 来执行 HTTP 具有非阻塞 I/O 和反应流背压的请求,以及用于 Reactor Netty 和反应式 Jetty HttpClient 的适配器。 应用程序中使用的更高级别的 WebClient 建立在这个基本契约之上。 -
对于客户端和服务器,用于序列化的编解码器和 HTTP 请求和响应内容的反序列化。
1.2.1.HttpHandler
HttpHandler 是一个简单的协定,具有处理请求和响应的单一方法。是的 有意最小化,其主要且唯一目的是成为最小抽象 通过不同的 HTTP 服务器 API 进行。
下表描述了支持的服务器 API:
服务器名称 | 使用的服务器 API | Reactive Streams 支持 |
---|---|---|
网 |
Netty API |
|
Undertow |
Undertow API |
spring-web: Undertow 到 Reactive Streams 桥 |
Tomcat |
Servlet 3.1 非阻塞 I/O;Tomcat API 读取和写入 ByteBuffers 与 byte[] |
spring-web:Servlet 3.1 非阻塞 I/O 到 Reactive Streams 桥 |
Jetty |
Servlet 3.1 非阻塞 I/O;Jetty API 写入 ByteBuffers 与 byte[] |
spring-web:Servlet 3.1 非阻塞 I/O 到 Reactive Streams 桥 |
Servlet 3.1 容器 |
Servlet 3.1 非阻塞 I/O |
spring-web:Servlet 3.1 非阻塞 I/O 到 Reactive Streams 桥 |
下表描述了服务器依赖项(另请参阅支持的版本):
服务器名称 | 组 ID | 项目名称 |
---|---|---|
Reactor Netty |
io.projectreactor.netty |
反应器-NETTY |
Undertow |
io.undertow |
undertow-core |
Tomcat |
org.apache.tomcat.embed |
tomcat-embed-core |
Jetty |
org.eclipse.jetty 网站 |
jetty 服务器、jetty-servlet |
下面的代码片段显示了如何使用HttpHandler
adapters 的 API 中:
Reactor Netty
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
Tomcat
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
Jetty
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
Servlet 3.1+ 容器
要作为 WAR 部署到任何 Servlet 3.1+ 容器,您可以扩展并包含AbstractReactiveWebInitializer
在战争中。该类将HttpHandler
跟ServletHttpHandlerAdapter
和寄存器
那作为一个Servlet
.
1.2.2.WebHandler
应用程序接口
这org.springframework.web.server
软件包构建在HttpHandler
合同
提供通用的 Web API,用于通过多个WebExceptionHandler
倍数WebFilter
和单个WebHandler
元件。链条可以
与WebHttpHandlerBuilder
通过简单地指向 SpringApplicationContext
自动检测组件,和/或通过注册组件
与构建器。
而HttpHandler
有一个简单的目标,即抽象出不同 HTTP 服务器的使用,WebHandler
API 旨在提供 Web 应用程序中常用的更广泛的功能集
如:
-
具有属性的用户会话。
-
请求属性。
-
解决
Locale
或Principal
对于请求。 -
访问已解析和缓存的表单数据。
-
多部分数据的抽象。
-
和更多..
特殊 bean 类型
下表列出了WebHttpHandlerBuilder
可以在
Spring ApplicationContext,或者可以直接使用它注册:
Bean 名称 | Bean 类型 | 计数 | 描述 |
---|---|---|---|
<任意> |
|
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
.目前,Synchronoss NIO Multipart 是唯一的
支持第三方库,并且是我们所知道的唯一一个用于非阻塞解析的库
multipart 请求。它是通过ServerCodecConfigurer
豆
(请参阅 Web 处理程序 API)。
要以流式处理方式解析多部分数据,您可以使用Flux<Part>
从HttpMessageReader<Part>
相反。例如,在带注解的控制器中,使用@RequestPart
意味 着Map
类访问各个部分,因此需要
完整解析多部分数据。相比之下,您可以使用@RequestBody
要解码
content 设置为Flux<Part>
而不收集到MultiValueMap
.
请求头转发
当请求通过代理(例如负载均衡器)时,主机、端口和 方案可能会发生变化。从客户的角度来看,这使得创建指向正确 host、port 和 scheme。
RFC 7239 定义了Forwarded
HTTP 标头
代理可用于提供有关原始请求的信息。还有其他
非标准标头,包括X-Forwarded-Host
,X-Forwarded-Port
,X-Forwarded-Proto
,X-Forwarded-Ssl
和X-Forwarded-Prefix
.
ForwardedHeaderTransformer
是一个组件,用于修改
请求,然后删除这些标头。如果您声明
it 作为名为forwardedHeaderTransformer
,它将被检测并使用。
转发的 Headers 存在安全注意事项,因为应用程序无法知道
标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么
应将信任边界的代理配置为删除传入的不受信任的转发流量
从外面。您还可以配置ForwardedHeaderTransformer
跟removeOnly=true
,在这种情况下,它会删除但不使用标头。
在 5.1 中ForwardedHeaderFilter 已弃用并被ForwardedHeaderTransformer 因此,请求头转发可以在
Exchange 已创建。如果仍然配置了过滤器,则会将其从
filters 和ForwardedHeaderTransformer 。 |
1.2.3. 过滤器
在WebHandler
应用程序接口,您可以使用WebFilter
应用 interception 样式
过滤器其余处理链和目标之前的和之后的逻辑WebHandler
.使用 WebFlux Config 时,注册一个WebFilter
就是这么简单
将其声明为 Spring Bean 并(可选地)通过使用@Order
上
bean 声明或通过实现Ordered
.
CORS
Spring WebFlux 通过对 CORS 配置的注释提供细粒度的支持
控制器。但是,当您将其与 Spring Security 一起使用时,我们建议依赖内置的CorsFilter
,它必须在 Spring Security 的过滤器链之前订购。
有关更多详细信息,请参阅 CORS 和 webflux-cors.html 部分。
1.2.4. 异常
在WebHandler
应用程序接口,您可以使用WebExceptionHandler
处理
异常WebFilter
instances 和目标WebHandler
.使用 WebFlux Config 时,注册一个WebExceptionHandler
就像将其声明为
Spring Bean 和(可选)通过使用@Order
在 Bean 声明上或
通过实施Ordered
.
下表描述了可用的WebExceptionHandler
实现:
异常处理程序 | 描述 |
---|---|
|
提供对 |
|
扩展 此处理程序在 WebFlux Config 中声明。 |
1.2.5. 编解码器
这spring-web
和spring-core
modules 支持序列化和
通过非阻塞 I/O 将字节内容反序列化到更高级别的对象或从更高级别的对象反序列化
反应流背压。下面介绍了此支持:
-
HttpMessageReader
和HttpMessageWriter
是合同 对 HTTP 消息内容进行编码和解码。 -
一
Encoder
可以用EncoderHttpMessageWriter
使其适应 Web 中的 application,而Decoder
可以用DecoderHttpMessageReader
. -
DataBuffer
abstracts 不同 字节缓冲区表示形式(例如 NettyByteBuf
,java.nio.ByteBuffer
等)和 is 所有编解码器都适用于什么。请参阅 Data Buffers and Codecs 中的 “Spring Core” 部分,了解有关此主题的更多信息。
这spring-core
module 提供byte[]
,ByteBuffer
,DataBuffer
,Resource
和String
编码器和解码器实现。这spring-web
模块提供 Jackson
JSON、Jackson Smile、JAXB2、Protocol Buffers 和其他编码器和解码器以及
用于表单数据、多部分内容、
server-sent 事件等。
ClientCodecConfigurer
和ServerCodecConfigurer
通常用于配置和
自定义要在应用程序中使用的编解码器。请参阅 配置 HTTP 消息编解码器 部分。
Jackson JSON
JSON 和二进制 JSON (Smile) 是 当存在 Jackson 库时,两者都受支持。
这Jackson2Decoder
工作原理如下:
-
Jackson 的异步、非阻塞解析器用于聚合字节块流 到
TokenBuffer
的每个 JSON 对象。 -
每
TokenBuffer
传递给 Jackson'sObjectMapper
以创建更高级别的对象。 -
当解码为单值发布者(例如
Mono
),则有一个TokenBuffer
. -
当解码为多值发布者(例如
Flux
)、每个TokenBuffer
传递给 这ObjectMapper
一旦收到足够多的字节用于完全形成的对象。这 input content 可以是 JSON 数组,也可以是行分隔的 JSON(如果 content-type 为application/stream+json
.
这Jackson2Encoder
工作原理如下:
-
对于单值发布者(例如
Mono
),只需通过ObjectMapper
. -
对于具有
application/json
,默认情况下,使用Flux#collectToList()
然后序列化生成的集合。 -
对于具有流媒体类型(如
application/stream+json
或application/stream+x-jackson-smile
、encode、write 和 使用以行分隔的 JSON 格式单独刷新每个值。 -
对于 SSE ,
Jackson2Encoder
按事件调用,并刷新输出以确保 毫不拖延地交货。
默认情况下,两者都 |
表单数据
FormHttpMessageReader
和FormHttpMessageWriter
支持解码和编码application/x-www-form-urlencoded
内容。
在经常需要从多个位置访问表单内容的服务器端,ServerWebExchange
提供专用的getFormData()
解析内容的方法
通过FormHttpMessageReader
然后缓存结果以供重复访问。
请参阅 表单数据 中的WebHandler
应用程序接口部分。
一次getFormData()
时,无法再从
请求正文。因此,应用程序应通过ServerWebExchange
始终如一地访问缓存的表单数据,而不是从原始请求正文中读取。
多部分
MultipartHttpMessageReader
和MultipartHttpMessageWriter
支持解码和
对 “multipart/form-data” 内容进行编码。挨次MultipartHttpMessageReader
delegates 到
另一个HttpMessageReader
对于实际解析为Flux<Part>
然后简单地
将零件收集到MultiValueMap
.目前,Synchronoss NIO Multipart 用于
实际解析。
在服务器端,可能需要从多个访问多部分表单内容
地方ServerWebExchange
提供专用的getMultipartData()
解析
内容通过MultipartHttpMessageReader
然后缓存结果以供重复访问。
请参阅 Multipart Data 中的WebHandler
应用程序接口部分。
一次getMultipartData()
时,无法再从
请求正文。因此,应用程序必须始终如一地使用getMultipartData()
对部分进行重复的、类似 Map 的访问,或者依赖SynchronossPartHttpMessageReader
一次性访问Flux<Part>
.
限制
Decoder
和HttpMessageReader
缓冲部分或全部 input 的实现
stream 可以配置对内存中缓冲的最大字节数的限制。
在某些情况下,之所以发生缓冲,是因为输入被聚合并表示为单个
object — 例如,具有@RequestBody byte[]
,x-www-form-urlencoded
data 等。流式处理也可能发生缓冲,当
拆分输入流 — 例如,分隔文本、JSON 对象流,以及
等等。对于这些流式处理情况,限制适用于关联的字节数
在流中有一个对象。
要配置缓冲区大小,您可以检查给定的Decoder
或HttpMessageReader
暴露一个maxInMemorySize
property 属性,如果是这样,Javadoc 将包含有关 default 的详细信息
值。在服务器端,ServerCodecConfigurer
提供从
设置所有编解码器,请参阅 HTTP 消息编解码器。在客户端,对
所有编解码器都可以在 WebClient.Builder 中更改。
对于 Multipart 解析,maxInMemorySize
属性限制
非文件部分的大小。对于文件部分,它确定部分达到的阈值
已写入磁盘。对于写入磁盘的文件部分,还有一个额外的maxDiskUsagePerPart
属性来限制每个部分的磁盘空间量。还有
一个maxParts
属性来限制 Multipart 请求中的段总数。
要在 WebFlux 中配置所有这三个实例,您需要提供一个预配置的MultipartHttpMessageReader
自ServerCodecConfigurer
.
流
当流式传输到 HTTP 响应(例如text/event-stream
,application/stream+json
),请务必定期发送数据,以便
尽早可靠地检测断开连接的客户端。这样的发送可以是
仅注释、空 SSE 事件或任何其他“无作”数据,这些数据实际上可以用作
心跳。
DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。Spring Core 的
此参考在 Data Buffers and Codecs 一节中提供了更多相关信息。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区是池化的和引用计数的,并且必须释放
当使用以避免内存泄漏时。
WebFlux 应用程序通常不需要关心此类问题,除非它们 直接使用或生成数据缓冲区,而不是依赖编解码器转换为 以及更高级别的对象,或者除非他们选择创建自定义编解码器。对于这样的 情况请查看 Data Buffers 和 Codecs、 尤其是 使用 DataBuffer 的部分。
1.2.6. 日志记录
DEBUG
Spring WebFlux 中的级别日志记录被设计为紧凑、最小和
人性化。它侧重于有用的高价值信息
over again 与仅在调试特定问题时有用的其他 cookie 进行比较。
TRACE
级别日志记录通常遵循与DEBUG
(例如,还有
不应是 Firehose),但可用于调试任何问题。此外,一些日志
消息可能会在TRACE
与DEBUG
.
良好的日志记录来自使用日志的经验。如果您发现任何 未达到既定目标,请告诉我们。
日志 ID
在 WebFlux 中,单个请求可以在多个线程上运行,线程 ID 对于关联属于特定请求的日志消息没有用。这就是为什么 默认情况下,WebFlux 日志消息以特定于请求的 ID 为前缀。
在服务器端,日志 ID 存储在ServerWebExchange
属性
(LOG_ID_ATTRIBUTE
),
而基于该 ID 的完全格式化前缀可从ServerWebExchange#getLogPrefix()
.在WebClient
端,日志 ID 存储在ClientRequest
属性
(LOG_ID_ATTRIBUTE
)
,而完全格式化的前缀可从ClientRequest#logPrefix()
.
敏感数据
DEBUG
和TRACE
日志记录可以记录敏感信息。这就是为什么表单参数和
默认情况下,标头是屏蔽的,您必须显式启用其日志记录。
以下示例显示了如何对服务器端请求执行此作:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}
以下示例说明如何对客户端请求执行此作:
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()
自定义编解码器
应用程序可以注册自定义编解码器以支持其他媒体类型、 或默认编解码器不支持的特定行为。
以下示例说明如何对客户端请求执行此作:
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
1.3.DispatcherHandler
Spring WebFlux 与 Spring MVC 类似,是围绕前端控制器模式设计的,
其中中央WebHandler
这DispatcherHandler
为
请求处理,而实际工作由可配置的委托组件执行。
此模型非常灵活,并支持多种工作流。
DispatcherHandler
从 Spring 配置中发现它需要的 delegate 组件。
它本身也被设计为 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. 特殊 bean 类型
这DispatcherHandler
委托给特殊 bean 来处理请求并呈现
适当的回应。我们所说的 “特殊 bean” 是指 Spring 管理的Object
实例
实现 WebFlux 框架 Contract。这些通常带有内置合约,但
您可以自定义、扩展或替换其属性。
下表列出了DispatcherHandler
.请注意,
还有一些其他 bean 在较低级别检测到(参见 Web 处理程序 API 中的特殊 bean 类型)。
Bean 类型 | 解释 |
---|---|
|
将请求映射到处理程序。映射基于一些标准,详细信息
变化 主要的 |
|
帮助 |
|
处理处理程序调用的结果并完成响应。 请参阅 结果处理。 |
1.3.2. WebFlux 配置
应用程序可以声明基础结构 bean(列在 Web 处理程序 API 和DispatcherHandler
),这些请求是处理请求所需的。
但是,在大多数情况下,WebFlux Config 是最好的起点。它声明
所需的 bean,并提供更高级别的配置回调 API 来自定义它。
Spring Boot 依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供 许多额外的方便选择。 |
1.3.3. 处理
DispatcherHandler
按如下方式处理请求:
-
每
HandlerMapping
要求查找匹配的处理程序,并使用第一个匹配项。 -
如果找到处理程序,则会通过适当的
HandlerAdapter
哪 将执行的返回值公开为HandlerResult
. -
这
HandlerResult
被赋予适当的HandlerResultHandler
完成 通过直接写入响应或使用 View 进行渲染来进行处理。
1.3.4. 结果处理
调用处理程序的返回值,通过HandlerAdapter
,被包裹
作为HandlerResult
,以及一些额外的上下文,并传递给第一个HandlerResultHandler
声称支持它。下表显示了可用的HandlerResultHandler
实现,所有这些都在 WebFlux Config 中声明:
结果处理程序类型 | 返回值 | 默认订单 |
---|---|---|
|
|
0 |
|
|
0 |
|
处理返回值 |
100 |
|
另请参阅 View Resolution。 |
|
1.3.5. 异常
这HandlerResult
从HandlerAdapter
可以暴露函数的错误
基于某些处理程序特定机制的处理。如果出现以下情况,则调用此错误函数:
-
处理程序(例如
@Controller
) 调用失败。 -
通过
HandlerResultHandler
失败。
只要 error 出现 信号发生在从处理程序返回的反应式类型生成任何数据项之前。
就是这样@ExceptionHandler
methods 中的@Controller
支持类。
相比之下,Spring MVC 中对相同的支持是建立在HandlerExceptionResolver
.
这通常无关紧要。但是,请记住,在 WebFlux 中,您不能使用@ControllerAdvice
处理在选择处理程序之前发生的异常。
1.3.6. 视图分辨率
视图分辨率允许使用 HTML 模板和模型呈现到浏览器,而无需
将您与特定的 View 技术联系起来。在 Spring WebFlux 中,视图分辨率为
通过专用的 HandlerResultHandler 提供支持,该 HandlerResultHandler 使用ViewResolver
实例将 String (表示逻辑视图名称) 映射到View
实例。这View
然后用于呈现响应。
处理
这HandlerResult
传递到ViewResolutionResultHandler
包含返回值
从处理程序和包含请求期间添加的属性的模型
处理。返回值将作为以下值之一进行处理:
-
String
,CharSequence
:要解析为View
通过 已配置列表ViewResolver
实现。 -
void
:根据请求路径选择默认视图名称,减去前导和 尾部斜杠,并将其解析为View
.当视图名称 未提供(例如,返回 model 属性)或异步返回值 (例如,Mono
completed empty)。 -
渲染:API for 查看解决方案。探索 IDE 中的代码完成选项。
-
Model
,Map
:要添加到请求的模型的额外模型属性。 -
任何其他:任何其他返回值(简单类型除外,由 BeanUtils#isSimpleProperty 确定) 被视为要添加到模型的 model 属性。属性名称是派生的 从类名中通过使用约定, 除非 Handler 方法
@ModelAttribute
注释存在。
该模型可以包含异步的反应式类型(例如,来自 Reactor 或 RxJava)。事先
到 rendering,AbstractView
将此类模型属性解析为具体值
并更新模型。单值响应式类型被解析为单个
value 或no value(如果为空),而多值响应式类型(例如Flux<T>
) 是
收集并解析为List<T>
.
要配置视图分辨率,只需添加ViewResolutionResultHandler
豆
添加到您的 Spring 配置中。WebFlux Config 提供了一个
用于视图解析的专用配置 API。
有关与 Spring WebFlux 集成的视图技术的更多信息,请参见View Technologies。
重 定向
特别的redirect:
prefix 允许您执行重定向。这UrlBasedViewResolver
(和子类)将此视为一条指令,
redirect 是必需的。视图名称的其余部分是重定向 URL。
实际效果与控制器返回RedirectView
或Rendering.redirectTo("abc").build()
,但现在控制器本身可以
根据逻辑视图名称进行作。视图名称(如redirect:/some/resource
是相对于当前应用程序而言的,而视图名称(如redirect:https://example.com/arbitrary/path
重定向到绝对 URL。
内容协商
ViewResolutionResultHandler
支持内容协商。它比较请求
媒体类型,以及每个选定的媒体类型View
.第一个View
,该 API 的 IP 地址。
为了支持 JSON 和 XML 等媒体类型,Spring WebFlux 提供了HttpMessageWriterView
,这是一个特殊的View
它通过 HttpMessageWriter 呈现。通常,您会将这些配置为默认
视图。默认视图为
如果它们与请求的媒体类型匹配,则始终选中并使用。
1.4. 带注解的控制器
Spring WebFlux 提供了一个基于 Comments 的编程模型,其中@Controller
和@RestController
组件使用注解来表示请求映射、请求输入、
处理异常等。带 Comments 的控制器具有灵活的方法签名和
不必扩展基类,也不必实现特定的接口。
下面的清单显示了一个基本示例:
@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
beans 中,你可以将组件扫描添加到
您的 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
annotation 用于将请求映射到 controllers 方法。它有
按 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” 的变量 |
|
|
匹配 regexp |
|
|
匹配零个或多个路径段,直到路径的末尾,并将其捕获为名为 “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
compiler 标志。
语法{*varName}
声明与零个或多个剩余路径匹配的 URI 变量
段。例如/resources/{*path}
匹配/resources/
和"path"
variable 捕获完整的相对路径。
语法{varName:regex}
使用正则表达式声明一个 URI 变量,该正则表达式具有
语法:{varName:regex}
.例如,假设 URL 为/spring-web-3.0.5 .jar
,则使用以下方法
提取名称、版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式也可以嵌入${…}
启动时解析的占位符
通过PropertyPlaceHolderConfigurer
针对 Local、System、Environment 和其他属性
来源。例如,您可以使用它来根据某些外部 URL 参数化基本 URL
配置。
Spring WebFlux 使用PathPattern 和PathPatternParser ,了解 URI 路径匹配支持。
这两个类都位于spring-web 并专为与 HTTP URL 一起使用而设计
paths 的 Web 应用程序中的 paths,其中在运行时匹配了大量 URI 路径模式。 |
Spring WebFlux 不支持后缀模式匹配——与 Spring MVC 不同,其中
映射(如/person
也匹配到/person.*
.对于基于 URL 的内容
negotiation,如果需要,我们建议使用 query 参数,它更简单、更
显式的,并且不易受到基于 URL 路径的漏洞利用。
形态比较
当多个模式与一个 URL 匹配时,必须比较它们以找到最佳匹配。此作已完成
跟PathPattern.SPECIFICITY_COMPARATOR
,它查找更具体的模式。
对于每个模式,都会根据 URI 变量和通配符的数量计算分数。 其中 URI 变量的得分低于通配符。总分较低的模式 赢了。如果两个模式具有相同的分数,则选择较长的模式。
Catch-all 模式(例如 ,**
{*varName}
) 被排除在评分之外,并且始终
排序为 last。如果两个模式都是 catch-all,则选择较长的模式。
易耗品介质类型
您可以根据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
属性。与大多数其他请求不同
mapping 属性,但是,当在类级别使用时,方法级别的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 String petId): Pet {
// ...
}
媒体类型可以指定字符集。支持否定表达式 — 例如!text/plain
指除text/plain
.
您可以声明一个共享的produces
属性。与大多数其他请求不同
mapping 属性,但是,当在类级别使用时,方法级别的produces
属性
覆盖而不是扩展类级别声明。
MediaType 为常用的媒体类型提供常量 — 例如APPLICATION_JSON_VALUE ,APPLICATION_XML_VALUE . |
参数和标头
您可以根据查询参数条件缩小请求映射的范围。您可以测试
存在查询参数 (myParam
),因为它不存在 (!myParam
) 或
特定值 (myParam=myValue
).以下示例测试具有值的参数:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查myParam 等于myValue . |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查myParam 等于myValue . |
您还可以对请求标头条件使用相同的方法,如以下示例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查myHeader 等于myValue . |
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查myHeader 等于myValue . |
HTTP 头, 选项
@GetMapping
和@RequestMapping(method=HttpMethod.GET)
支持 HTTP HEAD
透明地用于请求映射目的。控制器方法不需要更改。
响应包装器,应用于HttpHandler
server 适配器,确保Content-Length
header 设置为写入的字节数,但未实际写入响应。
默认情况下,HTTP OPTIONS 是通过设置Allow
响应标头的 HTTP 列表
所有@RequestMapping
方法。
对于@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 方法匹配。如果你需要一个组合的示例
annotations 中,看看这些是怎么声明的。
Spring WebFlux 还支持具有自定义请求匹配的自定义请求映射属性
逻辑。这是一个更高级的选项,需要子类化RequestMappingHandlerMapping
并覆盖getCustomMethodCondition
方法,其中
您可以检查 custom 属性并返回您自己的RequestCondition
.
显式注册
您可以通过编程方式注册 Handler 方法,这些方法可用于动态 registrations 或高级情况,例如同一处理程序的不同实例 在不同的 URL 下。以下示例显示了如何执行此作:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
1 | 注入 target handlers 和控制器的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 注入 target handlers 和控制器的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
1.4.3. 处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列
支持的控制器方法参数和返回值。
方法参数
下表显示了支持的控制器方法参数。
响应式类型(Reactor、RxJava 或其他)是 支持需要阻塞 I/O 的参数(例如,读取请求正文)到 被解决。这在 Description (描述) 列中标记。不需要响应式类型 在不需要阻塞的参数上。
JDK 1.8 的java.util.Optional
支持作为方法参数与
具有required
属性(例如@RequestParam
,@RequestHeader
,
和其他)并且等效于required=false
.
控制器方法参数 | 描述 |
---|---|
|
访问完整版 |
|
访问 HTTP 请求或响应。 |
|
访问会话。这不会强制启动新会话,除非属性 被添加。支持响应式类型。 |
|
当前经过身份验证的用户 — 可能是特定的 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的名称-值对。请参见矩阵变量。 |
|
用于访问 Servlet 请求参数。参数值将转换为声明的
method 参数类型。看 请注意,使用 |
|
用于访问请求标头。标头值将转换为声明的方法参数
类型。看 |
|
用于访问 Cookie。Cookie 值将转换为声明的方法参数类型。
看 |
|
用于访问 HTTP 请求正文。正文内容被转换为声明的方法
参数类型 |
|
用于访问请求标头和正文。主体转换为 |
|
|
|
用于访问在 HTML 控制器中使用并作为模板公开的模型 视图渲染的一部分。 |
|
要访问模型中的现有属性(如果不存在,则实例化),请使用
应用了数据绑定和验证。看 请注意,使用 |
|
用于访问命令对象的验证和数据绑定中的错误,即 |
|
用于将表单处理标记为完成,从而触发会话属性的清理
通过类级别 |
|
用于准备相对于当前请求的主机、端口、方案和路径的 URL。 请参阅 URI 链接。 |
|
用于访问任何 session 属性 — 与 session 中存储的 model 属性相反
由于类级别 |
|
用于访问请求属性。看 |
任何其他参数 |
如果方法参数与上述任何内容都不匹配,则默认情况下,它会解析为
一个 |
返回值
下表显示了支持的控制器方法返回值。请注意,反应式 来自 Reactor、RxJava 或其他库的类型是 通常支持所有返回值。
控制器方法返回值 | 描述 |
---|---|
|
返回值通过 |
|
返回值指定完整响应,包括 HTTP 标头,并且正文经过编码
通过 |
|
用于返回带有标头且没有正文的响应。 |
|
要解析的视图名称 |
|
一个 |
|
要添加到隐式模型的属性,其中视图名称是隐式确定的 基于请求路径。 |
|
要添加到模型中的属性,视图名称隐式确定 在请求路径上。 请注意, |
|
用于模型和视图渲染场景的 API。 |
|
具有 如果以上都不是真的,则 |
|
发出服务器发送的事件。这 |
任何其他返回值 |
如果返回值与上述任何内容都不匹配,则默认情况下,它被视为视图
name(如果是 |
类型转换
一些带注释的控制器方法参数表示基于字符串的请求输入(例如@RequestParam
,@RequestHeader
,@PathVariable
,@MatrixVariable
和@CookieValue
)
如果参数声明为String
.
对于此类情况,将根据配置的转换器自动应用类型转换。
默认情况下,简单类型(例如int
,long
,Date
等)支持。类型转换
可以通过WebDataBinder
(参见DataBinder
) 或通过注册Formatters
使用FormattingConversionService
(参见 Spring Field Formatting)。
矩阵变量
矩阵变量可以出现在任何路径段中,每个变量用分号和
多个值,以逗号分隔 — 例如"/cars;color=red,green;year=2012"
.倍数
值也可以通过重复的变量名称来指定 — 例如"color=red;color=green;color=blue"
.
与 Spring MVC 不同,在 WebFlux 中,URL 中存在或不存在矩阵变量 不会影响请求映射。换句话说,您不需要使用 URI 变量 以屏蔽可变内容。也就是说,如果你想从 controller 方法中,你需要在路径段中添加一个 URI 变量,其中 matrix 变量是预期的。以下示例显示了如何执行此作:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有路径段都可以包含矩阵变量,您有时可能需要 消除矩阵变量预期位于哪个路径变量中的歧义, 如下例所示:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
您可以定义一个矩阵变量,该变量可以定义为可选变量,并指定一个默认值 如下例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,请使用MultiValueMap
,如下例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
您可以使用@RequestParam
注解将查询参数绑定到
控制器。以下代码片段显示了用法:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | 用@RequestParam . |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 用@RequestParam . |
Servlet API“请求参数”概念将查询参数、表单
data 和 multipart 合二为一。但是,在 WebFlux 中,每个都可以通过ServerWebExchange .而@RequestParam 绑定到查询参数,则可以使用
数据绑定,用于将查询参数、表单数据和多部分应用于命令对象。 |
使用@RequestParam
annotation 是必需的,但
您可以通过设置@RequestParam
自false
或者使用java.util.Optional
包装纸。
如果目标方法参数 type 不是 type,则会自动应用类型转换String
.请参阅类型转换。
当@RequestParam
注解在Map<String, String>
或MultiValueMap<String, String>
参数,则 Map 中填充了所有查询参数。
请注意,使用@RequestParam
是可选的 — 例如,设置其属性。由
default,任何作为简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟@RequestParam
.
@RequestHeader
您可以使用@RequestHeader
注解将请求标头绑定到
控制器。
以下示例显示了一个带有标头的请求:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
以下示例获取Accept-Encoding
和Keep-Alive
头:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取Accept-Encoging 页眉。 |
2 | 获取Keep-Alive 页眉。 |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | 获取Accept-Encoging 页眉。 |
2 | 获取Keep-Alive 页眉。 |
如果目标方法参数 type 不是 type,则会自动应用类型转换String
.请参阅类型转换。
当@RequestHeader
注解用于Map<String, String>
,MultiValueMap<String, String>
或HttpHeaders
参数,则 Map 会被填充
替换为所有标头值。
内置支持可用于将逗号分隔的字符串转换为
字符串的数组或集合,或者类型转换系统已知的其他类型的集合。为
example,一个带有@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 值。 |
如果目标方法参数 type 不是 type,则会自动应用类型转换String
.请参阅类型转换。
@ModelAttribute
您可以使用@ModelAttribute
注解来访问来自
model 或实例化它(如果不存在)。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
. -
从默认构造函数的调用。
-
从具有与 query 匹配的参数的“主构造函数”的调用 参数或表单字段。参数名称是通过 JavaBeans 确定的
@ConstructorProperties
或通过字节码中运行时保留的参数名称。
获取 model 属性实例后,将应用数据绑定。这WebExchangeDataBinder
类将查询参数和表单字段的名称与字段匹配
目标上的名称Object
.在应用类型转换后填充匹配字段
必要时。有关数据绑定(和验证)的更多信息,请参阅验证。有关自定义数据绑定的更多信息,请参阅DataBinder
.
数据绑定可能会导致错误。默认情况下,WebExchangeBindException
被提升,但是,
要在控制器方法中检查此类错误,您可以添加BindingResult
论点
紧邻@ModelAttribute
,如下例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 添加BindingResult . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 添加BindingResult . |
您可以通过在数据绑定后自动应用验证,方法是添加javax.validation.Valid
注解或 Spring 的@Validated
注解(另请参见 Bean 验证和 Spring 验证)。以下示例使用@Valid
注解:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 用@Valid 在 model 属性参数上。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 用@Valid 在 model 属性参数上。 |
与 Spring MVC 不同,Spring WebFlux 在模型中支持响应式类型——例如,Mono<Account>
或io.reactivex.Single<Account>
.您可以声明@ModelAttribute
论点
有或没有响应式类型包装器,并且它将被相应地解析,
如有必要,调整为实际值。但是,请注意,要使用BindingResult
参数,则必须声明@ModelAttribute
参数,而没有响应式
type 包装器,如前所述。或者,您可以通过
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 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟@ModelAttribute
.
@SessionAttributes
@SessionAttributes
用于将模型属性存储在WebSession
之间
请求。它是一个类型级注释,用于声明
特定控制器。这通常列出模型属性的名称或
model 属性,这些属性应该透明地存储在会话中以供后续使用
请求访问。
请考虑以下示例:
@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
到 controller 方法中。
作为控制器的一部分,在会话中临时存储模型属性
workflow,请考虑使用SessionAttributes
,如@SessionAttributes
.
@RequestAttribute
与@SessionAttribute
中,您可以使用@RequestAttribute
annotation 添加到
访问之前创建的预先存在的请求属性(例如,通过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 的访问
内容。在控制器中处理文件上传表单(例如,从浏览器)的最佳方法
是通过数据绑定到 Command 对象,
如下例所示:
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
注解,这会导致应用 Standard Bean Validation。验证
错误会导致WebExchangeBindException
这将产生 400 (BAD_REQUEST) 响应。
异常包含一个BindingResult
与错误详细信息一起,也可以处理
在控制器方法中,使用异步包装器声明参数,然后使用
错误相关运算符:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}
要以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>
),如下例所示:
@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
注解,这会导致应用 Standard Bean Validation。验证
错误会导致WebExchangeBindException
,这将产生 400 (BAD_REQUEST) 响应。
异常包含一个BindingResult
替换为错误详细信息,并且可以在
controller 方法,方法是使用 async 包装器声明参数,然后使用 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
对方法进行注释以序列化返回
传递给响应正文。以下内容
示例展示了如何做到这一点:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别也受支持,在这种情况下,它由
所有控制器方法。这是@RestController
,仅此而已
比标有@Controller
和@ResponseBody
.
您可以组合@ResponseBody
方法。
有关详细信息,请参阅 Jackson JSON。
您可以使用 WebFlux Config 的 HTTP 消息编解码器选项来 配置或自定义消息写入。
ResponseEntity
ResponseEntity
就像@ResponseBody
但有 status 和 headers。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux 支持使用单值响应式类型
生成ResponseEntity
异步和/或单值和多值响应式类型
为了身体。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring WebFlux 为 Jackson 的序列化视图提供了内置支持,
,它只允许渲染Object
.若要将其与@ResponseBody
或ResponseEntity
controller 方法,你可以使用 Jackson 的@JsonView
注解激活序列化视图类,如下例所示:
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser(): User {
return User("eric", "7!jd#h23")
}
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String
) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许视图类数组,但您只能为每个
controller 方法。如果需要激活多个视图,请使用复合界面。 |
1.4.4.Model
您可以使用@ModelAttribute
注解:
本节讨论@ModelAttribute
方法或前面列表中的第二项。
控制器可以具有任意数量的@ModelAttribute
方法。所有这些方法都是
之前调用@RequestMapping
方法。一个@ModelAttribute
方法也可以通过@ControllerAdvice
.有关更多详细信息,请参阅 Controller Advice 部分。
@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
ininvokecation 中,提供了@ModelAttribute
argument 是
在没有包装器的情况下声明,如下例所示:
@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
method 被解释为
model 属性。这通常不是必需的,因为它是 HTML 中的默认行为
控制器,除非返回值是String
否则会被解释为
作为视图名称。@ModelAttribute
还可以帮助自定义 Model 属性名称,
如下例所示:
@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
-based 请求值(例如请求参数、路径变量、 headers、cookie 等)传递给 controller 方法参数的目标类型。 -
将模型对象值格式化为
String
值。
@InitBinder
方法可以注册特定于控制器的java.bean.PropertyEditor
或
SpringConverter
和Formatter
组件。此外,您可以使用 WebFlux Java 配置来注册Converter
和Formatter
全局共享FormattingConversionService
.
@InitBinder
方法支持许多相同的参数@RequestMapping
方法
do 的@ModelAttribute
(command 对象) 参数。通常,它们被声明
替换为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 请求的绑定 parameters ((即表单数据或查询参数) ) 添加到模型对象中的属性,以及 其嵌套对象。
只public
遵循 JavaBeans 命名约定的属性将公开用于数据绑定,例如,public String getFirstName()
和public void setFirstName(String)
方法firstName
财产。
模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO (Plain Old Java Object)。 |
默认情况下, Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为 客户端可以面向任何公共属性路径,甚至是一些不需要的公共属性路径 针对给定的使用案例。
例如,给定一个 HTTP 表单数据端点,恶意客户端可能会提供 存在于模型对象图中但不属于 HTML 表单的属性 在浏览器中显示。这可能会导致在模型对象上设置数据,并且任何 的嵌套对象中。
推荐的方法是使用仅公开
与表单提交相关的属性。例如,在用于更改
用户的电子邮件地址,则 Model 对象应声明一组最小属性,例如
如下所示ChangeEmailForm
.
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
如果您不能或不想为每个数据使用专用模型对象
binding 用例中,您必须限制允许进行数据绑定的属性。
理想情况下,您可以通过setAllowedFields()
method 开启WebDataBinder
.
例如,要在应用程序中注册允许的字段模式,您可以实现@InitBinder
方法中的@Controller
或@ControllerAdvice
组件,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以注册不允许的
字段模式通过setDisallowedFields()
method 中DataBinder
及其子类。
但请注意,“允许列表”比“拒绝列表”更安全。因此setAllowedFields()
应该被青睐setDisallowedFields()
.
请注意,与允许的字段模式匹配区分大小写;鉴于匹配 Against Disallowed 字段模式不区分大小写。此外,匹配 不允许的模式将不被接受,即使它也恰好与 允许列表。
正确配置允许和不允许的字段模式非常重要 直接公开域模型以进行数据绑定时。否则,它是一个 安全风险大。 此外,强烈建议您不要使用域中的类型 模型(如 JPA 或 Hibernate 实体)作为数据绑定场景中的模型对象。 |
1.4.6. 管理异常
@Controller
@ControllerAdvice类可以具有@ExceptionHandler
方法处理来自控制器方法的异常。以下内容
example 包含这样的处理程序方法:
@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
被抛出)或针对顶级包装器中的直接原因
exception(例如,IOException
包装在IllegalStateException
).
对于匹配的异常类型,最好将目标异常声明为方法参数,
如前面的示例所示。或者,注释声明可以缩小
异常类型进行匹配。我们通常建议在
参数签名,并在@ControllerAdvice
prioritizeed 和相应的顺序。
有关详细信息,请参阅 MVC 部分。
一@ExceptionHandler method 支持相同的方法参数和
将值作为@RequestMapping 方法,请求正文除外 -
和@ModelAttribute -相关的方法参数。 |
支持@ExceptionHandler
方法由HandlerAdapter
为@RequestMapping
方法。看DispatcherHandler
了解更多详情。
REST API 异常
REST 服务的一个常见要求是在
响应。Spring Framework 不会自动这样做,因为表示
of error details 是特定于应用程序的。但是,@RestController
可以使用@ExceptionHandler
方法中带有ResponseEntity
返回
值以设置响应的状态和正文。此类方法也可以声明
在@ControllerAdvice
类来全局应用它们。
请注意,Spring WebFlux 没有 Spring MVC 的等效项ResponseEntityExceptionHandler ,因为 WebFlux 仅引发ResponseStatusException (或其子类),并且这些不需要被翻译成
HTTP 状态代码。 |
1.4.7. 控制器建议
通常,@ExceptionHandler
,@InitBinder
和@ModelAttribute
方法 适用
在@Controller
类(或类层次结构)。如果你
希望这些方法更全局地应用(跨控制器),你可以在
类@ControllerAdvice
或@RestControllerAdvice
.
@ControllerAdvice
被注释为@Component
,这意味着此类类可以是
通过组件扫描注册为 Spring bean。@RestControllerAdvice
是带有注解的合成注解
同时@ControllerAdvice
和@ResponseBody
,这本质上意味着@ExceptionHandler
方法通过消息转换呈现到响应体
(相对于视图分辨率或模板渲染)。
启动时,用于@RequestMapping
和@ExceptionHandler
方法检测带有@ControllerAdvice
,然后应用其
方法。全球@ExceptionHandler
方法(来自@ControllerAdvice
) 是
在本地 Bean 的 Swift Controller 之后应用(从@Controller
).相比之下,全球@ModelAttribute
和@InitBinder
方法先于 local 方法应用。
默认情况下,@ControllerAdvice
方法适用于每个请求(即所有控制器),
但是,您可以通过使用
annotation 中,如下例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}
前面示例中的选择器在运行时进行评估,可能会产生负面影响
性能。请参阅@ControllerAdvice
javadoc 了解更多详情。
1.5. 功能端点
Spring WebFlux 包括 WebFlux.fn,这是一个轻量级函数式编程模型,其中函数 用于路由和处理请求,而 Contract 旨在实现不变性。 它是基于 Comments 的编程模型的替代方案,但在其他方面运行 相同的 Reactive Core 基础。
1.5.1. 概述
在 WebFlux.fn 中,HTTP 请求使用HandlerFunction
:一个函数,该函数采用ServerRequest
并返回一个延迟的ServerResponse
(即Mono<ServerResponse>
).
请求和响应对象都有不可变的契约,这些契约提供对 JDK 8 友好的
访问 HTTP 请求和响应。HandlerFunction
等价于@RequestMapping
方法中的
基于注释的编程模型。
传入请求被路由到具有RouterFunction
:一个函数,该函数
需要ServerRequest
并返回一个延迟的HandlerFunction
(即Mono<HandlerFunction>
).
当 router 函数匹配时,将返回一个处理程序函数;否则为空 Mono。RouterFunction
等价于@RequestMapping
注解,但带有 major
不同之处在于 router 函数不仅提供数据,还提供行为。
RouterFunctions.route()
提供便于创建路由器的 Router 构建器,
如下例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = coRouter { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
suspend fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
suspend fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
suspend fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
1 | 使用协程路由器 DSL 创建路由器,也可以通过router { } . |
运行RouterFunction
是将其转换为HttpHandler
并安装
通过其中一个内置服务器适配器:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多数应用程序可以通过 WebFlux Java 配置运行,请参阅运行服务器。
1.5.2. HandlerFunction 函数
ServerRequest
和ServerResponse
是提供 JDK 8 友好的不可变接口
访问 HTTP 请求和响应。
请求和响应都提供 Reactive Streams 背压
与身体流相反。
请求正文由 Reactor 表示Flux
或Mono
.
响应体由任何 Reactive Streams 表示Publisher
包括Flux
和Mono
.
有关更多信息,请参阅 反应式库。
ServerRequest
ServerRequest
提供对 HTTP 方法、URI、标头和查询参数的访问,
而对身体的访问是通过body
方法。
以下示例将请求正文提取为Mono<String>
:
Mono<String> string = request.bodyToMono(String.class);
val string = request.awaitBody<String>()
以下示例将主体提取到Flux<Person>
(或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)).awaitFirst()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
以下示例显示如何访问表单数据:
Mono<MultiValueMap<String, String> map = request.formData();
val map = request.awaitFormData()
以下示例显示如何以 map 形式访问多部分数据:
Mono<MultiValueMap<String, Part> map = request.multipartData();
val map = request.awaitMultipartData()
以下示例显示了如何以流式处理方式一次访问多个部分:
Flux<Part> parts = request.body(BodyExtractors.toParts());
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse
ServerResponse
提供对 HTTP 响应的访问,并且由于它是不可变的,因此您可以使用
一个build
方法创建它。您可以使用生成器设置响应状态,以添加响应
headers 或提供正文。以下示例使用 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()
根据所使用的编解码器,可以传递 hint 参数来自定义 body 是序列化的或反序列化的。例如,要指定 Jackson JSON 视图:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
处理程序类
我们可以将处理程序函数编写为 lambda,如下例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
这很方便,但在应用程序中,我们需要多个函数和多个内联
Lambda 可能会变得混乱。
因此,将相关的处理程序函数一起分组到一个处理程序类中是很有用的,该
具有与@Controller
在基于注释的应用程序中。
例如,下面的类公开了一个响应式Person
存储 库:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
1 | listPeople 是一个处理函数,它返回所有Person 对象作为
JSON 的 JSON 格式。 |
2 | createPerson 是一个处理程序函数,它将新的Person 包含在请求正文中。
请注意,PersonRepository.savePerson(Person) 返回Mono<Void> :空的Mono 那个会发出
从请求中读取并存储人员时的完成信号。所以我们使用build(Publisher<Void>) 方法在收到该完成信号时发送响应(即
当Person 已保存)。 |
3 | getPerson 是一个处理程序函数,它返回一个 person,由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 的 JSON 格式。 |
2 | createPerson 是一个处理程序函数,它将新的Person 包含在请求正文中。
请注意,PersonRepository.savePerson(Person) 是没有返回类型的挂起函数。 |
3 | getPerson 是一个处理程序函数,它返回一个 person,由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
Router 函数用于将请求路由到相应的HandlerFunction
.
通常,您不会自己编写 router 函数,而是在RouterFunctions
Utility 类来创建一个。RouterFunctions.route()
(无参数)为您提供用于创建路由器的 Fluent 构建器
函数,而RouterFunctions.route(RequestPredicate, HandlerFunction)
提供直接方式
创建路由器。
通常建议使用route()
builder,因为它提供了
适用于典型映射场景的便捷捷径,无需难以发现
static imports。
例如,router 函数构建器提供了GET(String, HandlerFunction)
为 GET 请求创建映射;和POST(String, HandlerFunction)
用于 POST。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种引入其他
谓词。
对于每个 HTTP 方法,都有一个重载的变体,它采用RequestPredicate
作为
参数,但可以表示其他约束。
谓词
您可以编写自己的RequestPredicate
,但RequestPredicates
Utility 类
提供常用的实现,基于请求路径、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 被评估,依此类推。 因此,在一般路由之前声明更具体的路由是有意义的。 请注意,此行为与基于 Comments 的编程模型不同,其中 “最具体”控制器方法会自动选取。
当使用 router 函数构建器时,所有定义的路由都组合成一个RouterFunction
即从build()
.
还有其他方法可以将多个 router 功能组合在一起:
-
add(RouterFunction)
在RouterFunctions.route()
架构工人 -
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— 的快捷方式RouterFunction.and()
with 嵌套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 匹配 JSON 的标头将路由到PersonHandler.getPerson |
2 | GET /person 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | GET /person/{id} 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.getPerson |
2 | GET /person 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
嵌套路由
一组 router 函数通常具有共享谓词,例如
共享路径。在上面的示例中,共享谓词将是一个路径谓词,该
比赛/person
,由其中 3 条路由使用。使用注释时,您需要删除
此复制通过使用类型级别@RequestMapping
注解,映射到/person
.在 WebFlux.fn 中,路径谓词可以通过path
方法上的
router 函数构建器。例如,上面示例的最后几行可以是
通过使用嵌套路由,通过以下方式进行了改进:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
1 | 请注意,第二个参数path 是采用 Router Builder 的使用者。 |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以使用
这nest
方法。
以上仍然包含一些共享Accept
-header 谓词。
我们可以通过使用nest
方法与accept
:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.build();
val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
1.5.4. 运行服务器
如何在 HTTP 服务器中运行路由器功能?一个简单的选择是将路由器
函数添加到HttpHandler
使用以下选项之一:
-
RouterFunctions.toHttpHandler(RouterFunction)
-
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
然后,您可以使用返回的HttpHandler
按照 HttpHandler 获取特定于服务器的说明。
Spring Boot 也使用一个更典型的选项,是使用DispatcherHandler
-based 设置,它使用 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
路由上的 methods
函数构建器。
通过 annotations,您可以通过使用@ControllerAdvice
一个ServletFilter
和/或两者。
该筛选条件将应用于构建器构建的所有路由。
这意味着嵌套路由中定义的筛选条件不适用于 “top-level” 路由。
例如,请考虑以下示例:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 这before 添加自定义请求标头的 filter 仅适用于两个 GET 路由。 |
2 | 这after filter 记录响应的 filter 应用于所有路由,包括嵌套路由。 |
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST("/person", handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
}
1 | 这before 添加自定义请求标头的 filter 仅适用于两个 GET 路由。 |
2 | 这after filter 记录响应的 filter 应用于所有路由,包括嵌套路由。 |
这filter
方法采用HandlerFilterFunction
:一个
函数,该函数采用ServerRequest
和HandlerFunction
并返回一个ServerResponse
.
handler 函数参数表示链中的下一个元素。
这通常是路由到的处理程序,但也可以是另一个
filter (如果应用了多个)。
现在我们可以向路由添加一个简单的安全过滤器,假设我们有一个SecurityManager
那
可以确定是否允许特定路径。
以下示例显示了如何执行此作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用next.handle(ServerRequest)
是可选的。
我们只允许在允许访问时运行处理程序函数。
除了使用filter
方法,则可以应用
过滤到现有的 router 函数RouterFunction.filter(HandlerFilterFunction)
.
对功能端点的 CORS 支持通过专用的CorsWebFilter . |
1.6. URI 链接
本节描述了 Spring Framework 中可用于准备 URI 的各种选项。
1.6.1. UriComponents
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
有助于使用变量从 URI 模板构建 URI,如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
前面的示例可以合并为一个链,并使用buildAndExpand
,
如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()
您可以通过直接转到 URI(这意味着编码)来进一步缩短它, 如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.6.2. Uri生成器
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
实现UriBuilder
.您可以创建一个UriBuilder
,反过来,使用UriBuilderFactory
.一起UriBuilderFactory
和UriBuilder
提供一种可插拔的机制来从 URI 模板构建 URI,基于
共享配置,例如基本 URL、编码首选项和其他详细信息。
您可以配置RestTemplate
和WebClient
替换为UriBuilderFactory
以自定义 URI 的准备工作。DefaultUriBuilderFactory
是默认值
实现UriBuilderFactory
使用UriComponentsBuilder
internally 和
公开共享配置选项。
以下示例显示如何配置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
但是,它不是静态工厂方法,而是一个实际实例
,其中包含 configuration 和 preferences,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.6.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode() 中: 首先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。
-
UriComponents#encode() 中: 在 URI 变量展开后对 URI 组件进行编码。
这两个选项都用转义的八位字节替换非 ASCII 字符和非法字符。但是,第一个选项 还会替换 URI 变量中出现的具有保留含义的字符。
请考虑 “;”,它在 path 中是合法的,但具有保留的含义。第一个选项将 “;” 在 URI 变量中带有 “%3B”,但在 URI 模板中没有。相比之下,第二个选项永远不会 替换 “;”,因为它是路径中的合法字符。 |
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量作为不透明数据进行完全编码,而选项 2 仅在以下情况下有用 URI 变量有意包含保留字符。
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到 URI(这意味着编码)来缩短前面的示例, 如下例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
这WebClient
和RestTemplate
通过
这UriBuilderFactory
策略。两者都可以使用自定义策略进行配置。
如下例所示:
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
implementation usesUriComponentsBuilder
internally 到
展开并编码 URI 模板。作为工厂,它提供了一个配置位置
编码方法,基于以下编码模式之一:
-
TEMPLATE_AND_VALUES
:使用UriComponentsBuilder#encode()
,对应于 前面列表中的第一个选项,用于预编码 URI 模板并在 扩大。 -
VALUES_ONLY
:不对 URI 模板进行编码,而是应用严格编码 到 URI 变量UriUtils#encodeUriUriVariables
在将它们扩展到 模板。 -
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
implementations 提供对 CORS 的内置支持。成功后
将请求映射到处理程序,则HandlerMapping
检查 CORS 配置的
given request 和 handler 并采取进一步的作。处理印前检查请求
直接,而简单和实际的 CORS 请求被拦截、验证,并具有
所需的 CORS 响应标头集。
为了启用跨域请求(即Origin
header 存在且
与请求的主机不同),您需要有一些显式声明的 CORS
配置。如果未找到匹配的 CORS 配置,则印前 Backup 请求为
拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中
因此,浏览器会拒绝它们。
每HandlerMapping
可以使用基于 URL 模式的 URL 进行单独配置CorsConfiguration
映射。在大多数情况下,应用程序
使用 WebFlux Java 配置来声明此类映射,这将产生单个
传递给所有HandlerMapping
实现。
您可以在HandlerMapping
Level with More (更多级别)
精细的处理程序级 CORS 配置。例如,带注解的控制器可以使用
类级或方法级@CrossOrigin
注解(其他处理程序可以实现CorsConfigurationSource
).
组合全局配置和本地配置的规则通常是累加的 — 例如,
所有全球和所有本地源。对于只能使用单个值
accepted,例如allowCredentials
和maxAge
,则 local 将覆盖 global 值。看CorsConfiguration#combine(CorsConfiguration)
了解更多详情。
要从源中了解更多信息或进行高级自定义,请参阅:
|
1.7.3.@CrossOrigin
这@CrossOrigin
annotation 支持对带注解的控制器方法进行跨域请求,因为
以下示例显示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
-
所有来源。
-
所有标头。
-
控制器方法映射到的所有 HTTP 方法。
allowedCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。
maxAge
设置为 30 分钟。
@CrossOrigin
在类级别也受支持,并且被所有方法继承。
以下示例指定某个域,并将maxAge
到一小时:
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
您可以使用@CrossOrigin
在类和方法级别,
如下例所示:
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com") (2)
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
1 | 用@CrossOrigin 在类级别。 |
2 | 用@CrossOrigin 在方法级别。 |
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com") (2)
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
1 | 用@CrossOrigin 在类级别。 |
2 | 用@CrossOrigin 在方法级别。 |
1.7.4. 全局配置
除了细粒度的 controller 方法级配置之外,您可能还希望
也定义一些全局 CORS 配置。您可以设置基于 URLCorsConfiguration
映射HandlerMapping
.但是,大多数应用程序都使用
WebFlux Java 配置来执行此作。
默认情况下,全局配置会启用以下内容:
-
所有来源。
-
所有标头。
-
GET
,HEAD
和POST
方法。
allowedCredentials
默认情况下不启用,因为这会建立信任级别
暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。
maxAge
设置为 30 分钟。
要在 WebFlux Java 配置中启用 CORS,您可以使用CorsRegistry
回调
如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
1.7.5. CORSWebFilter
您可以通过内置的CorsWebFilter
,它是一个
与功能端点拟合良好。
如果您尝试使用CorsFilter 使用 Spring Security,请记住 Spring
Security 具有对 CORS 的内置支持。 |
要配置过滤器,您可以声明CorsWebFilter
bean 并传递一个CorsConfigurationSource
添加到其构造函数中,如下例所示:
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
1.8. Web 安全
Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。请参阅 Spring Security 参考文档,包括:
1.9. 查看技术
Spring WebFlux 中视图技术的使用是可插拔的。无论您决定 使用 Lymeleaf、FreeMarker 或其他一些视图技术主要是 配置更改。本章介绍了与 Spring 集成的视图技术 WebFlux 的 Web Flux 中。我们假设您已经熟悉 View Resolution。
1.9.1. 百里香叶
Thymeleaf 是一个现代的服务器端 Java 模板引擎,强调自然 HTML 可以通过双击在浏览器中预览的模板,这非常 有助于独立处理 UI 模板(例如,由设计人员),而无需 正在运行的服务器。Thymeleaf 提供了一组广泛的功能,并且正在积极开发 并维持。有关更完整的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring WebFlux 的集成由 Thymeleaf 项目管理。这
配置涉及一些 bean 声明,例如SpringResourceTemplateResolver
,SpringWebFluxTemplateEngine
和ThymeleafReactiveViewResolver
.有关详细信息,请参阅 Thymeleaf+Spring 和 WebFlux 集成公告。
1.9.2. 自由标记
Apache FreeMarker 是一个模板引擎,用于生成任何 从 HTML 到电子邮件等的文本输出类型。Spring Framework 内置了 用于将 Spring WebFlux 与 FreeMarker 模板一起使用的集成。
View 配置
以下示例显示了如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates/freemarker")
}
}
您的模板需要存储在FreeMarkerConfigurer
,
如前面的示例所示。给定上述配置,如果您的控制器
返回视图名称,welcome
中,解析程序会查找classpath:/templates/freemarker/welcome.ftl
模板。
FreeMarker 配置
您可以将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarkerConfiguration
对象(由 Spring 管理)通过设置适当的 bean
属性FreeMarkerConfigurer
豆。这freemarkerSettings
property 需要
一个java.util.Properties
object 和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/>
元素。此元素主要允许表单显示来自
表单支持对象,并显示来自Validator
在
Web 或业务层。Spring 在 FreeMarker 中也支持相同的功能,
具有用于生成表单输入元素本身的附加便捷宏。
Bind 宏
一组标准的宏在spring-webflux.jar
文件
FreeMarker,因此它们始终可用于适当配置的应用程序。
Spring 模板库中定义的一些宏被认为是内部的
(私有),但宏定义中不存在此类范围,因此所有宏都可见
调用代码和用户模板。以下各节仅重点介绍宏
您需要直接从模板中调用。如果您想查看宏代码
直接调用该文件spring.ftl
,并且位于org.springframework.web.reactive.result.view.freemarker
包。
有关绑定支持的更多详细信息,请参阅简单 Spring MVC 的绑定。
1.9.3. 脚本视图
Spring Framework 有一个内置的集成,用于将 Spring WebFlux 与任何 可以在 JSR-223 Java 脚本引擎上运行的模板库。 下表显示了我们在不同脚本引擎上测试的模板库:
脚本库 | 脚本引擎 |
---|---|
集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngine 和Invocable 接口。 |
要求
您需要在 Classpath 上具有脚本引擎,其详细信息因脚本引擎而异:
-
Nashorn JavaScript 引擎随 Java 8+ 的强烈建议使用可用的最新更新版本。
-
应该将 JRuby 添加为 Ruby 支持的依赖项。
-
应将 Jython 添加为 Python 支持的依赖项。
-
org.jetbrains.kotlin:kotlin-script-util
dependency 和META-INF/services/javax.script.ScriptEngineFactory
包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
应添加 line 以支持 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
function 使用以下参数调用:
-
String template
:模板内容 -
Map model
:视图模型 -
RenderingContext renderingContext
:这RenderingContext
,可以访问应用程序上下文、语言环境、模板加载器和 URL(自 5.0 起)
Mustache.render()
与此签名本机兼容,因此您可以直接调用它。
如果您的模板技术需要一些自定义,则可以提供一个脚本,该脚本 实现自定义 render 函数。例如,Handlerbars 需要在使用模板之前编译模板,并且需要一个 polyfill 来模拟一些 浏览器工具在服务器端脚本引擎中不可用。 以下示例显示如何设置自定义 render 函数:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
设置sharedEngine property 设置为false 在使用非线程安全时是必需的
具有非并发性模板库的脚本引擎,例如 Handlebars 或
React 在 Nashorn 上运行。在这种情况下,由于此错误,需要 Java SE 8 Update 60,但通常是
建议在任何情况下使用最新的 Java SE 补丁版本。 |
polyfill.js
仅定义window
对象才能正常运行,
如以下代码段所示:
var window = {};
这个基本render.js
implementation 在使用模板之前对其进行编译。A 生产
ready 实现还应存储和重用缓存的模板或预编译的模板。
这可以在脚本端完成,也可以完成您需要的任何自定义(管理
模板引擎配置)。
以下示例显示了如何编译模板:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.9.4. JSON 和 XML
出于 Content Negotiation 目的,能够交替使用
使用 HTML 模板或其他格式(如 JSON 或 XML)渲染模型之间,
取决于客户端请求的内容类型。为了支持这样做,Spring WebFlux
提供HttpMessageWriterView
,可用于从spring-web
如Jackson2JsonEncoder
,Jackson2SmileEncoder
,
或Jaxb2XmlEncoder
.
与其他视图技术不同,HttpMessageWriterView
不需要ViewResolver
而是配置为默认视图。您可以
配置一个或多个这样的默认视图,包装不同的HttpMessageWriter
实例
或Encoder
实例。在运行时使用与请求的内容类型匹配的 URL。
在大多数情况下,一个模型包含多个属性。要确定要序列化的 Cookie,
您可以配置HttpMessageWriterView
替换为 model 属性的名称
渲染。如果模型仅包含一个属性,则使用该属性。
1.10. HTTP 缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存
围绕Cache-Control
响应标头和后续条件请求
标头,例如Last-Modified
和ETag
.Cache-Control
建议私有(例如,浏览器)
和 public (例如 proxy) caches,了解如何缓存和重用响应。一ETag
header 的
要发出可能导致 304 (NOT_MODIFIED) 没有正文的条件请求,
如果内容未更改。ETag
可以看作是
这Last-Modified
页眉。
本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。
1.10.1.CacheControl
CacheControl
提供支持
配置与Cache-Control
header 并被接受为参数
在许多地方:
虽然 RFC 7234 描述了所有可能的
指令的Cache-Control
response 标头中,CacheControl
type 接受
面向用例的方法,侧重于常见场景,如下例所示:
// 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
requests 中,您可以将响应设置为
304 (NOT_MODIFIED)。对于有条件的POST
,PUT
和DELETE
中,您可以改为设置响应
设置为 412 (PRECONDITION_FAILED) 以防止并发修改。
1.11. WebFlux 配置
WebFlux Java 配置声明了处理
请求,并且它提供了一个 API 来
自定义配置。这意味着您不需要了解底层
由 Java 配置创建的 bean。但是,如果您想了解它们,
您可以在WebFluxConfigurationSupport
或阅读更多关于它们是什么的信息
在 Special Bean Types 中。
对于配置 API 中不可用的更高级自定义,您可以 通过高级配置模式获得对配置的完全控制。
1.11.1. 启用 WebFlux 配置
您可以使用@EnableWebFlux
注解,如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig {
}
@Configuration
@EnableWebFlux
class WebConfig
前面的示例注册了许多 Spring WebFlux 基础结构 bean 并适应依赖项 available on the classpath — 用于 JSON、XML 等。
1.11.2. WebFlux 配置 API
在 Java 配置中,您可以实现WebFluxConfigurer
接口
如下例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// Implement configuration methods...
}
1.11.3. 转换、格式化
默认情况下,会安装各种数字和日期类型的格式化程序,以及支持
用于自定义@NumberFormat
和@DateTimeFormat
在字段上。
要在 Java 配置中注册自定义格式化程序和转换器,请使用以下内容:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
默认情况下,Spring WebFlux 在解析和格式化日期时会考虑请求 Locale 值。这适用于日期表示为带有 “input” 形式的字符串的表单 领域。但是,对于“日期”和“时间”表单字段,浏览器使用定义的固定格式 在 HTML 规范中。对于此类情况,可以按如下方式自定义日期和时间格式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
看FormatterRegistrar SPI 系列和FormattingConversionServiceFactoryBean 有关何时
用FormatterRegistrar 实现。 |
1.11.4. 验证
默认情况下,如果存在 Bean Validation
在 Classpath(例如,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
header 中,
但您也可以启用基于查询参数的策略。
以下示例显示如何自定义请求的内容类型解析:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
// ...
}
}
1.11.6. HTTP 消息编解码器
以下示例显示如何自定义请求和响应正文的读取和写入方式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(512 * 1024);
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
}
}
ServerCodecConfigurer
提供一组默认读取器和写入器。您可以使用它来添加
更多读取器和写入器,自定义默认读取器和写入器,或完全替换默认读取器和写入器。
对于 Jackson JSON 和 XML,请考虑使用Jackson2ObjectMapperBuilder
,
,它使用以下属性自定义 Jackson 的默认属性:
如果在 Classpath 中检测到以下众所周知的模块,它还会自动注册它们:
-
jackson-datatype-joda
:支持 Joda-Time 类型。 -
jackson-datatype-jsr310
:支持 Java 8 日期和时间 API 类型。 -
jackson-datatype-jdk8
:支持其他 Java 8 类型,例如Optional
. -
jackson-module-kotlin
:支持 Kotlin 类和数据类。
1.11.7. 查看解析器
以下示例显示如何配置视图分辨率:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// ...
}
}
这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
}
}
支持 Content Negotiation 和呈现其他格式
通过视图分辨率(除了 HTML),您可以配置一个或多个基于
在HttpMessageWriterView
implementation 的 API 中,它接受spring-web
.以下示例显示了如何执行此作:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = Jackson2JsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
// ...
}
有关与 Spring WebFlux 集成的视图技术的更多信息,请参见 View Technologies。
1.11.8. 静态资源
此选项提供了一种从列表中提供静态资源的便捷方法Resource
基于位置。
在下一个示例中,给定一个以/resources
,则相对路径为
用于查找和提供相对于/static
在 Classpath 上。资源
提供一年的未来到期时间,以确保最大限度地使用浏览器缓存
以及减少浏览器发出的 HTTP 请求。这Last-Modified
header 也是
已评估,如果存在,则为304
返回 status code。以下列表显示
示例:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
}
}
资源处理程序还支持ResourceResolver
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 中,没有办法透明地重写 static
资源 URL,因为没有可以使用非阻塞链的视图技术
的旋转转换器。当仅提供本地资源时,解决方法是使用ResourceUrlProvider
直接 (例如,通过自定义元素) 和 Block 进行。
请注意,当同时使用EncodedResourceResolver
(例如,Gzip、Brotli 编码)和VersionedResourceResolver
,它们必须按该顺序注册,以确保基于内容的
版本始终基于未编码的文件进行可靠计算。
WebJar 也通过WebJarsResourceResolver
当org.webjars:webjars-locator-core
library 存在于 Classpath 中。解析程序可以
重写 URL 以包含 jar 的版本,并且还可以与传入的 URL 匹配
没有版本 — 例如,从/jquery/jquery.min.js
自/jquery/1.2.0/jquery.min.js
.
1.11.9. 路径匹配
您可以自定义与路径匹配相关的选项。有关各个选项的详细信息,请参阅PathMatchConfigurer
javadoc 的 Java 文档。
以下示例演示如何使用PathMatchConfigurer
:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
}
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController::class.java))
}
}
Spring WebFlux 依赖于请求路径的解析表示,称为 Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,在 Spring MVC 中,我们 也建议远离 依赖它。 |
1.11.10. 高级配置模式
@EnableWebFlux
进口DelegatingWebFluxConfiguration
那:
-
为 WebFlux 应用程序提供默认的 Spring 配置
-
检测并委托给
WebFluxConfigurer
实现来自定义该配置。
对于高级模式,您可以删除@EnableWebFlux
并直接从DelegatingWebFluxConfiguration
而不是实现WebFluxConfigurer
,
如下例所示:
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {
// ...
}
您可以将现有方法保留在WebConfig
,但您现在也可以覆盖 Bean 声明
从基类中,并且仍然具有任意数量的其他WebMvcConfigurer
上的 implementations
类路径。
1.12. HTTP/2 协议
Reactor Netty、Tomcat、Jetty 和 Undertow 支持 HTTP/2。但是,有 与 Server 配置相关的注意事项。有关更多详细信息,请参阅 HTTP/2 wiki 页面。
2. 网页客户端
Spring WebFlux 包括一个反应式的、非阻塞的WebClient
用于 HTTP 请求。客户端
具有一个功能齐全的 Fluent API,其中包含用于声明式组合的反应式类型,请参阅 反应式库。WebFlux 客户端和服务器依赖于
用于编码和解码请求的相同非阻塞编解码器
和响应内容。
内部WebClient
委托给 HTTP 客户端库。默认情况下,它使用 Reactor Netty,内置了对
Jetty 响应式 HttpClient,
而其他可以通过ClientHttpConnector
.
2.1. 配置
创建WebClient
是通过 static 工厂方法之一:
-
WebClient.create()
-
WebClient.create(String baseUrl)
上述方法使用 Reactor NettyHttpClient
使用默认设置和 EXPECTio.projectreactor.netty:reactor-netty
以位于 Classpath 上。
您还可以使用WebClient.builder()
有更多选项:
-
uriBuilderFactory
:定制UriBuilderFactory
以用作基本 URL。 -
defaultHeader
:每个请求的标头。 -
defaultCookie
:每个请求的 Cookie。 -
defaultRequest
:Consumer
自定义每个请求。 -
filter
:每个请求的客户端筛选器。 -
exchangeStrategies
:HTTP 消息读取器/写入器自定义。 -
clientConnector
:HTTP 客户端库设置。
以下示例配置 HTTP 编解码器:
WebClient client = WebClient.builder()
.exchangeStrategies(builder -> {
return builder.codecs(codecConfigurer -> {
//...
});
})
.build();
val webClient = WebClient.builder()
.exchangeStrategies { strategies ->
strategies.codecs {
//...
}
}
.build()
构建完成后,一个WebClient
instance 是不可变的。但是,您可以克隆它并构建一个
修改后的 copy 而不影响原始实例,如下例所示:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
2.1.1. MaxInMemorySize (最大内存大小)
Spring WebFlux 配置缓冲限制 数据在编解码器中,以避免应用程序内存问题。默认情况下,这是 配置为 256KB,如果这还不够满足您的使用案例,您将看到以下内容:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
您可以使用以下代码示例在所有默认编解码器上配置此限制:
WebClient webClient = WebClient.builder()
.exchangeStrategies(builder ->
builder.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();
val webClient = WebClient.builder()
.exchangeStrategies { builder ->
builder.codecs {
it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
}
}
.build()
2.1.2. 反应堆 Netty
要自定义 Reactor Netty 设置,只需提供预配置的HttpClient
:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
资源
默认情况下,HttpClient
参与全球 Reactor Netty 资源持有reactor.netty.http.HttpResources
,包括事件循环线程和连接池。
这是推荐的模式,因为固定的共享资源是事件循环的首选
并发。在此模式下,全局资源将保持活动状态,直到进程退出。
如果服务器与进程定时,则通常不需要显式
关闭。但是,如果服务器可以在进程内启动或停止(例如,Spring MVC
application 部署为 WAR),则可以声明ReactorResourceFactory
跟globalResources=true
(默认)来确保 Reactor
Netty 全局资源在 SpringApplicationContext
已关闭,
如下例所示:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
您也可以选择不参与全局 Reactor Netty 资源。然而 在这种模式下,您有责任确保所有 Reactor Netty 客户端和服务器 实例使用共享资源,如下例所示:
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false); (1)
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper); (2)
return WebClient.builder().clientConnector(connector).build(); (3)
}
1 | 创建独立于全局资源的资源。 |
2 | 使用ReactorClientHttpConnector 构造函数。 |
3 | 将连接器插入WebClient.Builder . |
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false (1)
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Further customizations...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)
return WebClient.builder().clientConnector(connector).build() (3)
}
1 | 创建独立于全局资源的资源。 |
2 | 使用ReactorClientHttpConnector 构造函数。 |
3 | 将连接器插入WebClient.Builder . |
超时
要配置连接超时:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}
要配置读取和/或写入超时值:
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create().tcpConfiguration {
it.doOnConnected { conn -> conn
.addHandlerLast(ReadTimeoutHandler(10))
.addHandlerLast(WriteTimeoutHandler(10))
}
}
2.1.3. Jetty
以下示例显示如何自定义 JettyHttpClient
设置:
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
val httpClient = HttpClient()
httpClient.cookieStore = ...
val connector = JettyClientHttpConnector(httpClient)
val webClient = WebClient.builder().clientConnector(connector).build();
默认情况下,HttpClient
创建自己的资源 (Executor
,ByteBufferPool
,Scheduler
),
在进程退出之前保持活动状态,或者stop()
被调用。
您可以在 Jetty 客户端(和服务器)的多个实例之间共享资源,并且
确保在 SpringApplicationContext
由
声明 Spring 管理的 Bean 类型JettyResourceFactory
,如下例所示
显示:
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory()); (1)
return WebClient.builder().clientConnector(connector).build(); (2)
}
1 | 使用JettyClientHttpConnector 构造函数。 |
2 | 将连接器插入WebClient.Builder . |
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further customizations...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)
return WebClient.builder().clientConnector(connector).build() (2)
}
1 | 使用JettyClientHttpConnector 构造函数。 |
2 | 将连接器插入WebClient.Builder . |
2.2.retrieve()
这retrieve()
method 是获取响应正文并对其进行解码的最简单方法。
以下示例显示了如何执行此作:
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()
您还可以获取从响应中解码的对象流,如下例所示:
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()
默认情况下,具有 4xx 或 5xx 状态代码的响应会导致WebClientResponseException
或其特定于 HTTP 状态的子类之一,例如WebClientResponseException.BadRequest
,WebClientResponseException.NotFound
等。
您还可以使用onStatus
方法来自定义生成的异常,
如下例所示:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()
什么时候onStatus
,如果预期响应包含内容,则onStatus
callback 应该使用它。否则,内容将自动排空以确保
资源被释放。
2.3.exchange()
这exchange()
方法提供的控制比retrieve
方法。以下示例是等效的
自retrieve()
但也提供对ClientResponse
:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.awaitBody<Person>()
在此级别,您还可以创建完整的ResponseEntity
:
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.awaitExchange()
.toEntity<Person>()
请注意,(与retrieve()
),其中exchange()
,则没有
4xx 和 5xx 响应。您必须检查状态代码并决定如何继续。
与 |
2.4. 请求体
请求正文可以从ReactiveAdapterRegistry
,
喜欢Mono
或 Kotlin 协程Deferred
如下例所示:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
您还可以对对象流进行编码,如下例所示:
Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()
或者,如果您有实际值,则可以使用bodyValue
快捷方法,
如下例所示:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()
2.4.1. 表单数据
要发送表单数据,您可以提供MultiValueMap<String, String>
作为 body 进行。请注意,
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. Multipart 数据
要发送多部分数据,您需要提供MultiValueMap<String, ?>
其值为
也Object
表示部分内容的实例或HttpEntity
表示内容和
Headers 的 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", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
在大多数情况下,您不必指定Content-Type
对于每个部分。内容
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-样式,通过内置的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()
过滤器全局应用于每个请求。更改特定
request 中,您可以将 request 属性添加到ClientRequest
然后可以访问
按链中的所有筛选器,如下例所示:
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.awaitBody<Unit>()
您还可以复制现有的WebClient
、插入新滤镜或已删除
已注册的过滤器。以下示例在
索引 0:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
2.6. 同步使用
WebClient
可以通过在末尾阻塞来以同步样式使用结果:
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}
但是,如果需要进行多个调用,则避免在每个调用上阻塞会更有效 response 中,而是等待组合的结果:
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
val data = runBlocking {
val personDeferred = async {
client.get().uri("/person/{id}", personId)
.retrieve().awaitBody<Person>()
}
val hobbiesDeferred = async {
client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlow<Hobby>().toList()
}
mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
}
以上只是一个例子。还有许多其他 pattern 和运算符可用于放置 一起进行许多远程调用(可能是一些嵌套的 相互依赖,直到最后都没有阻塞。
跟 |
2.7. 测试
要测试使用WebClient
,您可以使用模拟 Web 服务器,例如 OkHttp MockWebServer。查看示例
的用途,请查看WebClientIntegrationTests
在 Spring Framework 测试套件中,或者使用static-server
示例。
3. 网络套接字
参考文档的这一部分涵盖了对反应式堆栈 WebSocket 的支持 消息。
3.1. WebSocket 简介
WebSocket 协议 RFC 6455 提供了标准化的 在 Client 端和 Server 之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与 HTTP 不同的 TCP 协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重复使用现有防火墙规则。
WebSocket 交互以使用 HTTPUpgrade
页眉
进行升级,或者在本例中切换到 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 套接字将保留 open 以继续发送和接收消息。
有关 WebSockets 工作原理的完整介绍超出了本文档的范围。 请参阅 RFC 6455、HTML5 的 WebSocket 章节或许多介绍中的任何一个 Web 上的教程。
请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则 可能需要对其进行配置,以便将 WebSocket 升级请求传递给 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查 云提供商与 WebSocket 支持相关的说明。
3.1.1. HTTP 与 WebSocket
尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头, 重要的是要了解这两种协议导致非常不同的结果 体系结构和应用程序编程模型。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。要与应用程序交互, 客户端以请求-响应样式访问这些 URL。服务器将请求路由到 基于 HTTP URL、方法和标头的适当处理程序。
相比之下,在 WebSockets 中,初始连接通常只有一个 URL。 随后,所有应用程序消息都在同一 TCP 连接上流动。这指向 一种完全不同的异步、事件驱动的消息传递架构。
WebSocket 也是一种低级传输协议,与 HTTP 不同,它没有规定 消息内容的任何语义。这意味着无法路由或处理 一条消息,除非客户端和服务器在消息语义上达成一致。
WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议
(例如,STOMP),通过Sec-WebSocket-Protocol
标头。
如果没有这些,他们需要提出自己的惯例。
3.1.2. 何时使用 WebSockets
WebSockets 可以使网页具有动态和交互性。但是,在许多情况下, Ajax 和 HTTP 流式处理或长轮询的组合可以提供简单且 有效的解决方案。
例如,新闻、邮件和社交源需要动态更新,但可能需要 每隔几分钟这样做一次完全可以。协作、游戏和金融应用程序 另一方面,需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如, 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 低延迟、高频率和高容量的组合造就了最好的 case 来使用 WebSocket。
另请记住,在 Internet 上,您无法控制的限制性代理
可能会排除 WebSocket 交互,因为它们未配置为传递Upgrade
标头,或者因为它们关闭了看起来空闲的长期连接。这
意味着将 WebSocket 用于防火墙内的内部应用程序是
比面向公众的应用程序更直接的决定。
3.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写 client- 和 处理 WebSocket 消息的服务器端应用程序。
3.2.1. 服务器
要创建 WebSocket 服务器,您可以首先创建一个WebSocketHandler
.
以下示例显示了如何执行此作:
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
public class MyWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
// ...
}
}
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}
然后,您可以将其映射到 URL 并添加WebSocketHandlerAdapter
,如下例所示:
@Configuration
class WebConfig {
@Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/path", new MyWebSocketHandler());
int order = -1; // before annotated controllers
return new SimpleUrlHandlerMapping(map, order);
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
3.2.2.WebSocketHandler
这handle
method 的WebSocketHandler
需要WebSocketSession
并返回Mono<Void>
以指示应用程序对会话的处理何时完成。会话已处理
通过两个流,一个用于入站消息,一个用于出站消息。下表
介绍处理流的两种方法:
WebSocketSession 方法 |
描述 |
---|---|
|
提供对入站消息流的访问,并在连接关闭时完成。 |
|
获取传出消息的源,写入消息,并返回一个 |
一个WebSocketHandler
必须将入站和出站流组合成一个统一的流,并且
返回一个Mono<Void>
这反映了该流程的完成。取决于应用
要求,则统一流将在以下情况下完成:
-
入站或出站消息流完成。
-
入站流完成(即连接关闭),而出站流是无限的。
-
在选定的点,通过
close
method 的WebSocketSession
.
当入站和出站消息流组合在一起时,无需 检查连接是否打开,因为 Reactive Streams 向 end activity 发出信号。 入站流接收到完成或错误信号,出站流 接收取消信号。
处理程序的最基本实现是处理入站流的处理程序。这 以下示例显示了此类实现:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() (1)
.doOnNext(message -> {
// ... (2)
})
.concatMap(message -> {
// ... (3)
})
.then(); (4)
}
}
1 | 访问入站消息流。 |
2 | 对每条消息执行一些作。 |
3 | 执行使用消息内容的嵌套异步作。 |
4 | 返回一个Mono<Void> ,在 receiving complete 时完成。 |
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> ,在 receiving complete 时完成。 |
对于嵌套的异步作,您可能需要调用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 的
该参考在 Data Buffers and Codecs 一节中有更多关于这方面的内容。要理解的关键点是,在某些
像 Netty 这样的服务器,字节缓冲区是池化的和引用计数的,并且必须释放
当使用以避免内存泄漏时。
在 Netty 上运行时,应用程序必须使用DataBufferUtils.retain(dataBuffer)
如果他们
希望保留 input 数据缓冲区以确保它们不会被释放,并且
后续使用DataBufferUtils.release(dataBuffer)
当缓冲区被消耗时。
3.2.4. 握手
WebSocketHandlerAdapter
delegates 传递给WebSocketService
.默认情况下,这是一个实例
之HandshakeWebSocketService
执行基本检查,它对 WebSocket 请求执行基本检查,而
然后使用RequestUpgradeStrategy
对于正在使用的服务器。目前,有内置的
支持 Reactor Netty、Tomcat、Jetty 和 Undertow。
HandshakeWebSocketService
暴露一个sessionAttributePredicate
允许
将Predicate<String>
要从WebSession
并插入它们
添加到WebSocketSession
.
3.2.5. 服务器配置
这RequestUpgradeStrategy
为每个服务器公开 WebSocket 相关的配置
选项。以下示例将
在 Tomcat 上运行时的 WebSocket 选项:
@Configuration
class WebConfig {
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter(webSocketService());
}
@Bean
public WebSocketService webSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
@Configuration
class WebConfig {
@Bean
fun handlerAdapter() =
WebSocketHandlerAdapter(webSocketService())
@Bean
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
检查服务器的升级策略以查看可用的选项。现在 只有 Tomcat 和 Jetty 公开了此类选项。
3.2.6. CORS
配置 CORS 并限制对 WebSocket 终端节点的访问的最简单方法是
拥有您的WebSocketHandler
实现CorsConfigurationSource
并返回一个CorsConfiguraiton
包含允许的来源、标头和其他详细信息。如果你做不到
这样,您还可以设置corsConfigurations
属性SimpleUrlHandler
自
通过 URL 模式指定 CORS 设置。如果同时指定了这两者,则使用combine
method 开启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 客户端的 Web 节点。
4. 测试
这spring-test
模块提供了ServerHttpRequest
,ServerHttpResponse
和ServerWebExchange
.
请参阅 Spring Web Reactive 以获取
对 mock 对象的讨论。
WebTestClient
构建在这些 mock 请求和
response 对象,以支持在没有 HTTP 的情况下测试 WebFlux 应用程序
服务器。您可以使用WebTestClient
也适用于端到端集成测试。
5. RS锁
本节描述了 Spring 框架对 RSocket 协议的支持。
5.1. 概述
RSocket 是一种用于通过 TCP 进行多路复用、双工通信的应用程序协议, WebSocket 和其他字节流传输,使用以下交互之一 模型:
-
Request-Response
— 发送一条消息,然后接收一条消息。 -
Request-Stream
— 发送一条消息并接收回消息流。 -
Channel
— 双向发送消息流。 -
Fire-and-Forget
— 发送单向消息。
建立初始连接后,“client” 与 “server” 的区别将丢失,因为 双方都变得对称,并且每一方都可以发起上述交互之一。 这就是为什么在协议中将参与方称为 “requester” 和 “responder” 的原因 而上述交互称为 “请求流” 或简称为 “请求”。
以下是 RSocket 协议的主要功能和优势:
-
跨网络边界的 Reactive Streams 语义 — 对于流式请求,例如
Request-Stream
和Channel
、背压信号 在请求者和响应者之间移动,允许请求者在 源,从而减少对网络层拥塞控制的依赖,以及需求 用于网络级别或任何级别的缓冲。 -
请求限制 — 此功能被命名为 “租赁” ,以
LEASE
frame 那个 可以从每一端发送,以限制另一端允许的请求总数 在给定的时间内。租约会定期续订。 -
会话恢复 — 这是为连接丢失而设计的,需要一些状态 待维护。状态管理对应用程序是透明的,并且运行良好 结合背压,可以在可能的情况下停止生产者并减少 所需的状态量。
-
大型消息的分片和重新汇编。
-
Keepalive (检测信号)。
RSocket 具有多种语言的实现。Java 库构建在 Project Reactor 之上, 和 Reactor Netty 用于运输。这意味着 应用程序中来自 Reactive Streams Publishers 的信号以透明方式传播 通过 RSocket 跨网络。
5.1.1. 协议
RSocket 的好处之一是它在 wire 上具有明确定义的行为,并且 易于阅读的规范以及一些协议扩展。因此它是 阅读规范是个好主意,独立于语言实现和更高级别 框架 API 的 API 中。本节提供了简洁的概述,以建立一些上下文。
连接
最初,客户端通过一些低级流传输(如
作为 TCP 或 WebSocket 发送,并发送一个SETUP
frame 添加到服务器中,为
连接。
服务器可能会拒绝SETUP
frame,但通常在发送之后(对于客户端)
和 received(对于服务器),双方都可以开始发出请求,除非SETUP
指示使用租赁语义来限制请求数,在这种情况下
双方都必须等待LEASE
frame 以允许发出请求。
发出请求
建立连接后,双方都可以通过
框架REQUEST_RESPONSE
,REQUEST_STREAM
,REQUEST_CHANNEL
或REQUEST_FNF
.每个
这些帧将一条消息从请求者传送到响应者。
然后,响应方可以返回PAYLOAD
帧,并且在这种情况下
之REQUEST_CHANNEL
请求者还可以发送PAYLOAD
具有更多请求的帧
消息。
当请求涉及消息流(如Request-Stream
和Channel
,
响应方必须遵循来自请求方的需求信号。Demand 表示为
消息数。初始需求在REQUEST_STREAM
和REQUEST_CHANNEL
框架。后续需求通过REQUEST_N
框架。
每一方还可以通过METADATA_PUSH
frame 的
与任何单个请求有关,但与整个连接有关。
消息格式
RSocket 消息包含数据和元数据。元数据可用于发送路由、
证券令牌等数据和元数据的格式可以不同。每个 Mime 类型
在SETUP
frame 并应用于给定连接上的所有请求。
虽然所有消息都可以包含元数据,但通常元数据(如路由)是按请求进行的
因此仅包含在请求的第一条消息中,即与其中一个帧一起REQUEST_RESPONSE
,REQUEST_STREAM
,REQUEST_CHANNEL
或REQUEST_FNF
.
协议扩展定义用于应用程序的常见元数据格式:
5.1.2. Java 实现
RSocket 的 Java 实现构建在 Project Reactor 之上。TCP 和 WebSocket 的传输方式是
基于 Reactor Netty 构建。作为反应式流
库,Reactor 简化了实现协议的工作。对于应用程序,它是
天生的使用Flux
和Mono
使用声明式运算符和透明 back
压力支持。
RSocket Java 中的 API 有意做到最小和基本。它侧重于协议 功能,并将应用程序编程模型(例如 RPC codegen 与其他模型)保留为 更高层次,独立关注。
主合约 io.rsocket.RSocket 使用Mono
表示
单条消息 /Flux
消息流,以及io.rsocket.Payload
实际的
message 中访问数据和元数据作为字节缓冲区。这RSocket
使用 Contract
对称。对于请求,应用程序将获得一个RSocket
执行
请求与。为了响应,应用程序实现了RSocket
处理请求。
这并不是一个详尽的介绍。在大多数情况下,Spring 应用程序 不必直接使用其 API。但是,观察或试验可能很重要 使用 RSocket 独立于 Spring。RSocket Java 存储库包含许多示例应用程序,这些应用程序 演示其 API 和协议功能。
5.1.3. Spring 支持
这spring-messaging
module 包含以下内容:
-
RSocketRequester — 通过
io.rsocket.RSocket
使用数据和元数据编码/解码。 -
带注释的响应者 —
@MessageMapping
的带注释的处理程序方法 响应。
这spring-web
module 包含Encoder
和Decoder
Jackson 等实现
CBOR/JSON 和 Protobuf 的 RSocket 应用程序可能需要。它还包含PathPatternParser
可以插入以进行高效的路由匹配。
Spring Boot 2.2 支持通过 TCP 或 WebSocket 建立 RSocket 服务器,包括
在 WebFlux 服务器中通过 WebSocket 公开 RSocket 的选项。还有 Client
支持和自动配置RSocketRequester.Builder
和RSocketStrategies
.
有关更多详细信息,请参阅 Spring Boot 参考中的 RSocket 部分。
Spring Security 5.2 提供了 RSocket 支持。
Spring 集成 5.2 提供了入站和出站网关来与 RSocket 交互 客户端和服务器。有关更多详细信息,请参见 Spring 集成参考手册。
Spring Cloud 网关支持 RSocket 连接。
5.2. RSocketRequester
RSocketRequester
提供 Fluent API 来执行 RSocket 请求、接受和
返回 data 和 metadata 的对象,而不是低级数据缓冲区。可以使用
对称地,从 Client 端发出请求,以及从 Server 发出请求。
5.2.1. 客户端请求者
要获取RSocketRequester
在客户端需要连接到服务器以及
准备并发送初始 RSocketSETUP
框架。RSocketRequester
提供
builder 为此。在内部,它建立在io.rsocket.core.RSocketConnector
.
这是使用默认设置进行连接的最基本方法:
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectTcp("localhost", 7000);
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectWebSocket(URI.create("https://example.org:8080/rsocket"));
import org.springframework.messaging.rsocket.connectTcpAndAwait
import org.springframework.messaging.rsocket.connectWebSocketAndAwait
val requester = RSocketRequester.builder()
.connectTcpAndAwait("localhost", 7000)
val requester = RSocketRequester.builder()
.connectWebSocketAndAwait(URI.create("https://example.org:8080/rsocket"))
以上是延迟的。要实际连接并使用请求者:
// Connect asynchronously
RSocketRequester.builder().connectTcp("localhost", 7000)
.subscribe(requester -> {
// ...
});
// Or block
RSocketRequester requester = RSocketRequester.builder()
.connectTcp("localhost", 7000)
.block(Duration.ofSeconds(5));
// Connect asynchronously
import org.springframework.messaging.rsocket.connectTcpAndAwait
class MyService {
private var requester: RSocketRequester? = null
private suspend fun requester() = requester ?:
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000).also { requester = it }
suspend fun doSomething() = requester().route(...)
}
// Or block
import org.springframework.messaging.rsocket.connectTcpAndAwait
class MyService {
private val requester = runBlocking {
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000)
}
suspend fun doSomething() = requester.route(...)
}
连接设置
RSocketRequester.Builder
提供了以下内容来自定义初始SETUP
框架:
-
dataMimeType(MimeType)
— 设置连接上数据的 MIME 类型。 -
metadataMimeType(MimeType)
— 设置连接上元数据的 MIME 类型。 -
setupData(Object)
- 要包含在SETUP
. -
setupRoute(String, Object…)
— 路由到元数据中以包含在SETUP
. -
setupMetadata(Object, MimeType)
— 要包含在SETUP
.
对于 data,默认 MIME 类型派生自第一个配置的Decoder
.为
metadata,则默认的 MIME 类型是 composite metadata,它允许多个
每个请求的元数据值和 MIME 类型对。通常,两者都不需要更改。
Data and metadata 中的SETUP
frame 是可选的。在服务器端,可以使用@ConnectMapping方法处理
connection 和SETUP
框架。元数据可用于连接
级别安全性。
策略
RSocketRequester.Builder
接受RSocketStrategies
以配置请求者。
您需要使用它来提供编码器和解码器,用于数据的 (de) 序列化和
metadata 值。默认情况下,只有spring-core
为String
,byte[]
和ByteBuffer
已注册。添加spring-web
提供对更多
可以按如下方式注册:
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcp("localhost", 7000);
import org.springframework.messaging.rsocket.connectTcpAndAwait
val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcpAndAwait("localhost", 7000)
RSocketStrategies
专为重复使用而设计。在某些情况下,例如客户端和服务器在
相同的应用程序,最好在 Spring 配置中声明它。
客户端响应方
RSocketRequester.Builder
可用于配置对来自
服务器。
您可以使用带注释的处理程序进行基于相同的客户端响应 在服务器上使用但以编程方式注册的基础结构,如下所示:
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();
SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.connectTcp("localhost", 7000);
1 | 用PathPatternRouteMatcher 如果spring-web 存在,以实现高效
路由匹配。 |
2 | 从类创建响应者@MessageMaping 和/或@ConnectMapping 方法。 |
3 | 注册响应方。 |
import org.springframework.messaging.rsocket.connectTcpAndAwait
val strategies = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher()) (1)
.build()
val responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(responder) } (3)
.connectTcpAndAwait("localhost", 7000)
1 | 用PathPatternRouteMatcher 如果spring-web 存在,以实现高效
路由匹配。 |
2 | 从类创建响应者@MessageMaping 和/或@ConnectMapping 方法。 |
3 | 注册响应方。 |
请注意,以上只是专为 client 的编程注册而设计的快捷方式
反应。对于客户端响应者处于 Spring 配置中的替代场景,
您仍然可以声明RSocketMessageHandler
作为 Spring Bean,然后按如下方式应用:
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.connectTcp("localhost", 7000);
import org.springframework.beans.factory.getBean
import org.springframework.messaging.rsocket.connectTcpAndAwait
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.connectTcpAndAwait("localhost", 7000)
对于上述内容,您可能还需要使用setHandlerPredicate
在RSocketMessageHandler
自
切换到不同的策略来检测客户端响应者,例如基于自定义
注解(例如@RSocketClientResponder
与默认值@Controller
.这
在客户端和服务器或多个客户端位于同一
应用。
另请参阅 Annotated Responders ,以了解有关编程模型的更多信息。
高深
RSocketRequesterBuilder
提供一个回调来暴露底层io.rsocket.core.RSocketConnector
有关 Keepalive 的更多配置选项
intervals、session resumption、interceptor 等。您可以配置选项
在该级别,如下所示:
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.connectTcp("localhost", 7000);
import org.springframework.messaging.rsocket.connectTcpAndAwait
val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}.connectTcpAndAwait("localhost", 7000)
5.2.2. 服务器请求者
要从服务器向连接的客户端发出请求,只需获取 来自服务器的已连接客户端的 requester。
在 Annotated Responders 中,@ConnectMapping
和@MessageMapping
方法支持RSocketRequester
论点。使用它来访问连接的请求者。保留
请注意@ConnectMapping
方法本质上是SETUP
frame which
必须在请求开始之前处理。因此,请求在一开始就必须是
与处理分离。例如:
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { (1)
// ...
});
return ... (2)
}
1 | 异步启动请求,独立于处理。 |
2 | 执行处理和退货完成Mono<Void> . |
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
// ...
}
}
/// ... (2)
}
1 | 异步启动请求,独立于处理。 |
2 | 在 suspending 函数中执行处理。 |
5.2.3. 请求
ViewBox viewBox = ... ;
Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlux(AirportLocation.class); (3)
1 | 指定要包含在请求消息元数据中的路由。 |
2 | 为请求消息提供数据。 |
3 | 声明预期的响应。 |
val viewBox: ViewBox = ...
val locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlow<AirportLocation>() (3)
1 | 指定要包含在请求消息元数据中的路由。 |
2 | 为请求消息提供数据。 |
3 | 声明预期的响应。 |
交互类型由输入的基数隐式确定,而
输出。上面的示例是一个Request-Stream
因为发送了一个值和一个流
of values 的值。在大多数情况下,您不需要考虑这个问题,只要
输入和输出的选择与 RSocket 交互类型以及 input 和
响应方所需的输出。无效组合的唯一示例是多对一。
这data(Object)
method 也接受任何 Reactive StreamsPublisher
包括Flux
和Mono
,以及在ReactiveAdapterRegistry
.对于多值Publisher
如Flux
,这将产生
相同类型的值,请考虑使用重载的data
避免
type checks 和Encoder
lookup on each element:
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
仅指示消息已成功发送,而不指示消息已处理。
5.3. 带注释的响应者
RSocket 响应程序可以实现为@MessageMapping
和@ConnectMapping
方法。@MessageMapping
方法处理单个请求,而@ConnectMapping
方法 handle
连接级事件 (Setup 和 Metadata Push)。支持带注释的响应者
对称地,用于从服务器端响应和从客户端响应。
5.3.1. 服务器响应器
要在服务器端使用带注释的响应程序,请添加RSocketMessageHandler
到你的 Spring
要检测的配置@Controller
bean 替换为@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))
.awaitFirst()
您需要设置Encoder
和Decoder
元数据和数据所需的实例
格式。您可能需要spring-web
module 进行编码实现。
默认情况下SimpleRouteMatcher
用于通过AntPathMatcher
.
我们建议插入PathPatternRouteMatcher
从spring-web
为
高效的路由匹配。RSocket 路由可以是分层的,但不是 URL 路径。
默认情况下,两个路由匹配器都配置为使用 “.” 作为分隔符,并且没有 URL
解码方式与 HTTP URL 一样。
RSocketMessageHandler
可通过以下方式进行配置RSocketStrategies
这可能很有用,如果
您需要在同一进程中在 Client 端和 Server 之间共享配置:
@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 中所述。 |
返回值应为一个或多个要序列化为响应的 Object
负载。这可以是异步类型,例如Mono
或Flux
、具体值或
也void
或无值异步类型,例如Mono<Void>
.
RSocket 交互类型中,一个@MessageMapping
方法 Supports 的确定依据
input 的基数(即@Payload
参数)和输出,其中
cardinality 的含义如下:
基数 | 描述 |
---|---|
1 |
显式值或单值异步类型(如 |
多 |
多值异步类型,例如 |
0 |
对于 input,这意味着该方法没有 对于输出,此值为 |
下表显示了所有输入和输出基数组合以及相应的 交互类型:
输入基数 | 输出基数 | 交互类型 |
---|---|---|
0, 1 |
0 |
Fire-and-Forget, Request-Response |
0, 1 |
1 |
请求-响应 |
0, 1 |
多 |
请求流 |
多 |
0、1、多 |
请求通道 |
5.3.4. @ConnectMapping
@ConnectMapping
处理SETUP
frame 在 RSocket 连接开始时,以及
通过METADATA_PUSH
frame 的metadataPush(Payload)
在io.rsocket.RSocket
.
@ConnectMapping
方法支持与 @MessageMapping 相同的参数,但基于元数据和数据SETUP
和METADATA_PUSH
框架。@ConnectMapping
可以有一个模式来缩小处理范围
在元数据中具有路由的特定连接,或者如果未声明任何模式
则所有连接都匹配。
@ConnectMapping
方法不能返回数据,必须使用void
或Mono<Void>
作为返回值。如果处理返回新的
connection,则连接将被拒绝。处理不得搁置以使
请求发送到RSocketRequester
对于连接。有关详细信息,请参阅 Server Requester 。
5.4. 元数据提取器
响应方必须解释元数据。复合元数据允许独立 格式化的元数据值(例如,用于路由、安全、跟踪),每个值都有自己的 MIME 类型。应用程序需要一种方法来配置元数据 MIME 类型以支持,以及一种方法 以访问提取的值。
MetadataExtractor
是一个合约,用于获取序列化元数据并返回解码
名称-值对,然后可以像 Headers 一样按名称访问,例如通过@Header
在带注释的处理程序方法中。
DefaultMetadataExtractor
可以给出Decoder
实例来解码元数据。出
它内置了对 “message/x.rsocket.routing.v0” 的支持,它将其解码为该框String
并保存在 “route” 键下。对于任何其他 mime 类型,您需要提供
一个Decoder
并注册 MIME 类型,如下所示:
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
复合元数据可以很好地组合独立的元数据值。但是,
请求者可能不支持复合元数据,或者可能选择不使用它。为此,DefaultMetadataExtractor
可能需要自定义逻辑来将解码的值映射到输出
地图。以下是将 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
并在内部使用它来编写异步
logic 并提供 Reactive Streams 支持。通常,WebFlux API 返回Flux
或Mono
(因为它们是内部使用的)并宽容地接受任何 Reactive StreamsPublisher
implementation 作为输入。的使用Flux
对Mono
很重要,因为
它有助于表达基数 — 例如,无论是单个还是多个异步
值,这对于做出决策可能是必不可少的(例如,当
编码或解码 HTTP 消息)。
对于带注释的控制器,WebFlux 透明地适应由
应用程序。这是在ReactiveAdapterRegistry
哪
为反应式库和其他异步类型提供可插拔支持。注册表
内置了对 RxJava 的支持,并且CompletableFuture
,但您也可以注册其他
对于功能性 API(例如功能性端点,WebClient
等)、一般规则
对于 WebFlux API,适用 —Flux
和Mono
作为返回值和一个 Reactive StreamsPublisher
作为输入。当Publisher
,无论是自定义的还是来自另一个响应式库,
,则只能将其视为语义未知 (0..N) 的流。但是,如果
语义是已知的,你可以用Flux
或Mono.from(Publisher)
相反
传递原始Publisher
.
例如,给定一个Publisher
那不是Mono
、Jackson JSON 消息编写器
需要多个值。如果媒体类型暗示无限流(例如,application/json+stream
),则单独写入和刷新值。否则
值缓冲到列表中并呈现为 JSON 数组。