# 反应式堆栈上的 Web
文档的这一部分涵盖了对构建在反应流 (opens new window)API 上的反应式堆栈 Web 应用程序的支持,该应用程序可在非阻塞服务器上运行,例如 Netty、 Undertow 和 Servlet 3.1+ 容器。个别章节涵盖了Spring WebFlux框架、反应性[WebClient
](#WebFlux-client)、对testing和反应库的支持。对于 Servlet-stack Web 应用程序,请参见Web on Servlet Stack。
# 1. Spring WebFlux
Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是后来在 5.0 版本中添加的。它是完全非阻塞的,支持反应流 (opens new window)背压,并在 Netty、 Undertow 和 Servlet 3.1+ 容器等服务器上运行。
这两个 Web 框架都反映了它们的源模块的名称(spring-webmvc (opens new window)和spring-webflux (opens new window)),并在 Spring 框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者在某些情况下,同时使用这两个模块—例如,具有 Spring MVC 控制器的反应性。
# 1.1.概述
为什么要创建 WebFlux?
部分解决方案是需要一个非阻塞的 Web 堆栈来处理少量线程的并发性,并以更少的硬件资源进行扩展。 Servlet 3.1 确实为非阻塞 I/O 提供了一个 API。但是,使用它会导致远离 Servlet API 的其余部分,其中契约是同步的(Filter
,Servlet
)或阻塞的(getParameter
,getPart
)。这就是一个新的通用 API 的动机,它可以作为跨任何非阻塞运行时的基础。这一点很重要,因为服务器(如 Netty)在异步、非阻塞空间中已经很好地建立了。
答案的另一部分是函数式编程。正如 爪哇5 中添加的注释创造了机会(例如注释的 REST 控制器或单元测试)一样,爪哇8 中添加的 lambda 表达式为 爪哇 中的功能 API 创造了机会。这对于允许异步逻辑的声明式组合的非阻塞应用程序和延续风格 API(由CompletableFuture
和ReactiveX (opens new window)推广)是一个福音。在编程模型级别,爪哇8 使 Spring WebFlux 能够在带注释的控制器之外提供功能性的 Web 端点。
# 1.1.1.定义“反应性”
我们谈到了“非阻塞”和“功能性”,但是反应性是什么意思呢?
术语“反应性”指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。从这个意义上说,非阻塞是反应性的,因为我们现在不是被阻塞,而是在操作完成或数据可用时对通知做出反应。
我们团队中还有另一个与“反应性”相关的重要机制,那就是无阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然的反压形式,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,这样快速生成器就不会淹没其目标。
反应流是一个small spec (opens new window)(在 爪哇9 中也是adopted (opens new window)),它定义了具有背压的异步组件之间的交互。例如,数据存储库(充当Publisher (opens new window))可以生成 HTTP 服务器(充当Subscriber (opens new window))随后可以写入响应的数据。反应流的主要目的是让订阅者控制发布者生成数据的速度或速度。
常见问题:如果出版商不能放慢速度怎么办? 反应流的目的只是为了建立机制和边界。 如果一个发布者不能减速,它就必须决定是缓冲、下降还是失败。 |
---|
# 1.1.2.反应性 API
反应流在互操作性中起着重要的作用。它是库和基础设施组件感兴趣的,但作为应用程序 API 用处不大,因为它的级别太低。应用程序需要一个更高层次和更丰富的功能 API 来组成异步逻辑——类似于 爪哇8Stream
API,但不仅仅是用于集合。这就是反应库所扮演的角色。
Reactor (opens new window)是 Spring WebFlux 选择的反应库。它提供了[Mono
](https://projectreactor.io/DOCS/core/release/api/reactor/core/publisher/mono.html)和[Flux
](https://projectreactor.io/core/core/release/api/reactor/reactor/publisher.html)API 类型,以便通过与 reactivex=“388”/>对齐的一组丰富的运算符对 0..1(Mono
)和 0.n(<gt r=“386”)的数据序列进行工作。反应器是一个反应库,因此,它的所有操作人员都支持无阻塞背压。Reactor 非常关注服务器端 爪哇。它是与 Spring 密切合作开发的。
WebFlux 需要将 Reactor 作为核心依赖项,但它可以通过反应流与其他反应库进行互操作。作为一般规则,WebFlux API 接受普通的Publisher
作为输入,在内部将其调整为反应器类型,并使用该类型,并返回Flux
或Mono
作为输出。因此,你可以将任何Publisher
作为输入传递,并且可以在输出上应用操作,但是你需要调整输出以与另一个反应库一起使用。只要可行(例如,带注释的控制器),WebFlux 就会透明地适应 RX爪哇 或其他反应库的使用。有关更多详细信息,请参见反应库。
除了反应性 API,WebFlux 还可以与Coroutines Kotlin 中的 API 一起使用,这提供了一种更必要的编程风格。 下面的 Kotlin 代码示例将与协程 API 一起提供。 |
---|
# 1.1.3.程序设计模型
spring-web
模块包含支撑 Spring WebFlux 的反应性基础,包括 HTTP 抽象、支持服务器的反应性流adapters、codecs,以及与 Servlet API 类似但具有非阻塞契约的核心[WebHandler
API](#WebFlux-web-handler-API)。
在此基础上, Spring WebFlux 提供了两种编程模型的选择:
带注释的控制器:与 Spring MVC 一致,并且基于来自
spring-web
模块的相同注释。 Spring MVC 和 WebFlux 控制器都支持反应(反应器和 RX爪哇)返回类型,因此,很难将它们区分开来。一个值得注意的区别是,WebFlux 还支持活性的@RequestBody
参数。功能端点:基于 lambda 的、轻量级的和函数式的编程模型。你可以将其视为一个小型的库或一组实用程序,应用程序可以使用它们来路由和处理请求。与注解控制器的最大区别在于,应用程序负责从头到尾处理请求,而不是通过注解声明意图并被回调。
# 1.1.4.适用性
Spring MVC 还是 WebFlux?
这是个自然的问题,但会造成一种不合理的二分法。实际上,两者共同作用来扩大可供选择的范围。这两种设计是为了彼此之间的连续性和一致性,它们可以并排使用,并且来自每一方的反馈对双方都有利。下面的图表显示了这两者之间的关系,它们的共同点,以及各自的独特支持:
我们建议你考虑以下几点:
如果你的 Spring MVC 应用程序运行良好,则无需更改。命令式编程是编写、理解和调试代码的最简单的方法。你有最多的库可供选择,因为从历史上看,大多数库都是阻塞的。
如果你已经在寻找一个非阻塞的 Web 堆栈, Spring WebFlux 提供了与该空间中的其他程序相同的执行模型的优点,并且还提供了服务器(Netty、 Tomcat、 Jetty、 Undertow 和 Servlet 3.1+ 容器)的选择,以及编程模型的选择(带注释的控制器和功能的 Web 端点),以及反应库的选择(反应器、RX爪哇 或其他)。
如果你对与 爪哇8Lambdas 或 Kotlin 一起使用的轻量级、功能性 Web 框架感兴趣,那么可以使用 Spring WebFlux Functional Web Endpoints。对于较小的应用程序或需求不那么复杂的微服务来说,这也是一个很好的选择,它们可以受益于更高的透明度和控制。
在微服务架构中,你可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器的应用程序,或者具有 Spring WebFlux 功能端点的应用程序。在两个框架中都支持相同的基于注释的编程模型,这使得在为正确的工作选择正确的工具的同时更容易重用知识。
评估应用程序的一种简单方法是检查其依赖关系。如果你有阻塞持久性 API( JPA、JDBC)或网络 API 可供使用, Spring MVC 至少是通用架构的最佳选择。对于 Reactor 和 Rx爪哇 来说,在单独的线程上执行阻塞调用在技术上是可行的,但是你不会充分利用非阻塞的 Web 堆栈。
如果你有一个 Spring MVC 应用程序,其中调用了远程服务,请尝试 reactive
WebClient
。你可以直接从 Spring MVC 控制器方法返回反应类型(reactor,rxjava,or other)。每次调用的延迟越大或调用之间的相互依赖性越大,其好处就越显著。 Spring MVC 控制器也可以调用其他无功分量。如果你有一个庞大的团队,请记住,在向非阻塞、函数式和声明式编程的转变中,学习曲线很陡。在没有全开关的情况下,一种实用的启动方式是使用反应式
WebClient
。除此之外,从小处着手,衡量收益。我们预计,对于广泛的应用而言,这种转变是不必要的。如果你不确定要寻找哪些好处,那么可以从了解非阻塞 I/O 的工作方式(例如,在单线程 node.js 上的并发性)及其效果开始。
# 1.1.5.服务器
Spring WebFlux 在 Tomcat、 Jetty、 Servlet 3.1+ 容器上以及在诸如 Netty 和 Undertow 等非 Servlet 运行时上得到支持。所有服务器都适应于低级别的common API,以便可以跨服务器支持更高级别的程序设计模型。
Spring WebFlux 不具有启动或停止服务器的内置支持。然而,很容易从 Spring 配置和WebFlux 基础设施和run it的应用程序中使用几行代码。
Spring Boot 有一个 WebFlux 启动器,可以自动执行这些步骤。默认情况下,启动器使用 Netty,但通过更改 Maven 或 Gradle 依赖关系,很容易切换到 Tomcat、 Jetty 或 Undertow。 Spring 启动默认为 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(以及一般的非阻塞服务器)中,假定应用程序不会阻塞。因此,非阻塞服务器使用一个小的、固定大小的线程池(事件循环工作者)来处理请求。
“可伸缩”和“少量线程”听起来可能是矛盾的,但永远不要阻塞 当前线程(而是依赖回调)意味着你不需要额外的线程,因为 没有要吸收的阻塞调用。 |
---|
调用阻塞 API
如果你确实需要使用阻塞库,该怎么办?Reactor 和 Rx爪哇 都提供publishOn
操作符,以便在不同的线程上继续处理。这意味着有一个很容易逃脱的舱口。然而,请记住,阻塞 API 并不适合这种并发模型。
可变状态
在 Reactor 和 RX爪哇 中,你通过运算符声明逻辑。在运行时,会形成一个反应性管道,在该管道中,数据会在不同的阶段中按顺序进行处理。这样做的一个主要好处是,它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会并发调用。
线程模型
在运行 Spring WebFlux 的服务器上,你应该看到哪些线程?
在“vanilla” Spring WebFlux 服务器上(例如,没有数据访问或其他可选的依赖关系),你可以期望为服务器提供一个线程,为请求处理提供几个线程(通常与 CPU 内核的数量一样多)。 Servlet 然而,容器可以以更多线程(例如, Tomcat 上的 10)开始,以支持 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 的使用。
反应式
WebClient
以事件循环方式进行操作。因此,你可以看到与此相关的处理线程的数量很少且是固定的(例如,reactor-http-nio-
与 Reactor Netty 连接器)。但是,如果 Reactor Netty 同时用于客户机和服务器,则默认情况下这两个服务器共享事件循环资源。Reactor 和 Rx爪哇 提供线程池抽象(称为调度器),与
publishOn
操作符一起使用,该操作符用于将处理切换到不同的线程池。调度程序的名称建议了一种特定的并发策略——例如,“并行”(用于线程数量有限的 CPU 绑定工作)或“弹性”(用于具有大量线程的 I/O 绑定工作)。如果你看到这样的线程,这意味着某些代码正在使用特定的线程池Scheduler
策略。数据访问库和其他第三方依赖项也可以创建和使用自己的线程。
配置
Spring 框架不提供对启动和停止servers的支持。要为服务器配置线程模型,你需要使用特定于服务器的配置 API,或者,如果你使用 Spring 引导,请检查每个服务器的 Spring 引导配置选项。你可以直接configureWebClient
。对于所有其他库,请参阅它们各自的文档。
# 1.2.反应核
spring-web
模块包含对反应式 Web 应用程序的以下基本支持:
对于服务器请求处理,有两个级别的支持。
Httphandler:使用非阻塞 I/O 和反应流反压处理 HTTP 请求的基本契约,以及用于反应堆网络、 Undertow、 Tomcat、 Jetty 和任何 Servlet 3.1+ 容器的适配器。
[
WebHandler
API](#WebFlux-Web-Handler-API):略高级别的、用于请求处理的通用 Web API,在此基础上构建了具体的编程模型,如带注释的控制器和功能端点。
对于客户端,有一个基本的
ClientHttpConnector
契约来执行具有非阻塞 I/O 和反应性流反压的 HTTP 请求,以及用于反应堆网状结构 (opens new window)、反应性Jetty HttpClient (opens new window)和Apache HttpComponents (opens new window)的适配器。应用程序中使用的较高级别WebClient建立在此基本契约上。对于客户机和服务器,codecs用于序列化和反序列化 HTTP 请求和响应内容。
# 1.2.1.HttpHandler
Httphandler (opens new window)是一个简单的契约,它只有一个方法来处理请求和响应。它是故意最小化的,它的主要目的也是唯一的目的是在不同的 HTTP 服务器 API 上进行最小化抽象。
下表描述了受支持的服务器 API:
Server name | 使用的服务器 API | Reactive Streams support |
---|---|---|
Netty | Netty API | Reactor Netty (opens new window) |
Undertow | Undertow 空气污染指数 | spring-web: Undertow to Reactive Streams bridge |
Tomcat | Servlet 3.1 非阻塞 I/O; Tomcat 读写字节缓冲器 VS 字节的 API[] | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
Jetty | Servlet 3.1 非阻塞 I/O; Jetty 写字节缓冲器 VS 字节的 API[] | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
Servlet 3.1 container | Servlet 3.1 非阻塞 I/O | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
下表描述了服务器的依赖关系(另请参见支持的版本 (opens new window)):
Server name | Group id | 工件名称 |
---|---|---|
Reactor Netty | io.projectreactor.netty | 反应堆网状结构 |
Undertow | io.undertow | Undertow-核心 |
Tomcat | org.apache.tomcat.embed | Tomcat-嵌入-核心 |
Jetty | org.eclipse.jetty | Jetty-服务器, Jetty- Servlet |
下面的代码片段显示了在每个服务器 API 中使用HttpHandler
适配器的情况:
反应堆网状结构
爪哇
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
Kotlin
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();
Kotlin
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();
Kotlin
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();
Kotlin
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+ Container
要将 WAR 部署到任何 Servlet 3.1+ 容器上,你可以在 WAR 中扩展并包括[AbstractReactiveWebInitializer
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/abstractreactivewebinitializer.html)。该类将HttpHandler
与ServletHttpHandlerAdapter
包装在一起,并将其注册为Servlet
。
# 1.2.2.WebHandler
api
org.springframework.web.server
包构建在[HttpHandler
](#WebFlux-Httphandler)合同的基础上,通过多个[WebExceptionHandler
](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/org/org/SpringFramework/Web/Server/WebExceptionHandler.html)的链提供一个通用的 Web API,用于处理请求(https:///gt://gt/DOCS. Spring.3.3.org/16.org/16.org/javloverFramework.org/jumerfilter 通过简单地指向一个 Spring ApplicationContext
,其中组件是自动检测,和/或通过向构建器注册组件,可以将链与WebHttpHandlerBuilder
放在一起。
虽然HttpHandler
的一个简单目标是抽象不同 HTTP 服务器的使用,但WebHandler
API 的目标是提供 Web 应用程序中常用的一组更广泛的功能,例如:
具有属性的用户会话。
请求属性。
已为请求解析
Locale
或Principal
。访问解析和缓存的表单数据。
多部分数据的抽象。
还有更多..
# 特殊 Bean 类型
下表列出了WebHttpHandlerBuilder
可以在 Spring ApplicationContext 中自动检测的组件,或者可以直接向其注册的组件:
Bean name | Bean type | Count | 说明 |
---|---|---|---|
<any> | WebExceptionHandler | 0..N | 为来自WebFilter 实例和目标WebHandler 的链中的异常提供处理。有关更多详细信息,请参见Exceptions。 |
<any> | WebFilter | 0..N | 将截取样式逻辑应用于过滤器链的其余部分之前和之后,以及 目标 WebHandler 。有关更多详细信息,请参见Filters。 |
webHandler | WebHandler | 1 | 请求的处理程序。 |
webSessionManager | WebSessionManager | 0..1 | 默认情况下,WebSession 实例的管理器通过ServerWebExchange 上的方法公开。DefaultWebSessionManager 实例。 |
serverCodecConfigurer | ServerCodecConfigurer | 0..1 | 用于访问HttpMessageReader 实例以解析表单数据和多部分数据,然后通过 ServerWebExchange 上的方法公开。默认情况下ServerCodecConfigurer.create() 。 |
localeContextResolver | LocaleContextResolver | 0..1 | 默认情况下,LocaleContext 的解析器通过ServerWebExchange 上的方法公开。AcceptHeaderLocaleContextResolver 。 |
forwardedHeaderTransformer | ForwardedHeaderTransformer | 0..1 | 对于处理转发的类型头,可以通过提取和删除它们,也可以只删除它们。 默认情况下不使用。 |
# 表单数据
ServerWebExchange
公开了以下访问表单数据的方法:
爪哇
Mono<MultiValueMap<String, String>> getFormData();
Kotlin
suspend fun getFormData(): MultiValueMap<String, String>
DefaultServerWebExchange
使用配置的HttpMessageReader
将表单数据(application/x-www-form-urlencoded
)解析为MultiValueMap
。默认情况下,FormHttpMessageReader
被配置为由ServerCodecConfigurer
Bean 使用(请参见Web 处理程序 API)。
# 多部分数据
ServerWebExchange
公开了以下访问多部分数据的方法:
爪哇
Mono<MultiValueMap<String, Part>> getMultipartData();
Kotlin
suspend fun getMultipartData(): MultiValueMap<String, Part>
DefaultServerWebExchange
使用配置的HttpMessageReader<MultiValueMap<String, Part>>
将multipart/form-data
内容解析为MultiValueMap
。默认情况下,这是DefaultPartHttpMessageReader
,它没有任何第三方依赖关系。或者,可以使用SynchronossPartHttpMessageReader
,这是基于Synchronoss 多部件蔚来 (opens new window)库的。这两个参数都是通过ServerCodecConfigurer
Bean 配置的(参见Web 处理程序 API)。
要以流式方式解析多部分数据,可以使用从HttpMessageReader<Part>
返回的Flux<Part>
代替。例如,在带注释的控制器中,使用@RequestPart
意味着通过名称对各个部分进行类似Map
的访问,因此需要完整地解析多部分数据。相比之下,你可以使用@RequestBody
将内容解码为Flux<Part>
,而无需收集到MultiValueMap
。
# 转发头
当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化。从客户机的角度来看,这使得创建指向正确的主机、端口和方案的链接成为一项挑战。
RFC 7239 (opens new window)定义了Forwarded
HTTP 报头,代理可以使用该报头来提供有关原始请求的信息。也有其他非标准标题,包括X-Forwarded-Host
、X-Forwarded-Port
、X-Forwarded-Proto
、X-Forwarded-Ssl
和X-Forwarded-Prefix
。
ForwardedHeaderTransformer
是一个组件,它基于转发的标头修改请求的主机、端口和方案,然后删除这些标头。如果将其声明为 Bean,并使用名称forwardedHeaderTransformer
,则将其声明为detected。
转发头的安全性需要考虑,因为应用程序不能知道头是由代理添加的,还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的转发流量。你还可以将ForwardedHeaderTransformer
配置为removeOnly=true
,在这种情况下,它会删除但不使用头。
在 5.1 中,ForwardedHeaderFilter 被弃用,并被ForwardedHeaderTransformer 取代,因此在创建交换之前,可以更早地处理转发头。如果无论如何都配置了过滤器,则将其从 过滤器列表中取出,并使用 ForwardedHeaderTransformer 代替。 |
---|
# 1.2.3.过滤器
在[WebHandler
API](#WebFlux-Web-Handler-API)中,可以使用WebFilter
在过滤器和目标WebHandler
的处理链的其余部分之前和之后应用拦截风格的逻辑。当使用WebFlux 配置时,注册WebFilter
就像将其声明为 Spring Bean 一样简单,并且(可选地)通过在 Bean 声明上使用@Order
或通过实现Ordered
来表示优先级。
# CORS
Spring WebFlux 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而,当你将其与 Spring 安全性一起使用时,我们建议依赖于内置的CorsFilter
,这必须在 Spring 安全性的过滤器链之前订购。
有关更多详细信息,请参见CORS和webflux-cors.html一节。
# 1.2.4.例外
在[WebHandler
API]中,可以使用WebExceptionHandler
来处理来自WebFilter
实例和目标WebHandler
的链中的异常。当使用WebFlux 配置时,注册WebExceptionHandler
就像将其声明为 Spring Bean 一样简单,并且(可选地)通过在 Bean 声明上使用@Order
或通过实现Ordered
来表示优先级。
下表描述了可用的WebExceptionHandler
实现:
Exception Handler | 说明 |
---|---|
ResponseStatusExceptionHandler | 通过将响应设置为异常的 HTTP 状态代码,为类型[ResponseStatusException ](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/responsestatusception.html)的异常提供处理。 |
WebFluxResponseStatusExceptionHandler | ResponseStatusExceptionHandler 的扩展,该扩展还可以在任何异常情况下确定代码的 HTTP 状态 @ResponseStatus 注释。在WebFlux 配置中声明此处理程序。 |
# 1.2.5.编解码器
spring-web
和spring-core
模块通过具有反应流反压力的非阻塞 I/O,提供对与高层对象之间的字节内容的序列化和反序列化的支持。以下介绍了这种支持:
[
Encoder
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/codec/encoder.html)和[Decoder
(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/core/codecoder.html)是独立于 http 的编码和解码内容的低级合同。[
HttpMessageReader
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/httpMessageReader.html)和[HttpMessageWriter
(https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javadoc-api/org/spramework/http/codec/httpmessagewriter.html)是对 HTTPMessageWriter 内容进行编码和解码的合同。Encoder
可以用EncoderHttpMessageWriter
包装,以使其适合在 Web 应用程序中使用,而Decoder
可以用DecoderHttpMessageReader
包装。[
DataBuffer
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/io/buffer/databuffer.html)抽象出不同的字节缓冲区表示(例如 nettyByteBuf
,java.nio.ByteBuffer
等),这也是所有编解码器的工作内容。有关此主题的更多信息,请参见“ Spring core”部分中的数据缓冲区和编解码器。
spring-core
模块提供byte[]
、ByteBuffer
、DataBuffer
、Resource
和String
编码器和解码器实现方式。spring-web
模块提供了 Jackson 的 JSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器,以及用于表单数据、多部分内容、服务器发送的事件等的仅用于 Web 的 HTTP 消息阅读器和编写器实现。
ClientCodecConfigurer
和ServerCodecConfigurer
通常用于配置和定制要在应用程序中使用的编解码器。参见关于配置HTTP 消息编解码器的部分。
# JacksonJSON
当 Jackson 库存在时,都支持 JSON 和二进制 JSON(Smile (opens new window))。
Jackson2Decoder
的工作原理如下:
Jackson 的异步、非阻塞解析器用于将一个字节块流聚合到
TokenBuffer
中,每个字节块代表一个 JSON 对象。每个
TokenBuffer
都传递给 Jackson 的ObjectMapper
,以创建一个更高级别的对象。当解码到单值发布器(例如
Mono
)时,存在一个TokenBuffer
。当解码到多值发布者(例如
Flux
)时,一旦接收到用于完全形成的对象的足够字节,每个TokenBuffer
都会传递到ObjectMapper
。输入内容可以是 JSON 数组,或任何线分隔的 JSON (opens new window)格式,例如 NDJSON、JSON 行或 JSON 文本序列。
Jackson2Encoder
的工作原理如下:
对于单个值发布者(例如
Mono
),只需通过ObjectMapper
序列化它。对于具有
application/json
的多值发布者,默认情况下,使用Flux#collectToList()
收集这些值,然后序列化生成的集合。对于具有流媒体类型(如
application/x-ndjson
或application/stream+x-jackson-smile
)的多值发布者,使用线分隔的 JSON (opens new window)格式对每个值分别进行编码、写入和刷新。其他流媒体类型可以在编码器中注册。对于 SSE,每个事件都调用
Jackson2Encoder
,并刷新输出,以确保及时交付。
默认情况下,Jackson2Encoder 和Jackson2Decoder 都不支持类型String 的元素。相反,默认的假设是字符串或字符串序列表示序列化的 JSON 内容,由 CharSequenceEncoder 呈现。如果需要的是从 Flux<String> 呈现一个 JSON 数组,则使用Flux#collectToList() 和编码 Mono<List<String>> 。 |
---|
# 表单数据
FormHttpMessageReader
和FormHttpMessageWriter
支持解码和编码application/x-www-form-urlencoded
内容。
在表单内容经常需要从多个地方访问的服务器端,ServerWebExchange
提供了一个专用的getFormData()
方法,该方法通过FormHttpMessageReader
解析内容,然后缓存结果以进行重复访问。参见[WebHandler
API]部分中的Form Data。
一旦使用getFormData()
,就不能再从请求主体中读取原始 RAW 内容。出于这个原因,应用程序需要始终通过ServerWebExchange
来访问缓存的表单数据,而不是从原始请求主体读取数据。
# 多部分
MultipartHttpMessageReader
和MultipartHttpMessageWriter
支持解码和编码“multipart/form-data”内容。反过来,MultipartHttpMessageReader
将实际解析委托给另一个HttpMessageReader
,然后简单地将部分收集到MultiValueMap
中。默认情况下,使用DefaultPartHttpMessageReader
,但这可以通过ServerCodecConfigurer
进行更改。有关DefaultPartHttpMessageReader
的更多信息,请参阅DefaultPartHttpMessageReader
的[javadoc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/multipart/defaultparthpmessagereader.html)。
在可能需要从多个地方访问多部分表单内容的服务器端,ServerWebExchange
提供了一个专用的getMultipartData()
方法,该方法通过MultipartHttpMessageReader
解析内容,然后缓存结果以进行重复访问。参见[WebHandler
API]部分中的多部分数据。
一旦使用getMultipartData()
,就不能再从请求主体中读取原始 RAW 内容。由于这个原因,应用程序必须始终使用getMultipartData()
进行重复的、类似于 MAP 的部分访问,或者以其他方式依赖于SynchronossPartHttpMessageReader
进行一次性访问Flux<Part>
。
# 限制
Decoder
和HttpMessageReader
实现了对部分或全部输入流进行缓冲,可以在内存中配置缓冲的最大字节数的限制。在某些情况下,发生缓冲是因为输入被聚合并表示为单个对象——例如,带有@RequestBody byte[]
、x-www-form-urlencoded
数据的控制器方法,以此类推。在分割输入流(例如,分隔的文本、JSON 对象流等)时,流也可以发生缓冲。对于那些流情况,限制应用于与流中的一个对象相关联的字节数。
要配置缓冲区大小,你可以检查给定的Decoder
或HttpMessageReader
是否公开了maxInMemorySize
属性,如果是这样,爪哇doc 将提供有关默认值的详细信息。在服务器端,ServerCodecConfigurer
提供了一个设置所有编解码器的位置,请参见HTTP 消息编解码器。在客户端,所有编解码器的限制可以在Webclient.builder中进行更改。
对于多部分解析,maxInMemorySize
属性限制了非文件部分的大小。对于文件部件,它确定将部件写入磁盘的阈值。对于写入磁盘的文件部件,有一个额外的maxDiskUsagePerPart
属性来限制每个部件的磁盘空间。还有一个maxParts
属性来限制多部分请求中的部分总数。要在 WebFlux 中配置这三个变量,你需要提供一个MultipartHttpMessageReader
到ServerCodecConfigurer
的预配置实例。
# 流媒体
当流到 HTTP 响应时(例如,text/event-stream
,application/x-ndjson
),定期发送数据是很重要的,以便可靠地检测断开连接的客户端,越早越好。这样的发送可能是一个只有评论的、空的 SSE 事件,或者是任何其他可以有效充当心跳的“无操作”数据。
# DataBuffer
DataBuffer
是 WebFlux 中字节缓冲区的表示形式。 Spring 该引用的核心部分在数据缓冲区和编解码器一节中有更多关于该引用的内容。要理解的关键点是,在一些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。
WebFlux 应用程序通常不需要关注这些问题,除非它们直接使用或产生数据缓冲区,而不是依赖编解码器来转换到更高级别的对象,或者除非它们选择创建自定义编解码器。对于这种情况,请参阅数据缓冲区和编解码器中的信息,特别是关于使用 Databuffer的部分。
# 1.2.6.伐木
Spring WebFlux 中的DEBUG
级别日志被设计为紧凑、最小且对人类友好的。它关注的是一次又一次有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。
TRACE
级别日志记录通常遵循与DEBUG
相同的原则(例如,也不应该是消防软管),但可以用于调试任何问题。此外,一些日志消息可能在TRACE
与DEBUG
处显示不同级别的详细信息。
良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标,请告诉我们。
# 日志 ID
在 WebFlux 中,单个请求可以在多个线程上运行,而线程 ID 对于关联属于特定请求的日志消息是没有用的。这就是为什么 WebFlux 日志消息在默认情况下使用特定于请求的 ID 作为前缀的原因。
在服务器端,日志 ID 存储在ServerWebExchange
属性中([LOG_ID_ATTRIBUTE
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/serverwebexchange.html#log_id_attribute)),而基于该 ID 的完全格式化的前缀可从ServerWebExchange#getLogPrefix()
获得。在WebClient
端,日志 ID 存储在ClientRequest
属性([LOG_ID_ATTRIBUTE
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/function/client/clientrequest.html#log_id_attribute)中),而完全格式化的前缀可从ClientRequest#logPrefix()
获得。
# 敏感数据
DEBUG
和TRACE
日志记录可以记录敏感信息。这就是为什么表单参数和标题在默认情况下是屏蔽的,并且你必须显式地完全启用它们的日志记录。
下面的示例展示了如何为服务器端请求执行此操作:
爪哇
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
Kotlin
@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();
Kotlin
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()
# 附录
SLF4j 和 log4j2 等日志记录库提供了避免阻塞的异步记录器。尽管这些方法有其自身的缺点,比如可能会丢弃无法排队记录的消息,但它们是当前在反应性、非阻塞应用程序中使用的最佳可用选项。
# 自定义编解码器
应用程序可以注册用于支持其他媒体类型的定制编解码器,或者默认编解码器不支持的特定行为。
开发人员表示的一些配置选项是在默认的编解码器上强制执行的。自定义编解码器可能希望有机会与这些首选项保持一致,比如强制缓冲限制或记录敏感数据。
下面的示例展示了如何为客户端请求执行此操作:
爪哇
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
Kotlin
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
# 1.3.DispatcherHandler
Spring WebFlux,类似于 Spring MVC,是围绕前控制器模式设计的,其中中心,,提供用于请求处理的共享算法,而实际工作是通过可配置的、委托的组件来执行的。这个模型是灵活的,并支持不同的工作流程。
DispatcherHandler
从 Spring 配置中发现它需要的委托组件。它本身也被设计为 Spring Bean 并且实现ApplicationContextAware
以访问它运行的上下文。如果DispatcherHandler
是以 Bean 名webHandler
声明的,那么它又是由[WebHttpHandlerBuilder
](https:/DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/webhtphandlerbuilder.html)发现的,它组合了一个请求-处理链,如[WebHandler
r="webpi-api-handler.html](#-flux)中所述。
Spring WebFlux 应用程序中的配置通常包括:
DispatcherHandler
与 Bean 名称webHandler
WebFilter
和WebExceptionHandler
beans[
DispatcherHandler
Special Beans](#WebFlux-Special- Bean-types)其他
将配置给WebHttpHandlerBuilder
以构建处理链,如下例所示:
爪哇
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
Kotlin
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
得到的HttpHandler
可以与服务器适配器一起使用了。
# 1.3.1.特殊类型 Bean
DispatcherHandler
将委托给特殊的 bean 来处理请求并呈现适当的响应。我们所说的“特殊 bean”是指实现 WebFlux 框架契约的 Spring-managedObject
实例。这些通常带有内置契约,但你可以自定义它们的属性,扩展它们或替换它们。
下表列出了DispatcherHandler
检测到的特殊 bean。请注意,在较低的级别上还检测到一些其他 bean(请参见 Web 处理程序 API 中的Special bean types)。
Bean type | 解释 |
---|---|
HandlerMapping | 将请求映射到处理程序。该映射是基于一些条件,其中 的细节通过 HandlerMapping 实现—注释控制器,简单URL 模式映射,以及其他不同的实现。 HandlerMapping 主要的RequestMappingHandlerMapping 实现为@RequestMapping 注释方法,RouterFunctionMapping 为功能端点<710"/>r= 路由,和SimpleUrlHandlerMapping 用于显式注册 URI 路径模式和 WebHandler 实例。 |
HandlerAdapter | 帮助DispatcherHandler 调用映射到请求的处理程序,而不管实际调用处理程序的方式如何。例如,调用带注释的控制器 需要解析注释。a HandlerAdapter 的主要目的是保护DispatcherHandler 不受这些细节的影响。 |
HandlerResultHandler | 处理来自处理程序调用的结果并最终确定响应。 参见结果处理。 |
# 1.3.2.WebFlux 配置
应用程序可以声明处理请求所需的基础设施 bean(在Web 处理程序 API和[DispatcherHandler
](#Webflux-special- Bean-types)下列出)。然而,在大多数情况下,WebFlux 配置是最好的起点。它声明所需的 bean,并提供一个更高级的配置回调 API 来定制它。
Spring 启动依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供了 许多额外的方便选项。 |
---|
# 1.3.3.处理
DispatcherHandler
按以下方式处理请求:
每个
HandlerMapping
都被要求找到一个匹配的处理程序,并使用第一个匹配。如果找到了一个处理程序,则通过一个适当的
HandlerAdapter
运行该处理程序,该处理程序将执行时的返回值公开为HandlerResult
。将
HandlerResult
赋予适当的HandlerResultHandler
,以通过直接写入响应或通过使用视图来呈现来完成处理。
# 1.3.4.结果处理
通过HandlerAdapter
调用处理程序的返回值被包装为HandlerResult
,以及一些附加的上下文,并传递给声称支持它的第一个HandlerResultHandler
。下表显示了可用的HandlerResultHandler
实现,所有这些实现都在WebFlux 配置中声明:
Result Handler Type | 返回值 | Default Order |
---|---|---|
ResponseEntityResultHandler | ResponseEntity ,通常来自@Controller 实例。 | 0 |
ServerResponseResultHandler | ServerResponse ,通常来自功能端点。 | 0 |
ResponseBodyResultHandler | 处理来自@ResponseBody 方法或@RestController 类的返回值。 | 100 |
ViewResolutionResultHandler | CharSequence ,[View ](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadoc-api/org/springframework/web/result/result/view/view.html),Model (opens new window),Map ,Rendering (opens new window),或其他任何Object 均作为模型属性处理。<>视图分辨率<<>r=">><< | Integer.MAX_VALUE |
# 1.3.5.例外
从HandlerAdapter
返回的HandlerResult
可以基于某些特定于处理程序的机制公开用于错误处理的函数。如果出现以下情况,则调用此错误函数:
处理程序(例如,
@Controller
)调用失败。通过
HandlerResultHandler
处理处理程序返回值失败。
错误函数可以更改响应(例如,到错误状态),只要在从处理程序返回的反应类型产生任何数据项之前发生错误信号。
这就是@Controller
类中的@ExceptionHandler
方法的支持方式。相比之下,对 Spring MVC 中相同内容的支持是建立在HandlerExceptionResolver
上的。这一点一般不会有什么影响。但是,请记住,在 WebFlux 中,不能使用@ControllerAdvice
来处理在选择处理程序之前发生的异常。
另请参见“注释控制器”部分中的管理异常或 WebHandler API 部分中的Exceptions。
# 1.3.6.视图分辨率
视图分辨率允许使用 HTML 模板和模型在浏览器上进行呈现,而无需将你绑定到特定的视图技术。在 Spring WebFlux 中,通过专用的HandlerResultHandler支持视图解析,该实例使用ViewResolver
实例将字符串(代表逻辑视图名称)映射到View
实例。然后使用View
来呈现响应。
# 处理
传递到ViewResolutionResultHandler
中的HandlerResult
包含来自处理程序的返回值和包含在请求处理过程中添加的属性的模型。返回值被处理为以下内容之一:
String
,CharSequence
:要通过配置的ViewResolver
实现的列表解析为View
的逻辑视图名称。void
:根据请求路径选择一个默认的视图名称,减去前导和后导斜杠,并将其解析为View
。当未提供视图名称(例如,返回了 model 属性)或异步返回值(例如,Mono
完全为空)时,也会发生同样的情况。Rendering (opens new window):视图解析场景的 API。探索你的 IDE 中的代码补全选项。
Model
,Map
:要为请求添加到模型中的额外模型属性。任何其他:任何其他返回值(简单类型除外,由Beanutils#IsSimpleProperty (opens new window)确定)被视为要添加到模型中的模型属性。属性名是通过使用惯例 (opens new window)从类名派生出来的,除非存在处理程序方法
@ModelAttribute
注释。
该模型可以包含异步的、反应性的类型(例如,来自 Reactor 或 RX爪哇)。在呈现之前,AbstractView
将这些模型属性解析为具体的值并更新模型。单值活性类型被解析为单值或无值(如果为空),而多值活性类型(例如,Flux<T>
)被收集并解析为List<T>
。
要配置视图分辨率就像在 Spring 配置中添加ViewResolutionResultHandler
Bean 一样简单。WebFlux 配置为视图分辨率提供了专用的配置 API。
有关与 Spring WebFlux 集成的视图技术的更多信息,请参见查看技术。
# 重定向
视图名称中的特殊redirect:
前缀允许你执行重定向。UrlBasedViewResolver
(和子类)将其视为需要重定向的指令。视图名称的其余部分是重定向 URL。
净效果与控制器返回RedirectView
或Rendering.redirectTo("abc").build()
相同,但现在控制器本身可以根据逻辑视图名称进行操作。像redirect:/some/resource
这样的视图名称是相对于当前应用程序的,而像redirect:https://example.com/arbitrary/path
这样的视图名称会重定向到一个绝对 URL。
# 内容协商
ViewResolutionResultHandler
支持内容协商。它将请求媒体类型与每个选定的View
所支持的媒体类型进行比较。使用了支持所请求的媒体类型的第一个View
。
为了支持诸如 JSON 和 XML 等媒体类型, Spring WebFlux 提供了HttpMessageWriterView
,这是一个特殊的View
,它通过HttpMessageWriter呈现。通常,你会通过WebFlux 配置将这些视图配置为默认视图。如果默认视图匹配所请求的媒体类型,则始终选择并使用它们。
# 1.4.带注释的控制器
Spring WebFlux 提供了一种基于注释的编程模型,其中@Controller
和@RestController
组件使用注释来表示请求映射、请求输入、处理异常等。带注释的控制器具有灵活的方法签名,不需要扩展基类,也不需要实现特定的接口。
下面的清单展示了一个基本示例:
Java
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
Kotlin
@RestController
class HelloController {
@GetMapping("/hello")
fun handle() = "Hello WebFlux"
}
在前面的示例中,该方法返回要写入响应主体的String
。
# 1.4.1.@Controller
你可以通过使用标准的 Spring Bean 定义来定义控制器 bean。该原型允许自动检测并与 Spring 用于检测 Classpath 中的类的通用支持保持一致,并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 Web 组件的角色。
要启用对此类@Controller
bean 的自动检测,可以将组件扫描添加到 Java 配置中,如下例所示:
Java
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {
// ...
}
1 | 扫描org.example.web 包。 |
---|
Kotlin
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {
// ...
}
1 | 扫描org.example.web 包。 |
---|
@RestController
是一个组合注释,它本身用@Controller
和@ResponseBody
进行了元注释,表示一个控制器,其每个方法都继承了类型级@ResponseBody
注释,因此,它直接写到响应主体与视图解析之间,并使用 HTML 模板进行呈现。
# 1.4.2.请求映射
@RequestMapping
注释用于将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射,或者在方法级别上使用它来缩小到特定的端点映射。
还有@RequestMapping
的特定于 HTTP 方法的快捷方式变体:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
提供前面的注释是自定义注释,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用@RequestMapping
,默认情况下,该方法匹配所有的 HTTP 方法。同时,在类级别上仍然需要一个@RequestMapping
来表示共享映射。
下面的示例使用类型和方法级别映射:
Java
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
# URI 模式
你可以使用 GLOB 模式和通配符来映射请求:
Pattern | Description | 例子 |
---|---|---|
? | Matches one character | "/pages/t?st.html" 匹配"/pages/test.html" 和"/pages/t3st.html" |
* | Matches zero or more characters within a path segment | "/resources/*.png" 匹配"/resources/file.png" 匹配 "/projects/*/versions" 但不匹配"/projects/spring/versions" |
** | Matches zero or more path segments until the end of the path | "/resources/**" 匹配"/resources/file.png" 和"/resources/images/file.png" "/resources/**/file.png" 是无效的,因为** 只允许在路径的末尾。 |
{name} | Matches a path segment and captures it as a variable named "name" | "/projects/{project}/versions" 匹配"/projects/spring/versions" 并捕获project=spring |
{name:[a-z]+} | Matches the regexp "[a-z]+" as a path variable named "name" | "/projects/{project:[a-z]+}/versions" 匹配"/projects/spring/versions" 但不匹配"/projects/spring1/versions" |
{*path} | Matches zero or more path segments until the end of the path and captures it as a variable named "path" | "/resources/{*file}" 匹配"/resources/images/file.png" 并捕获file=/images/file.png |
可以使用@PathVariable
访问捕获的 URI 变量,如下例所示:
Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
可以在类和方法级别声明 URI 变量,如下例所示:
Java
@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 映射。 |
Kotlin
@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")
),但是如果名称相同,并且你可以使用调试信息或 Java8 上的-parameters
编译器标志来编译代码,则可以忽略这些细节。
语法{*varName}
声明一个 URI 变量,该变量匹配零个或多个剩余的路径段。例如,/resources/{*path}
匹配/resources/
下的所有文件,并且"path"
变量捕获/resources
下的完整路径。
语法{varName:regex}
声明一个 URI 变量,其正则表达式的语法为:{varName:regex}
。例如,给定一个/spring-web-3.0.5.jar
的 URL,下面的方法会提取名称、版本和文件扩展名:
Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式还可以嵌入${…}
占位符,这些占位符在启动时通过PropertyPlaceHolderConfigurer
针对本地、系统、环境和其他属性源解析。例如,你可以使用它来基于某些外部配置参数化一个基本 URL。
Spring WebFlux 将PathPattern 和PathPatternParser 用于 URI 路径匹配支持。这两个类都位于 spring-web 中,并且明确地设计用于在 Web 应用程序中使用 http url路径,其中在运行时匹配了大量的 URI 路径模式。 |
---|
Spring WebFlux 不支持后缀模式匹配——不像 Spring MVC,其中像/person
这样的映射也匹配到/person.*
。对于基于 URL 的内容协商,如果需要,我们建议使用一个查询参数,该参数更简单,更明确,并且不易受到基于 URL 路径的攻击。
# 模式比较
当多个模式匹配一个 URL 时,必须对它们进行比较以找到最佳匹配。这是用PathPattern.SPECIFICITY_COMPARATOR
完成的,它寻找更具体的模式。
对于每个模式,都会根据 URI 变量和通配符的数量计算得分,其中 URI 变量的得分低于通配符。总分较低的模式获胜。如果两种模式得分相同,则选择较长的模式。
包罗万象的模式(例如,**
,{*varName}
)被排除在评分之外,并且总是排在最后。如果两种模式都是包罗万象的,则选择较长的模式。
# 可消费媒体类型
你可以基于请求的Content-Type
缩小请求映射,如下例所示:
Java
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
Kotlin
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
// ...
}
Consumes 属性还支持否定表达式——例如,!text/plain
表示除text/plain
以外的任何内容类型。
你可以在类级别声明一个共享的consumes
属性。然而,与大多数其他请求映射属性不同的是,当在类级使用时,方法级consumes
属性覆盖而不是扩展类级声明。
MediaType 提供了常用媒体类型的常量——例如,APPLICATION_JSON_VALUE 和APPLICATION_XML_VALUE 。 |
---|
# 可生产媒体类型
你可以基于Accept
请求头和控制器方法产生的内容类型列表来缩小请求映射的范围,如下例所示:
Java
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
Kotlin
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
// ...
}
媒体类型可以指定字符集。支持否定表达式——例如,!text/plain
表示除text/plain
以外的任何内容类型。
你可以在类级别声明一个共享的produces
属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别produces
属性覆盖而不是扩展类级别声明。
MediaType 提供了常用媒体类型的常量,例如APPLICATION_JSON_VALUE ,APPLICATION_XML_VALUE 。 |
---|
# 参数和标题
你可以根据查询参数条件缩小请求映射的范围。你可以测试查询参数的存在(myParam
)、它的不存在(!myParam
)或特定值(myParam=myValue
)。下面的示例测试具有值的参数:
Java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查myParam 等于myValue 。 |
---|
Kotlin
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查myParam 等于myValue 。 |
---|
你也可以在请求头条件中使用相同的方法,如下面的示例所示:
Java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 检查myHeader 等于myValue 。 |
---|
Kotlin
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 检查myHeader 等于myValue 。 |
---|
# HTTP 头,选项
@GetMapping
和@RequestMapping(method=HttpMethod.GET)
透明地支持用于请求映射目的的 HTTP head。控制器的方法不需要改变。在HttpHandler
服务器适配器中应用的响应包装器确保将Content-Length
头设置为不实际写入响应的字节数。
默认情况下,通过将Allow
响应头设置为所有@RequestMapping
方法中列出的具有匹配 URL 模式的 HTTP 方法列表来处理 HTTP 选项。
对于不带 HTTP 方法声明的@RequestMapping
,Allow
头被设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
。控制器方法应该总是声明受支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体—@GetMapping
,@PostMapping
,以及其他)。
你可以显式地将@RequestMapping
方法映射到 HTTPHead 和 HTTPOptions,但在常见情况下这不是必需的。
# 自定义注释
Spring WebFlux 支持使用组合注释进行请求映射。这些注释本身是用@RequestMapping
进行元注释的,其组成是为了重新声明@RequestMapping
属性的一个子集(或全部),具有更窄、更具体的目的。
@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
,和@PatchMapping
是合成注释的例子。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用@RequestMapping
,后者默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例,请查看这些注释是如何声明的。
Spring WebFlux 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,它需要子类化RequestMappingHandlerMapping
并覆盖getCustomMethodCondition
方法,在该方法中,你可以检查自定义属性并返回你自己的RequestCondition
。
# 显式注册
你可以以编程方式注册处理程序方法,这些方法可以用于动态注册或高级情况,例如同一处理程序在不同 URL 下的不同实例。下面的示例展示了如何做到这一点:
Java
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
1 | 为控制器注入目标处理程序和处理程序映射。 |
---|---|
2 | 准备请求映射元数据。 |
3 | 获取 handler 方法。 |
4 | 添加注册。 |
Kotlin
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 为控制器注入目标处理程序和处理程序映射。 |
---|---|
2 | 准备请求映射元数据。 |
3 | 获取 handler 方法。 |
4 | 添加注册。 |
# 1.4.3.处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,并且可以从受支持的控制器方法参数和返回值的范围中进行选择。
# 方法参数
下表显示了受支持的控制器方法参数。
对于需要解析阻塞 I/O(例如,读取请求主体)的参数,支持反应性类型(reactor,RxJava,or other)。这一点在“描述”栏中进行了标记。在不需要阻塞的参数上,反应式类型是不被期望的。
JDK1.8 的java.util.Optional
作为方法参数被支持,并与具有required
属性(例如,@RequestParam
,@RequestHeader
,以及其他)的注释相结合,并且与required=false
等价。
Controller method argument | 说明 |
---|---|
ServerWebExchange | 访问完整的ServerWebExchange —用于 HTTP 请求和响应的容器、请求和会话属性、 checkNotModified 方法等。 |
ServerHttpRequest , ServerHttpResponse | 访问 HTTP 请求或响应。 |
WebSession | 访问会话。除非添加了属性 ,否则不强制开始新的会话。支持反应性类型。 |
java.security.Principal | 当前经过身份验证的用户——如果已知的话,可能是特定的 实现类。 支持反应性类型。 |
org.springframework.http.HttpMethod | 请求的 HTTP 方法。 |
java.util.Locale | 当前的请求区域设置,由可用的最特定的LocaleResolver 确定—在效果中,配置的 LocaleResolver /LocaleContextResolver 。 |
java.util.TimeZone + java.time.ZoneId | 与当前请求相关联的时区,由LocaleContextResolver 确定。 |
@PathVariable | 用于访问 URI 模板变量。见URI 模式。 |
@MatrixVariable | 用于访问 URI 路径段中的名称-值对。见矩阵变量。 |
@RequestParam | 用于访问 Servlet 请求参数。参数值被转换为声明的 方法参数类型。参见[ @RequestParam ]。注意, @RequestParam 的使用是可选的——例如,用于设置其属性。参见本表后面的“任何其他参数”。 |
@RequestHeader | 用于访问请求头。标头值被转换为声明的方法参数 type。参见[ @RequestHeader ]。 |
@CookieValue | 获取 cookie 的权限。cookie 值被转换为声明的方法参数类型。 参见[ @CookieValue ]。 |
@RequestBody | 用于访问 HTTP 请求主体。通过使用HttpMessageReader 实例,主体内容被转换为声明的方法参数类型。支持反应性类型。 参见[ @RequestBody ]。 |
HttpEntity<B> | 用于访问请求头和主体。主体使用HttpMessageReader 实例进行转换。实例支持反应类型。参见[ HttpEntity ]。 |
@RequestPart | 用于访问multipart/form-data 请求中的部件。支持反应类型。参见多部分内容和多部分数据。 |
java.util.Map , org.springframework.ui.Model , and org.springframework.ui.ModelMap . | 用于访问 HTML 控制器中使用的模型,并作为 视图呈现的一部分暴露于模板中。 |
@ModelAttribute | 用于访问模型中的现有属性(如果不存在则实例化),并应用 数据绑定和验证。参见[ @ModelAttribute ](#Webflux-ann-modelattrib-method-args)以及as[ Model ](#Webflux-ann-modelattrib-methods)和[<DataBinder (#Webflux-ann-initbinder)。注意,使用 是可选的,例如,用于设置其属性。 |
Errors , BindingResult | 用于访问来自命令对象的验证和数据绑定的错误,即@ModelAttribute 参数。一个Errors 或BindingResult 参数必须在经过验证的方法参数之后立即声明。 |
SessionStatus + class-level @SessionAttributes | 要标记表单处理完成,这将触发清除通过类级@SessionAttributes 注释声明的会话属性。 有关更多详细信息,请参见[ @SessionAttributes ]。 |
UriComponentsBuilder | 用于准备相对于当前请求的主机、端口、方案和 上下文路径的 URL。见 @RequestParam 。 |
@SessionAttribute | 用于访问任何会话属性——与存储在会话 中的模型属性形成对比,后者是类级别 @SessionAttributes 声明的结果。有关更多详细信息,请参见[@SessionAttribute ]。 |
@RequestAttribute | 用于访问请求属性。有关更多详细信息,请参见[@RequestAttribute ]。 |
Any other argument | 如果方法参数与上述任何一个参数不匹配,则默认情况下将其解析为 a @RequestParam 如果它是一个简单类型,则由Beanutils#IsSimpleProperty (opens new window)、或 a @ModelAttribute 确定,否则。 |
# 返回值
下表显示了受支持的控制器方法的返回值。请注意,对于所有返回值,通常都支持来自诸如 reactor、rxjava、or other等库的反应类型。
Controller method return value | 说明 |
---|---|
@ResponseBody | 返回值通过HttpMessageWriter 实例进行编码,并写入响应。参见[ @ResponseBody ]。 |
HttpEntity<B> , ResponseEntity<B> | 返回值指定了完整的响应,包括 HTTP 头,并且通过HttpMessageWriter 实例对主体进行编码,并将其写入响应。 参见[ ResponseEntity ](#WebFlux-Ann-ResponseEntity)。 |
HttpHeaders | 返回带有标题而没有正文的响应。 |
String | 要用ViewResolver 实例解析的视图名称,并与隐式模型一起使用——通过命令对象和 @ModelAttribute 方法确定。处理程序方法还可以通过声明一个 Model 参数(描述earlier)以编程方式丰富模型。 |
View | 一个View 实例用于与隐式模型一起进行渲染——通过命令对象和方法确定 。处理程序方法还可以通过声明一个 Model 参数(描述earlier)以编程方式丰富模型。 |
java.util.Map , org.springframework.ui.Model | 要添加到隐式模型中的属性,并根据请求路径隐式地确定视图名称 。 |
@ModelAttribute | 要添加到模型中的一个属性,其视图名称是基于请求路径隐式确定的 。 注意,Beanutils#IsSimpleProperty (opens new window)是可选的。请参阅下面的 中的“任何其他返回值”。 |
Rendering | 用于模型和视图呈现场景的 API。 |
void | 如果方法具有void ,可能是异步的(例如Mono<Void> ),返回类型(或null 返回值),则认为该方法已完全处理了响应,如果该方法还具有 ServerHttpResponse 、参数,或 ServerWebExchange 注释。同样也是真的如果控制器进行了正的 ETag 或 lastModified 时间戳检查。//todo:详情请参见控制器。 如果以上都不是真的, void 返回类型还可以表示REST 控制器的“无响应体”或 HTML 控制器的默认视图名称选择。 |
Flux<ServerSentEvent> , Observable<ServerSentEvent> , or other reactive type | 发出服务器发送的事件。当只需要 写入数据时(但是, text/event-stream 必须通过属性在映射中请求或声明 text/event-stream ),可以省略ServerSentEvent 包装器。 |
Any other return value | 如果一个返回值与上述任一项不匹配,则默认情况下,它被视为一个视图 名称,如果它是 String 或(应用默认的视图名称选择),或者作为一个要添加到模型中的模型属性,除非是简单的类型,如由Beanutils#IsSimpleProperty (opens new window)确定的,否则 在这种情况下仍未解决。 |
# 类型转换
一些表示基于字符串的请求输入的带注释的控制器方法参数(例如,@RequestParam
,@RequestHeader
,@PathVariable
,@MatrixVariable
,和@CookieValue
)可以要求类型转换,如果参数被声明为String
以外的内容。
对于这样的情况,类型转换是基于配置的转换器自动应用的。默认情况下,支持简单类型(如int
、long
、Date
等)。可以通过WebDataBinder
(参见[null
](#WebFlux-Ann-initbinder))或通过使用FormattingConversionService
注册Formatters
来定制类型转换(参见Spring Field Formatting)。
类型转换中的一个实际问题是空字符串源值的处理。如果由于类型转换而使该值变为Spring Field Formatting,则将其视为缺失。这可能是Long
、UUID
和其他目标类型的情况。如果要允许注入null
,可以在参数注释上使用required
标志,或者将参数声明为null
。
# 矩阵变量
RFC 3986 (opens new window)讨论路径段中的名称-值对。在 Spring WebFlux 中,我们将那些称为基于 Tim Berners-Lee 的“old post” (opens new window)的“矩阵变量”,但它们也可以称为 URI 路径参数。
矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔——例如,"/cars;color=red,green;year=2012"
。还可以通过重复的变量名指定多个值——例如,"color=red;color=green;color=blue"
。
Spring 与 MVC 不同,在 WebFlux 中,URL 中是否存在矩阵变量并不影响请求映射。换句话说,你不需要使用 URI 变量来屏蔽变量内容。也就是说,如果你想从控制器方法访问矩阵变量,则需要在期望矩阵变量的路径段中添加一个 URI 变量。下面的示例展示了如何做到这一点:
Java
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
Kotlin
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有的路径段都可以包含矩阵变量,因此有时你可能需要消除矩阵变量预期在哪个路径变量中的歧义,如下例所示:
Java
// 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
}
Kotlin
@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
}
你可以定义一个可以定义为可选的矩阵变量,并指定一个默认值,如下例所示:
Java
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
Kotlin
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,请使用MultiValueMap
,如下例所示:
Java
// 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]
}
Kotlin
// 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
注释将查询参数绑定到控制器中的方法参数。下面的代码片段显示了该用法:
Java
@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 。 |
---|
Kotlin
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 使用@RequestParam 。 |
---|
Servlet API“Request Parameter”概念将查询参数、表单 数据和多个部分合并为一个。然而,在 WebFlux 中,每个都可以通过 ServerWebExchange 单独访问。虽然@RequestParam 仅绑定到查询参数,但你可以使用数据绑定来将查询参数、表单数据和多个部分应用到 @RequestParam 。 |
---|
默认情况下需要使用@RequestParam
注释的方法参数,但是可以通过将@RequestParam
的所需标志设置为false
,或者通过使用java.util.Optional
包装器声明参数来指定方法参数是可选的。
如果目标方法参数类型不是String
,则自动应用类型转换。见类型转换。
当在Map<String, String>
或MultiValueMap<String, String>
参数上声明
注释时,映射将填充所有查询参数。
请注意,@RequestParam
的使用是可选的——例如,用于设置其属性。默认情况下,任何参数是一个简单的值类型(由Beanutils#IsSimpleProperty (opens new window)确定)且不是由任何其他参数解析器解析的,都将被视为用@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
标题的值:
Java
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取Accept-Encoging 标头的值。 |
---|---|
2 | 获取Model 标头的值。 |
Kotlin
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | 获取Accept-Encoging 标头的值。 |
---|---|
2 | 获取Keep-Alive 标头的值。 |
如果目标方法参数类型不是String
,则自动应用类型转换。见类型转换。
当在@RequestHeader
、MultiValueMap<String, String>
或HttpHeaders
参数上使用@RequestHeader
注释时,映射将填充所有头值。
内置支持用于将逗号分隔的字符串转换为 数组或字符串集合或类型转换系统已知的其他类型。对于 示例,用 @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 值。 |
---|
Kotlin
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
1 | 获取 cookie 值。 |
---|
如果目标方法参数类型不是String
,则自动应用类型转换。见类型转换。
# @ModelAttribute
你可以在方法参数上使用@ModelAttribute
注释来访问模型中的一个属性,如果不存在,也可以实例化它。model 属性还覆盖了查询参数和表单字段的值,这些字段的名称与字段名称匹配。这被称为数据绑定,它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例绑定Pet
的实例:
爪哇
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 | 绑定Pet 的实例。 |
---|
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 | 绑定Pet 的实例。 |
---|
前面示例中的Pet
实例解析如下:
如果已经通过[
Model
](#Webflux-ann-modelattrib-methods)添加了该模型。从 HTTP 会话到[
@SessionAttributes
](#WebFlux-Ann-SessionAttributes)。从默认构造函数的调用。
调用带有匹配查询参数或表单字段的参数的“主构造函数”。参数名称是通过 爪哇Beans
@ConstructorProperties
或通过字节码中的运行时保留参数名称确定的。
在获得模型属性实例之后,再进行数据绑定。WebExchangeDataBinder
类将查询参数和窗体字段的名称与目标Object
上的字段名称匹配。在必要时应用类型转换后,将填充匹配字段。有关数据绑定(和验证)的更多信息,请参见验证。有关自定义数据绑定的更多信息,请参见[DataBinder
]。
数据绑定可能会导致错误。默认情况下,会引发WebExchangeBindException
,但是,要检查控制器方法中的此类错误,可以在BindingResult
旁边立即添加一个BindingResult
参数,如下例所示:
爪哇
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 添加BindingResult 。 |
---|
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 添加BindingResult 。 |
---|
通过添加javax.validation.Valid
注释或 Spring 的@Validated
注释,可以在数据绑定后自动应用验证(另请参见Bean Validation和Spring validation)。下面的示例使用@Valid
注释:
爪哇
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 在模型属性参数上使用@Valid 。 |
---|
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 在模型属性参数上使用@Valid 。 |
---|
Spring 与 Spring MVC 不同,WebFlux 在模型中支持反应性类型——例如,Mono<Account>
或io.reactivex.Single<Account>
。你可以声明一个@ModelAttribute
参数,带或不带反应性类型包装器,如果有必要,它将相应地解析为实际值。但是,请注意,要使用BindingResult
参数,你必须在不使用反应式类型包装器的情况下声明@ModelAttribute
参数,如前面所示。或者,你也可以通过 reactive 类型来处理任何错误,如下例所示:
爪哇
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}
Kotlin
@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 (opens new window)确定)且未由任何其他参数解析器解析的参数都将被视为已用@ModelAttribute
注释。
# @SessionAttributes
@SessionAttributes
用于在请求之间的WebSession
中存储模型属性。它是一种类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以供后续的访问请求使用。
考虑以下示例:
爪哇
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注释。 |
---|
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注释。 |
---|
在第一个请求中,当将名称为pet
的 model 属性添加到 model 时,它会自动升级到WebSession
并保存在WebSession
中。在另一个控制器方法使用SessionStatus
方法参数来清除存储之前,它一直保持不变,如下例所示:
爪哇
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete();
// ...
}
}
}
1 | 使用@SessionAttributes 注释。 |
---|---|
2 | 使用SessionStatus 变量。 |
Kotlin
@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 | 使用SessionStatus 。 |
---|
Kotlin
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
1 | 使用@SessionAttribute 。 |
---|
对于需要添加或删除会话属性的用例,可以考虑将WebSession
注入到控制器方法中。
对于将会话中的模型属性临时存储为控制器工作流的一部分,可以考虑使用SessionAttributes
,如[@SessionAttributes
]中所述。
# @RequestAttribute
与@SessionAttribute
类似,你可以使用@RequestAttribute
注释来访问先前创建的预先存在的请求属性(例如,通过WebFilter
),如下例所示:
爪哇
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 使用WebFilter 。 |
---|
Kotlin
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 使用@RequestAttribute 。 |
---|
# 多部分内容
正如多部分数据中所解释的,ServerWebExchange
提供了对多部分内容的访问。在控制器中处理文件上载表单(例如,从浏览器)的最佳方法是通过数据绑定到命令对象,如下例所示:
爪哇
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
Kotlin
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 获取文件。 |
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
}
1 | 使用@RequestPart 获取元数据。 |
---|---|
2 | 使用@RequestPart 获取文件。 |
要反序列化 RAW Part 内容(例如,到 JSON——类似于@RequestBody
),你可以声明一个具体的目标Object
,而不是多部分数据,如下例所示:
爪哇
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
1 | 使用@RequestPart 获取元数据。 |
---|
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
1 | 使用@RequestPart 获取元数据。 |
---|
你可以将@RequestPart
与javax.validation.Valid
或 Spring 的@Validated
注释结合使用,这将导致应用标准 Bean 验证。验证错误会导致WebExchangeBindException
,从而导致 400(bad_request)响应。异常包含带有错误详细信息的BindingResult
,也可以通过使用异步包装器声明参数并使用与错误相关的操作符来在 Controller 方法中进行处理:
爪哇
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
Kotlin
@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 。 |
---|
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
1 | 使用@RequestMapping 。 |
---|
要按顺序访问多部分数据,在流式方式中,你可以使用@RequestBody
与Flux<Part>
(或Flow<Part>
在 Kotlin 中)代替,如下例所示:
爪哇
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
1 | 使用@RequestBody 。 |
---|
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
// ...
}
1 | 使用@RequestBody 。 |
---|
# @RequestBody
你可以使用@RequestBody
注释,通过HttpMessageReader将请求主体读取并反序列化为Object
。下面的示例使用了@RequestBody
参数:
爪哇
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
与 Spring MVC 不同,在 WebFlux 中,@RequestBody
方法参数支持反应性类型和完全非阻塞的读取和(客户机到服务器)流。
爪哇
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
你可以使用WebFlux 配置的HTTP 消息编解码器选项来配置或自定义消息阅读器。
你可以将@RequestBody
与javax.validation.Valid
或 Spring 的Web MVC注释结合使用,这将导致应用标准 Bean 验证。验证错误会导致WebExchangeBindException
,从而导致 400(bad_request)响应。异常包含带有错误详细信息的BindingResult
,可以通过使用异步包装器声明参数,然后使用与错误相关的操作符,在 Controller 方法中进行处理:
爪哇
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
// use one of the onError* operators...
}
Kotlin
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
// ...
}
# HttpEntity
HttpEntity
与使用[@RequestBody
](#WebFlux-Ann-RequestBody)或多或少相同,但它基于一个容器对象,该对象公开了请求头和主体。下面的示例使用HttpEntity
:
爪哇
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
Kotlin
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
# @ResponseBody
你可以在方法上使用@ResponseBody
注释,通过HttpMessageWriter将返回序列化到响应主体。下面的示例展示了如何做到这一点:
爪哇
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别上也受到支持,在这种情况下,所有控制器方法都会继承它。这是@RestController
的效果,它不过是一个标记有@Controller
和@ResponseBody
的元注释。
@ResponseBody
支持反应类型,这意味着你可以返回 reactor 或 rxjava 类型,并将它们产生的异步值呈现给响应。有关更多详细信息,请参见Streaming和JSON 渲染。
你可以将@ResponseBody
方法与 JSON 序列化视图合并。详见JacksonJSON。
可以使用WebFlux 配置的HTTP 消息编解码器选项来配置或自定义消息写入。
# ResponseEntity
ResponseEntity
类似于[@ResponseBody
](#WebFlux-Ann-ResponseBody),但具有状态和标题。例如:
爪哇
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
Kotlin
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux 支持使用单个值反应型异步地生成ResponseEntity
,和/或为主体生成单个值和多值的反应类型。这允许使用ResponseEntity
的各种异步响应,如下所示:
ResponseEntity<Mono<T>>
或ResponseEntity<Flux<T>>
在稍后异步提供主体时,立即使响应状态和头为已知。如果主体由 0.1 个值组成,则使用Mono
;如果可以产生多个值,则使用Flux
。Mono<ResponseEntity<T>>
在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。Mono<ResponseEntity<Mono<T>>>
或Mono<ResponseEntity<Flux<T>>>
是另一种可能的选择,尽管不太常见。它们首先异步地提供响应状态和报头,然后是响应主体,也是异步地提供响应主体。
# JacksonJSON
Spring 提供对 JacksonJSON 库的支持。
# JSON 视图
Spring WebFlux 提供了对Jackson 的序列化视图 (opens new window)的内置支持,它只允许呈现Object
中所有字段的一个子集。要与@ResponseBody
或@ResponseBody
控制器方法一起使用它,你可以使用 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;
}
}
Kotlin
@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
}
允许一个视图类的数组,但是每个 控制器方法只能指定一个。如果需要激活多个视图,请使用复合接口。 |
---|
# 1.4.4.Model
你可以使用@ModelAttribute
注释:
在方法参数中的
@RequestMapping
方法上创建或访问模型中的对象,并通过WebDataBinder
将其绑定到请求。作为
@Controller
或@ModelAttribute
类中的方法级注释,在任何@RequestMapping
方法调用之前帮助初始化模型。在
@RequestMapping
方法上将其返回值标记为模型属性。
本节讨论@ModelAttribute
方法,或者前面列表中的第二个项。控制器可以有任意数量的@ModelAttribute
方法。所有这些方法都是在同一个控制器中的@RequestMapping
方法之前调用的。还可以通过@ControllerAdvice
在控制器之间共享@ModelAttribute
方法。有关更多详细信息,请参见财务总监建议一节。
@ModelAttribute
方法具有灵活的方法签名。它们支持许多与@RequestMapping
方法相同的参数(除了@ModelAttribute
本身和与请求主体相关的任何参数)。
下面的示例使用@ModelAttribute
方法:
爪哇
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
Kotlin
@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);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
当未显式指定名称时,默认的名称是基于类型选择的, ,正如在 爪哇doc 中对[ Conventions ](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/conventions.html)所解释的那样。通过重载的 addAttribute 方法或addAttribute 方法或(用于返回值)上的 name 属性,始终可以指定一个显式名称。 |
---|
Spring WebFlux 与 Spring MVC 不同,在模型中显式地支持反应类型(例如,或)。这样的异步模型属性可以在@RequestMapping
调用时透明地解析(并更新模型)到它们的实际值,只要不使用包装器声明@ModelAttribute
参数,如下例所示:
爪哇
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
// ...
}
Kotlin
import org.springframework.ui.set
@ModelAttribute
fun addAccount(@RequestParam number: String) {
val accountMono: Mono<Account> = accountRepository.findAccount(number)
model["account"] = accountMono
}
@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
// ...
}
此外,在视图呈现之前,具有反应性类型包装器的任何模型属性都将被解析为它们的实际值(以及模型更新)。
你还可以使用@ModelAttribute
作为@RequestMapping
方法的方法级注释,在这种情况下,@RequestMapping
方法的返回值被解释为一个模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是String
,否则该返回值将被解释为视图名称。@RequestBody
还可以帮助自定义模型属性名,如下例所示:
爪哇
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
# 1.4.5.DataBinder
@Controller
或@ControllerAdvice
类可以具有@InitBinder
方法,以初始化WebDataBinder
的实例。这些反过来又被用来:
将请求参数(即表单数据或查询)绑定到模型对象。
将基于
String
的请求值(例如请求参数、路径变量、头、cookie 和其他)转换为控制器方法参数的目标类型。在呈现 HTML 窗体时,将模型对象值格式化为
String
值。
@InitBinder
方法可以注册控制器特定的java.beans.PropertyEditor
或 Spring Converter
和Formatter
组件。此外,可以使用WebFlux 爪哇 配置在全局共享的FormattingConversionService
中注册Converter
和Formatter
类型。
@InitBinder
方法支持许多与@RequestMapping
方法相同的参数,但@ModelAttribute
(命令对象)参数除外。通常,它们是用WebDataBinder
参数声明的,用于注册,并使用void
返回值。下面的示例使用@InitBinder
注释:
爪哇
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
1 | 使用@InitBinder 注释。 |
---|
Kotlin
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
或者,当通过共享的FormattingConversionService
使用基于Formatter
的设置时,你可以重新使用相同的方法并注册特定于控制器的Formatter
实例,如下例所示:
爪哇
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
1 | 添加自定义格式化程序(在本例中为DateFormatter )。 |
---|
Kotlin
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
1 | 添加自定义格式化程序(在本例中为DateFormatter )。 |
---|
# 1.4.6.管理异常
@Controller
和@controlleradvice类可以使用@ExceptionHandler
方法来处理来自控制器方法的异常。以下示例包括这样的处理程序方法:
爪哇
@Controller
public class SimpleController {
// ...
@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
1 | 声明@ExceptionHandler 。 |
---|
Kotlin
@Controller
class SimpleController {
// ...
@ExceptionHandler (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
1 | 声明@ExceptionHandler 。 |
---|
异常可以与正在传播的顶级异常(即抛出的直接IOException
)匹配,也可以与顶级包装异常中的直接原因匹配(例如,在IOException
中包装的IllegalStateException
)。
为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。或者,注释声明可以缩小异常类型以进行匹配。我们通常建议在参数签名中尽可能具体,并在@ControllerAdvice
上以相应的顺序优先声明主根异常映射。详见MVC 部门。
WebFlux 中的@ExceptionHandler 方法支持与方法相同的方法参数和 返回值,但请求主体- 和 @ModelAttribute -相关的方法参数除外。 |
---|
Spring WebFlux 中对@ExceptionHandler
方法的支持是由HandlerAdapter
方法提供的。有关更多详细信息,请参见[DispatcherHandler
]。
# REST API 异常
REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动这样做,因为响应主体中的错误细节的表示是特定于应用程序的。但是,@RestController
可以使用带有ResponseEntity
返回值的@ExceptionHandler
方法来设置响应的状态和主体。这样的方法也可以在@ControllerAdvice
类中声明,以在全局范围内应用它们。
请注意, Spring WebFlux 对于 Spring MVC没有一个等价的,因为 WebFlux 仅引发(或其子类),并且这些不需要被翻译为一个 HTTP 状态代码。 |
---|
# 1.4.7.财务总监建议
通常,@ExceptionHandler
、@InitBinder
和@ModelAttribute
方法应用于声明它们的@Controller
类(或类层次结构)中。如果你希望这样的方法在全局范围内(在控制器之间)应用得更多,那么可以在一个用@ControllerAdvice
或@RestControllerAdvice
注释的类中声明它们。
@ControllerAdvice
注释为@Component
,这意味着这样的类可以通过组件扫描注册为 Spring bean。@RestControllerAdvice
是一种组合注释,它同时使用@ControllerAdvice
和@ResponseBody
进行注释,其本质上意味着@ExceptionHandler
方法通过消息转换(与视图解析或模板呈现)呈现到响应主体。
在启动时,@RequestMapping
和@ExceptionHandler
方法的基础设施类检测用@ControllerAdvice
注释的 Spring bean,然后在运行时应用它们的方法。全局@ExceptionHandler
方法(来自@ControllerAdvice
)应用于之后局部方法(来自@Controller
)。相比之下,全局@ModelAttribute
和@InitBinder
方法应用于局部方法在此之前。
默认情况下,@ControllerAdvice
方法适用于每个请求(即所有控制器),但你可以通过使用注释上的属性将其缩小到控制器的子集,如下例所示:
爪哇
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
Kotlin
// 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
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/controlleradvice.html)爪哇doc。
# 1.5.功能端点
Spring WebFlux 包括 WebFlux.FN,这是一种轻量级函数式编程模型,其中函数被用于路由和处理请求,并且契约被设计为具有不可变性。它是基于注释的编程模型的一种替代方案,但在其他情况下运行在相同的反应核基础上。
# 1.5.1.概述
在 WebFlux.FN 中,一个 HTTP 请求是用HandlerFunction
处理的:一个函数接受ServerRequest
并返回一个延迟的ServerResponse
(即Mono<ServerResponse>
)。请求和响应对象都具有不可更改的契约,这些契约提供对 HTTP 请求和响应的 JDK8 友好访问。HandlerFunction
相当于基于注释的编程模型中的@RequestMapping
方法的主体。
传入的请求被路由到带有RouterFunction
的处理程序函数:该函数接受ServerRequest
并返回延迟的HandlerFunction
(即Mono<HandlerFunction>
)。当路由器函数匹配时,将返回一个处理程序函数;否则将返回一个空的 mono。RouterFunction
相当于@RequestMapping
注释,但与此的主要区别是,路由器函数不仅提供数据,还提供行为。
RouterFunctions.route()
提供了一个路由器构建器,可以促进路由器的创建,如下例所示:
爪哇
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
Kotlin
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 爪哇 配置运行,参见运行服务器。
# 1.5.2.handlerfunction
ServerRequest
和ServerResponse
是不可变的接口,它们提供对 HTTP 请求和响应的 JDK8 友好访问。请求和响应都针对体流提供反应流 (opens new window)反压。请求主体用反应器Flux
或Mono
表示。响应体用任何反应流Publisher
表示,包括Flux
和Mono
。有关该问题的更多信息,请参见反应库。
# ServerRequest
ServerRequest
提供对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问是通过body
方法提供的。
下面的示例将请求主体提取到Mono<String>
:
爪哇
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()
下面的示例将主体提取到Flux<Person>
(或 Kotlin 中的Flow<Person>
),其中Person
对象是从形式化的形式(例如 JSON 或 XML)中解码的:
爪哇
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
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));
Kotlin
val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
下面的示例展示了如何访问表单数据:
爪哇
Mono<MultiValueMap<String, String>> map = request.formData();
Kotlin
val map = request.awaitFormData()
下面的示例展示了如何以地图的形式访问多部分数据:
爪哇
Mono<MultiValueMap<String, Part>> map = request.multipartData();
Kotlin
val map = request.awaitMultipartData()
下面的示例展示了如何以流媒体方式一次访问多个部分:
爪哇
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()
# ServerResponse
ServerResponse
提供对 HTTP 响应的访问,由于它是不可变的,你可以使用build
方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200(OK)响应:
爪哇
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
Kotlin
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
下面的示例展示了如何使用Location
标头构建 201(已创建)响应,而不使用正文:
爪哇
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()
根据使用的编解码器,可以传递提示参数来自定义如何序列化或反序列化主体。例如,要指定JacksonJSON 视图 (opens new window):
爪哇
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
# 处理程序类
我们可以将处理程序函数编写为 lambda,如下例所示:
爪哇
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
这很方便,但在一个应用程序中,我们需要多个功能,而多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数组合成一个处理程序类是有用的,该处理程序类在基于注释的应用程序中具有与@Controller
类似的作用。例如,下面的类公开了一个反应性Person
存储库:
爪哇
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
1 | listPeople 是一个处理函数,它将存储库中找到的所有Person 对象作为json 返回。 |
---|---|
2 | createPerson 是一个处理函数,它存储了一个包含在请求主体中的新Person 。注意, PersonRepository.savePerson(Person) 返回Mono<Void> :一个空的Mono ,当该人已从请求中读取并存储时,它会发出完成信号。因此,我们使用 build(Publisher<Void>) 方法在接收到完成信号时(即保存了 Person 时)发送响应。 |
3 | getPerson 是一个处理函数,它返回一个人,由id 路径变量标识。我们从存储库中检索 Person 并创建一个 JSON 响应,如果找到了。如果没有找到它,我们使用 switchIfEmpty(Mono<T>) 返回 404Not Found 响应。 |
Kotlin
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
1 | listPeople 是一个处理函数,它将存储库中找到的所有Person 对象作为json 返回。 |
---|---|
2 | createPerson 是一个处理函数,它存储了一个包含在请求主体中的新Person 。注意, PersonRepository.savePerson(Person) 是一个没有返回类型的挂起函数。 |
3 | getPerson 是一个处理函数,它返回一个人,由id 路径变量标识。我们从存储库中检索 Person 并创建一个 JSON 响应,如果找到了。如果没有找到它,我们将返回 404Not Found 响应。 |
# Validation
功能端点可以使用 Spring 的验证设施将验证应用于请求主体。例如,给定用于Person
的自定义 Spring Validator实现:
爪哇
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 响应的例外情况。 |
Kotlin
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 响应的例外情况。 |
处理程序还可以通过基于LocalValidatorFactoryBean
创建和注入一个全局Validator
实例来使用标准 Bean 验证 API(JSR-303)。见Spring Validation。
# 1.5.3.RouterFunction
路由器函数用于将请求路由到相应的HandlerFunction
。通常,你不会自己编写路由器函数,而是使用RouterFunctions
实用程序类上的一个方法来创建一个。RouterFunctions.route()
(无参数)为你提供了用于创建路由器函数的 Fluent 构建器,而RouterFunctions.route(RequestPredicate, HandlerFunction)
提供了一种直接创建路由器的方法。
通常,建议使用route()
Builder,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现的静态导入。例如,Router Function Builder 提供了方法GET(String, HandlerFunction)
来创建 GET 请求的映射;以及POST(String, HandlerFunction)
用于 POST。
除了基于 HTTP 方法的映射,Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变量,该变量将RequestPredicate
作为参数,尽管可以表示该参数的附加约束。
# 谓词
你可以编写自己的RequestPredicate
,但是RequestPredicates
实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于Accept
头创建一个约束:
爪哇
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
Kotlin
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
谓词组合在一起。
# 路线
对路由器的功能按顺序进行评估:如果第一条路由不匹配,则对第二条路由进行评估,依此类推。因此,在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时,这一点也很重要,后面将对此进行说明。请注意,这种行为与基于注释的编程模型不同,在该模型中,“最特定的”控制器方法是自动选择的。
当使用 Router 函数 builder 时,所有定义的路由都被组合成一个RouterFunction
,从build()
返回。还有其他方法可以将多个路由器功能组合在一起:
add(RouterFunction)
上的RouterFunctions.route()
构建器RouterFunction.and(RouterFunction)
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
—带有嵌套RouterFunctions.route()
的RouterFunction.and()
的快捷方式。
下面的示例显示了四条路线的组成:
爪哇
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
1 | 带有与 JSON 匹配的Accept 标头的GET /person/{id} 被路由到PersonHandler.getPerson |
---|---|
2 | 带有与 JSON 匹配的Accept 头的GET /person 被路由到PersonHandler.listPeople |
3 | 没有附加谓词的POST /person 映射到PersonHandler.createPerson ,并且 |
4 | otherRoute 是在其他地方创建的路由器功能,并将其添加到构建的路由中。 |
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | 带有与 JSON 匹配的GET /person/{id} 头的Accept 被路由到PersonHandler.getPerson |
---|---|
2 | 带有与 JSON 匹配的GET /person 头的Accept 被路由到PersonHandler.listPeople |
3 | 没有附加谓词的POST /person 映射到PersonHandler.createPerson ,并且 |
4 | otherRoute 是在其他地方创建的路由器功能,并将其添加到构建的路由中。 |
# 嵌套路线
一组路由器函数通常有一个共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配/person
的路径谓词,由三个路由使用。在使用注释时,可以使用映射到/person
的类型级@RequestMapping
注释来删除这种重复。在 WebFlux.FN 中,路径谓词可以通过 Router Function Builder 上的path
方法共享。例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行:
爪哇
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
1 | 请注意,path 的第二个参数是接受路由器生成器的使用者。 |
---|
Kotlin
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以通过在 Builder 上使用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();
Kotlin
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 使用)是通过WebFlux 配置使用基于[DispatcherHandler
](#WebFlux-Dispatcher-Handler)的设置运行,该设置使用 Spring 配置来声明处理请求所需的组件。WebFlux 爪哇 配置声明了以下支持功能端点的基础设施组件:
RouterFunctionMapping
:在 Spring 配置中检测一个或多个RouterFunction<?>
bean,命令他们,通过RouterFunction.andOther
对它们进行组合,并将请求路由到结果组合的RouterFunction
。HandlerFunctionAdapter
:允许DispatcherHandler
调用映射到请求的HandlerFunction
的简单适配器。ServerResponseResultHandler
:通过调用ServerResponse
的writeTo
方法来处理调用HandlerFunction
的结果。
前面的组件让功能端点适合DispatcherHandler
请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器的话)并排运行。这也是 Spring 引导 WebFlux 启动器启用功能端点的方式。
下面的示例显示了一个 WebFlux 爪哇 配置(有关如何运行它,请参见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...
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
# 1.5.5.过滤处理程序函数
你可以使用路由函数生成器上的before
、after
或filter
方法来过滤处理程序函数。对于注释,你可以通过使用@ControllerAdvice
、ServletFilter
或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如,考虑以下示例:
爪哇
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 添加自定义请求头的before 过滤器仅应用于两个 GET 路由。 |
---|---|
2 | 记录响应的after 过滤器应用于所有路由,包括嵌套的路由。 |
Kotlin
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST("/person", handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
}
1 | 添加自定义请求头的before 过滤器仅应用于两个 GET 路由。 |
---|---|
2 | 记录响应的after 过滤器应用于所有路由,包括嵌套的路由。 |
路由器构建器上的filter
方法接受HandlerFilterFunction
:一个函数接受ServerRequest
和HandlerFunction
并返回ServerResponse
。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。
现在,我们可以在路由中添加一个简单的安全过滤器,假设我们有一个SecurityManager
,它可以确定是否允许特定的路径。下面的示例展示了如何做到这一点:
爪哇
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
Kotlin
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用next.handle(ServerRequest)
是可选的。我们只允许在允许访问的情况下运行处理程序函数。
除了在路由器功能构建器上使用filter
方法外,还可以通过RouterFunction.filter(HandlerFilterFunction)
对现有的路由器功能应用过滤器。
CORS 对功能端点的支持是通过专用的[CorsWebFilter ]提供的。 |
---|
# 1.6.URI 链接
本节描述了在 Spring 框架中可用来准备 URI 的各种选项。
# 1.6.1.尿酸成分
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 。 |
Kotlin
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();
Kotlin
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");
Kotlin
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");
Kotlin
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
# 1.6.2.UriBuilder
Spring MVC 和 Spring WebFlux
[UriComponentsBuilder
]实现UriBuilder
。你可以创建UriBuilder
,然后使用UriBuilderFactory
。同时,UriBuilderFactory
和UriBuilder
提供了一种基于共享配置(例如基本 URL、编码首选项和其他细节)的可插入机制,用于从 URI 模板构建 URI。
你可以使用UriBuilderFactory
配置RestTemplate
和WebClient
来定制 URI 的准备。DefaultUriBuilderFactory
是UriBuilderFactory
的默认实现,它在内部使用UriComponentsBuilder
并公开共享配置选项。
下面的示例展示了如何配置RestTemplate
:
Java
// 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);
Kotlin
// 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
:
Java
// 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();
Kotlin
// 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
,但它是一个实际的实例,它保存配置和首选项,而不是静态工厂方法,如下例所示:
Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
Kotlin
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() (opens new window):先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。
uricomponents#encode() (opens new window):编码 URI 组件之后URI 变量被展开。
这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而,第一个选项也用 URI 变量中出现的保留意义替换字符。
考虑一下“;”,它在某种程度上是合法的,但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换 ;;",但不在 URI 模板中。相比之下,第二个选项永远不会 取代“;”,因为它是路径中的法律字符。 |
---|
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为不透明的数据来进行完全编码,而如果 URI 变量故意包含保留字符,则第二个选项是有用的。当完全不展开 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的内容进行编码。
下面的示例使用了第一个选项:
Java
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"
Kotlin
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(这意味着编码)来缩短前面的示例,如下例所示:
Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
可以使用完整的 URI 模板进一步缩短它,如下例所示:
Java
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
Kotlin
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
WebClient
和RestTemplate
通过UriBuilderFactory
策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示:
Java
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();
Kotlin
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
DefaultUriBuilderFactory
实现在内部使用UriComponentsBuilder
来扩展和编码 URI 模板。作为工厂,它提供了一个单独的位置来配置编码方法,该方法基于以下编码模式之一:
TEMPLATE_AND_VALUES
:使用UriComponentsBuilder#encode()
(对应于前面列表中的第一个选项)对 URI 模板进行预编码,并在展开时对 URI 变量进行严格编码。VALUES_ONLY
:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过UriUtils#encodeUriVariables
对 URI 变量进行严格编码。URI_COMPONENT
:使用UriComponents#encode()
,对应于前面列表中的第二个选项,来对 URI 组件值的编码之后URI 变量进行扩展。NONE
:不应用编码。
由于历史原因和向后兼容,RestTemplate
被设置为EncodingMode.URI_COMPONENT
。WebClient
依赖于DefaultUriBuilderFactory
中的默认值,该默认值从 5.0.x 中的EncodingMode.URI_COMPONENT
更改为 5.1 中的EncodingMode.TEMPLATE_AND_VALUES
。
# 1.7.科尔斯
Spring WebFlux 允许你处理 CORS(跨源资源共享)。这一节描述了如何做到这一点。
# 1.7.1.导言
出于安全原因,浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如,你可以在一个标签中设置你的银行帐户,而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如,从你的帐户中取款!
跨源资源共享是由大多数浏览器 (opens new window)实现的W3C 规范 (opens new window),它允许你指定授权哪种类型的跨域请求,而不是使用基于 iframe 或 JSONP 的安全性较低、功能较弱的解决方案。
# 1.7.2.处理
CORS 规范区分了飞行前、简单和实际请求。要了解 CORS 的工作原理,你可以阅读这篇文章 (opens new window)等,或者查看规范以获得更多详细信息。
Spring WebFluxHandlerMapping
实现为 CORS 提供了内置支持。在成功地将一个请求映射到一个处理程序之后,HandlerMapping
检查 CORS 配置中给定的请求和处理程序,并采取进一步的操作。前置请求是直接处理的,而简单和实际的 CORS 请求是截获、验证的,并设置了所需的 CORS 响应头。
为了启用跨源请求(即存在Origin
头并与请求的主机不同),你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。
每个HandlerMapping
都可以单独使用基于 URL 模式的configured (opens new window)映射CorsConfiguration
。在大多数情况下,应用程序使用 WebFlux Java 配置来声明这样的映射,这将导致一个单一的全局映射传递给所有HandlerMapping
实现。
你可以将HandlerMapping
级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如,带注释的控制器可以使用类或方法级别的@CrossOrigin
注释(其他处理程序可以实现CorsConfigurationSource
)。
结合全局和局部配置的规则通常是累加的——例如,所有全局配置和所有局部配置。对于那些只能接受单个值的属性,例如allowCredentials
和maxAge
,本地重写全局值。详见[CorsConfiguration#combine(CorsConfiguration)
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/corsconfiguration.html#combine-org.springframework.web.cors.corsconfiguration-)。
要从源代码中了解更多信息或进行高级定制,请参见:CorsConfiguration AbstractHandlerMapping 和 |
---|
# 1.7.3.@CrossOrigin
[@CrossOrigin
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/crossorigin.html)注解可以在带注释的控制器方法上实现跨源请求,如下例所示:
爪哇
@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) {
// ...
}
}
Kotlin
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
所有的起源。
所有标题。
将控制器方法映射到的所有 HTTP 方法。
allowCredentials
默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用allowOrigins
时,要么必须将allowOrigins
设置为一个或多个特定域(但不是特定值"*"
),要么可选择将allowOriginPatterns
属性用于匹配到源集的动态。
maxAge
设置为 30 分钟。
@CrossOrigin
在类级别上也受到支持,并被所有方法继承。下面的示例指定了一个特定的域,并将maxAge
设置为一个小时:
爪哇
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
Kotlin
@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 。 |
Kotlin
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com") (2)
@GetMapping("/{id}")
suspend fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
suspend fun remove(@PathVariable id: Long) {
// ...
}
}
1 | 在类级别上使用@CrossOrigin 。 |
---|---|
2 | 在方法级别使用@CrossOrigin 。 |
# 1.7.4.全局配置
除了细粒度的控制器方法级配置外,你可能还需要定义一些全局 CORS 配置。你可以在任何HandlerMapping
上单独设置基于 URL 的CorsConfiguration
映射。然而,大多数应用程序都使用 WebFlux 爪哇 配置来实现这一点。
默认情况下,全局配置启用以下功能:
所有的起源。
所有标题。
GET
,HEAD
,和POST
方法。
allowedCredentials
默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用allowOrigins
时,要么必须将allowOrigins
设置为一个或多个特定的域(但不是特殊值"*"
),要么将allowOriginPatterns
属性用于匹配到源集的动态。
maxAge
设置为 30 分钟。
要在 WebFlux 爪哇 配置中启用 CORS,可以使用CorsRegistry
回调,如下例所示:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
Kotlin
@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
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/active/corswebfilter.html)应用 CORS 支持,这与功能端点很好地匹配。
如果你试图将CorsFilter 与 Spring 安全性一起使用,请记住,对于 CORS, Spring 安全性具有内置支持 (opens new window)。 |
---|
要配置过滤器,你可以声明一个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);
}
Kotlin
@Bean
fun corsFilter(): CorsWebFilter {
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", config)
}
return CorsWebFilter(source)
}
# 1.8.网络安全
Spring Security (opens new window)项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档,包括:
# 1.9.查看技术
Spring WebFlux 中对视图技术的使用是可插入的。是否决定使用 ThymeLeaf、FreeMarker 或其他一些视图技术主要是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。我们假设你已经熟悉视图分辨率。
# 1.9.1.百里香叶
ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎,强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于在 UI 模板上独立工作(例如,由设计师)非常有帮助,而不需要运行的服务器。Thymeleaf 提供了一套广泛的功能,并且它是积极开发和维护的。有关更完整的介绍,请参见Thymeleaf (opens new window)项目主页。
ThymeLeaf 与 Spring WebFlux 的集成由 ThymeLeaf 项目管理。配置涉及一些 Bean 声明,例如SpringResourceTemplateResolver
、SpringWebFluxTemplateEngine
和ThymeleafReactiveViewResolver
。有关更多详细信息,请参见Thymeleaf+Spring (opens new window)和 WebFlux 集成公告 (opens new window)。
# 1.9.2.自由标记
Apache Freemarker (opens new window)是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring WebFlux 和 Freemarker 模板的内置集成。
# 视图配置
下面的示例展示了如何将 Freemarker 配置为一种视图技术:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
return configurer;
}
}
Kotlin
@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
模板。
# 自由标记配置
通过在FreeMarkerConfigurer
Bean 上设置适当的 Bean 属性,可以将自由标记’settings’和’sharedvariables’直接传递给自由标记Configuration
对象(由 Spring 管理)。freemarkerSettings
属性需要一个java.util.Properties
对象,而freemarkerVariables
属性需要一个java.util.Map
。下面的示例展示了如何使用FreeMarkerConfigurer
:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// ...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
Map<String, Object> variables = new HashMap<>();
variables.put("xml_escape", new XmlEscape());
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
configurer.setFreemarkerVariables(variables);
return configurer;
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// ...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
}
}
有关应用于Configuration
对象的设置和变量的详细信息,请参见 Freemarker 文档。
# 表单处理
Spring 提供了用于 JSP 的标记库,该标记库包括<spring:bind/>
元素。这个元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中Validator
的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持,具有用于生成表单输入元素本身的附加方便宏。
# BIND 宏
在spring-webflux.jar
freemarker 文件中维护了一组标准的宏,因此对于适当配置的应用程序,它们总是可用的。
Spring 模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围,这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,那么该文件被称为spring.ftl
,并且位于org.springframework.web.reactive.result.view.freemarker
包中。
有关绑定支持的更多详细信息,请参见简单绑定中的 Spring MVC。
# 表格宏
有关 Spring 对自由标记模板的表单宏支持的详细信息,请参阅 Spring MVC 文档的以下部分。
# 1.9.3.脚本视图
Spring 框架有一个内置的集成,用于使用 Spring WebFlux 和任何模板库,这些模板库可以在JSR-223 (opens new window)爪哇 脚本引擎之上运行。下表显示了我们在不同的脚本引擎上测试过的模板库:
集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngine 和Invocable 接口。 |
---|
# 所需经费
你需要在 Classpath 上安装脚本引擎,其细节因脚本引擎而异:
Nashorn (opens new window)爪哇Script 引擎由 爪哇8+ 提供。强烈推荐使用最新的可用更新版本。
JRuby (opens new window)应该作为 Ruby 支持的依赖项添加。
Jython (opens new window)应该作为 Python 支持的依赖项添加。
对于 Kotlin 脚本支持,应该添加
org.jetbrains.kotlin:kotlin-script-util
依赖项和包含META-INF/services/javax.script.ScriptEngineFactory
行的org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
文件。有关更多详细信息,请参见这个例子 (opens new window)。
你需要有脚本模板库。实现 爪哇Script 的一种方法是通过WebJars (opens new window)。
# 脚本模板
你可以声明一个ScriptTemplateConfigurer
Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn 爪哇Script 引擎:
爪哇
@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;
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
使用以下参数调用render
函数:
String template
:模板内容Map model
:视图模型RenderingContext renderingContext
:[RenderingContext
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/script/renderingcontext.html),它提供了对应用程序上下文、区域设置、模板装入器和 URL(自 5.0 起)的访问权限
Mustache.render()
与该签名在本机上兼容,因此你可以直接调用它。
如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,Handlerbars (opens new window)在使用模板之前需要对其进行编译,并且需要polyfill (opens new window),以便模拟服务器端脚本引擎中不可用的一些浏览器功能。下面的示例展示了如何设置自定义呈现函数:
爪哇
@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;
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
当使用非线程安全的 脚本引擎时,需要将 sharedEngine 属性设置为false ,该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或React。在那种情况下,由于this bug (opens new window),爪哇 SE8Update60 是必需的,但是一般情况下 推荐在任何情况下使用最近发布的 爪哇 SE 补丁。 |
---|
polyfill.js
只定义了处理栏正常运行所需的window
对象,如以下代码片段所示:
var window = {};
这个基本的render.js
实现在使用模板之前对其进行编译。为生产准备好的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端完成,也可以在你需要的任何定制中完成(例如,管理模板引擎配置)。下面的示例展示了如何编译模板:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
查看 Spring Framework Unit 测试,爪哇 (opens new window)和resources (opens new window),以获得更多配置示例。
# 1.9.4.JSON 和 XML
出于内容协商的目的,可以根据客户机请求的内容类型,在使用 HTML 模板或其他格式(例如 JSON 或 XML)呈现模型之间进行替换,这是非常有用的。为了支持这样做, Spring WebFlux 提供了HttpMessageWriterView
,你可以使用它来插入spring-web
中的任何可用的Codecs,例如Jackson2JsonEncoder
、Jackson2SmileEncoder
或Jaxb2XmlEncoder
。
与其他视图技术不同,HttpMessageWriterView
不需要ViewResolver
,而是将configured作为默认视图。你可以配置一个或多个这样的默认视图,包装不同的HttpMessageWriter
实例或Encoder
实例。在运行时使用与请求的内容类型匹配的内容类型。
在大多数情况下,一个模型包含多个属性。要确定要序列化哪个,你可以配置HttpMessageWriterView
,并使用要用于呈现的 model 属性的名称。如果模型只包含一个属性,则使用该属性。
# 1.10.HTTP 缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕Cache-Control
响应头和后续的条件请求头,例如Last-Modified
和ETag
。Cache-Control
建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。ETag
报头用于发出条件请求,如果内容没有更改,则该请求可能在没有正文的情况下导致 304(未 _modified)。ETag
可以看作是Last-Modified
页眉的更复杂的继承者。
本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。
# 1.10.1.CacheControl
[CacheControl
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/cachecontrol.html)提供了对配置Cache-Control
标头相关设置的支持,并在许多地方被接受为参数:
虽然RFC 7234 (opens new window)描述了Cache-Control
响应头的所有可能的指令,但CacheControl
类型采用了一种面向用例的方法,该方法专注于常见的场景,如下例所示:
爪哇
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
Kotlin
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
# 1.10.2.控制器
控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的lastModified
或ETag
值需要在与条件请求头进行比较之前进行计算。控制器可以将ETag
和Cache-Control
设置添加到ResponseEntity
中,如下例所示:
爪哇
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
Kotlin
@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(未 _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(未修改)。没有进一步的处理。 |
3 | 继续处理请求。 |
Kotlin
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {
val eTag: Long = ... (1)
if (exchange.checkNotModified(eTag)) {
return null(2)
}
model.addAttribute(...) (3)
return "myViewName"
}
1 | 应用程序特定的计算。 |
---|---|
2 | 响应已设置为 304(未修改)。没有进一步的处理。 |
3 | 继续处理请求。 |
针对eTag
值、lastModified
值或两者检查条件请求有三种变体。对于条件GET
和HEAD
请求,可以将响应设置为 304(不是 _modified)。对于条件POST
、PUT
和DELETE
,可以将响应设置为 412(前提条件 _ 失败),以防止并发修改。
# 1.10.3.静态资源
为了获得最佳性能,你应该使用Cache-Control
和条件响应头来服务静态资源。参见关于配置静态资源的部分。
# 1.11.WebFlux 配置
WebFlux 爪哇 配置声明了用带注释的控制器或功能端点处理请求所需的组件,并提供了一个 API 来定制配置。这意味着你不需要理解由 爪哇 配置创建的底层 bean。但是,如果你想了解它们,可以在WebFluxConfigurationSupport
中看到它们,或者在Special Bean Types中阅读有关它们的更多信息。
对于配置 API 中没有的更高级的定制,你可以通过高级配置模式获得对配置的完全控制。
# 1.11.1.启用 WebFlux 配置
你可以在 爪哇 配置中使用@EnableWebFlux
注释,如下例所示:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig {
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig
前面的示例注册了许多 Spring WebFlux基础设施 bean,并适应了 Classpath 上可用的依赖关系——对于 JSON、XML 和其他的依赖关系。
# 1.11.2.WebFlux 配置 API
在你的 爪哇 配置中,你可以实现WebFluxConfigurer
接口,如下例所示:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// Implement configuration methods...
}
# 1.11.3.转换、格式化
默认情况下,安装了用于各种数字和日期类型的格式化程序,并支持在字段上通过@NumberFormat
和@DateTimeFormat
进行定制。
要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
Spring 默认情况下,WebFlux 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
Kotlin
@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 验证器)上,则LocalValidatorFactoryBean
注册为全局validator,用于@Valid
和@Validated
上的@Controller
方法参数。
在你的 爪哇 配置中,你可以自定义全局Validator
实例,如下例所示:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun getValidator(): Validator {
// ...
}
}
请注意,你也可以在本地注册Validator
实现,如下例所示:
爪哇
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
Kotlin
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
如果需要在某个地方注入一个LocalValidatorFactoryBean ,请创建一个 Bean 并将标记为 @Primary ,以避免与 MVC 配置中声明的那个冲突。 |
---|
# 1.11.5.内容类型解析器
你可以配置 Spring WebFlux 如何从请求中确定@Controller
实例所请求的媒体类型。默认情况下,只检查Accept
头,但你也可以启用基于查询参数的策略。
下面的示例展示了如何自定义所请求的内容类型分辨率:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}
Kotlin
@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);
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
}
}
ServerCodecConfigurer
提供了一组默认的读取器和写入器。你可以使用它来添加更多的读取器和编写器,自定义缺省的读取器和编写器,或者完全替换缺省的读取器和编写器。
对于 Jackson 的 JSON 和 XML,可以考虑使用[Jackson2ObjectMapperBuilder
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2objectmapperbuilder.html),它使用以下属性定制 Jackson 的默认属性:
[
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/deserializationfeature.html#fail_on_unknown_properties)被禁用。[
MapperFeature.DEFAULT_VIEW_INCLUSION
](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/mapperfeature.html#default_view_inclusion)被禁用。
如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块:
[
jackson-datatype-joda
](https://github.com/fasterxml/Jackson-datatype-joda):支持 joda-time 类型。[
jackson-datatype-jsr310
](https://github.com/fasterxml/Jackson-datatype-jsr310):支持 爪哇8 日期和时间 API 类型。[
jackson-datatype-jdk8
](https://github.com/fasterxml/Jackson-datatype-jdk8):支持其他 爪哇8 类型,例如Optional
。[
jackson-module-kotlin
](https://github.com/fasterxml/Jackson-module- Kotlin):支持 Kotlin 类和数据类。
# 1.11.7.视图解析器
下面的示例展示了如何配置视图分辨率:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// ...
}
}
ViewResolverRegistry
具有 Spring 框架与之集成的视图技术的快捷方式。下面的示例使用 Freemarker(这也需要配置底层的 Freemarker 视图技术):
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure Freemarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
return configurer;
}
}
Kotlin
@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);
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
val resolver: ViewResolver = ...
registry.viewResolver(resolver
}
}
为了支持内容协商和通过视图分辨率呈现其他格式(除了 HTML),你可以基于HttpMessageWriterView
实现配置一个或多个默认视图,该实现接受来自spring-web
的任何可用的Codecs。下面的示例展示了如何做到这一点:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = Jackson2JsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
// ...
}
有关集成 Spring WebFlux 的视图技术的更多信息,请参见查看技术。
# 1.11.8.静态资源
此选项提供了一种方便的方式,可以从[Resource
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/resource.html)-based 位置的列表中提供静态资源。
在下一个示例中,给定一个以/resources
开头的请求,相对路径用于在 Classpath 上查找和服务相对于/static
的静态资源。资源将在一年后到期,以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还计算Last-Modified
头,如果存在,则返回304
状态代码。下面的列表显示了该示例:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
}
}
资源处理程序还支持[ResourceResolver
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/reactive/resource/资源olever.html)实现和[ResourceTransformer
](https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javoc-api-api-api-api-api/org/spractuframework/org/resource/resource/resource/resolver.html)
你可以使用VersionResourceResolver
实现基于内容、固定应用程序版本或其他信息计算的 MD5 散列的版本管理的资源 URL。aContentVersionStrategy
(md5hash)是一个很好的选择,但有一些明显的例外(例如与模块装入器一起使用的 爪哇Script 资源)。
下面的示例展示了如何在 爪哇 配置中使用VersionResourceResolver
:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
你可以使用ResourceUrlProvider
重写 URL,并应用完整的解析器和转换器(例如,用于插入版本)。WebFlux 配置提供了一个ResourceUrlProvider
,以便可以将其注入到其他配置中。
Spring 与 MVC 不同的是,目前,在 WebFlux 中,还没有透明地重写静态资源 URL 的方法,因为还没有视图技术可以利用解析器和转换器的非阻塞链。当只提供本地资源时,解决方法是直接使用ResourceUrlProvider
(例如,通过自定义元素)和块。
请注意,当同时使用EncodedResourceResolver
(例如,gzip,Brotli 编码)和VersionedResourceResolver
时,它们必须按该顺序进行注册,以确保始终基于未编码文件可靠地计算基于内容的版本。
WebJars (opens new window)也通过WebJarsResourceResolver
来支持,这是在 Classpath 上存在org.webjars:webjars-locator-core
库时自动注册的。解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从/jquery/jquery.min.js
到/jquery/1.2.0/jquery.min.js
。
基于ResourceHandlerRegistry 的 爪哇 配置为细粒度控制提供了进一步的选项,例如,上次修改行为和优化的资源解析。 |
---|
# 1.11.9.路径匹配
你可以自定义与路径匹配相关的选项。有关单个选项的详细信息,请参见[PathMatchConfigurer
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/config/pathmatchconfigrer.html)爪哇doc。下面的示例展示了如何使用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));
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController::class.java))
}
}
Spring WebFlux 依赖于对名为RequestPath 的请求路径的解析表示,用于访问已解码的路径段值,并删除带有分号内容的(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,你不需要指示 是否要对请求路径进行解码,也不需要指示是否要出于 路径匹配的目的删除分号内容。 Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,其中,我们 也是recommend远离 对它的依赖。 |
---|
# 1.11.10.WebSocketService
WebFlux 爪哇 Config 声明了一个WebSocketHandlerAdapter
Bean,它为 WebSocket 处理程序的调用提供了支持。这意味着,要处理 WebSocket 握手请求,仅需通过SimpleUrlHandlerMapping
将WebSocketHandler
映射到一个 URL。
在某些情况下,可能需要创建带有所提供的WebSocketHandlerAdapter
Bean 的WebSocketService
服务,该服务允许配置 WebSocket 服务器属性。例如:
爪哇
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public WebSocketService getWebSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}
# 1.11.11.高级配置模式
@EnableWebFlux
ImportsDelegatingWebFluxConfiguration
表示:
为 WebFlux 应用程序提供默认的 Spring 配置
检测并委托
WebFluxConfigurer
实现来定制该配置。
对于高级模式,可以删除@EnableWebFlux
并直接从DelegatingWebFluxConfiguration
扩展,而不是实现WebFluxConfigurer
,如下例所示:
爪哇
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
Kotlin
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {
// ...
}
你可以在WebConfig
中保留现有的方法,但是你现在也可以重写 Bean 来自基类的声明,并且在 Classpath 上仍然具有任何数量的其他WebMvcConfigurer
实现。
# 1.12.http/2
Tomcat、 Jetty 和 Undertow 支持 HTTP/2。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见HTTP/2Wiki 页面 (opens new window)。
# 2. WebClient
Spring WebFlux 包括用于执行 HTTP 请求的客户端。WebClient
具有功能强大的、基于 Reactor 的 Fluent API,参见反应库,它使异步逻辑的声明式组合无需处理线程或并发性。它是完全非阻塞的,它支持流媒体,并且依赖同样的codecs,这些也用于在服务器端对请求和响应内容进行编码和解码。
WebClient
需要一个 HTTP 客户库来执行请求。以下是内置的支持:
其他的可以通过
ClientHttpConnector
进行插接。
# 2.1.配置
创建WebClient
的最简单方法是通过一种静态工厂方法:
WebClient.create()
WebClient.create(String baseUrl)
你还可以使用WebClient.builder()
和其他选项:
uriBuilderFactory
:定制UriBuilderFactory
用作基本 URL。defaultUriVariables
:展开 URI 模板时要使用的默认值。defaultHeader
:每个请求的标题。defaultCookie
:每个请求都有 cookies。defaultRequest
:Consumer
来定制每个请求。filter
:每个请求的客户端过滤器。exchangeStrategies
:HTTP 消息阅读器/Writer 自定义。clientConnector
:http 客户库设置。
例如:
爪哇
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build()
一旦建立,WebClient
是不变的。但是,你可以复制它并构建一个修改后的副本,如下所示:
爪哇
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
Kotlin
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
编解码器有limits用于缓冲内存中的数据,以避免应用程序内存问题。默认情况下,这些被设置为 256KB。如果这还不够,那么你将得到以下错误:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
要更改默认编解码器的限制,请使用以下方法:
爪哇
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
Kotlin
val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()
# 2.1.2.反应堆网状结构
要定制反应堆网络设置,请提供预先配置的HttpClient
:
爪哇
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin
val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
# Resources
默认情况下,HttpClient
参与在reactor.netty.http.HttpResources
中持有的全局反应器网络资源,包括事件循环线程和连接池。这是推荐的模式,因为对于事件循环并发,首选的是固定的共享资源。在这种模式下,全局资源在流程退出之前一直处于活动状态。
如果服务器与进程同步,则通常不需要显式关机。然而,如果服务器可以在进程中启动或停止(例如, Spring MVC 应用程序部署为 WAR),可以用globalResources=true
(默认)声明类型ReactorResourceFactory
的 Spring-管理的 Bean,以确保在 Spring ApplicationContext
关闭时关闭反应堆网络全局资源,如下例所示:
爪哇
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
你也可以选择不参与全球反应堆网状资源。但是,在这种模式下,要确保所有 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 。 |
Kotlin
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
isUseGlobalResources = false (1)
}
@Bean
fun webClient(): WebClient {
val mapper: (HttpClient) -> HttpClient = {
// Further customizations...
}
val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)
return WebClient.builder().clientConnector(connector).build() (3)
}
1 | 创造独立于全球资源的资源。 |
---|---|
2 | 在资源工厂中使用ReactorClientHttpConnector 构造函数。 |
3 | 将连接器插入WebClient.Builder 。 |
# 超时
要配置连接超时:
爪哇
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Kotlin
import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
要配置读或写超时:
爪哇
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...
Kotlin
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))
}
// Create WebClient...
要为所有请求配置响应超时:
爪哇
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
Kotlin
val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
要为特定请求配置响应超时:
爪哇
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
Kotlin
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest { httpRequest: ClientHttpRequest ->
val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
reactorRequest.responseTimeout(Duration.ofSeconds(2))
}
.retrieve()
.bodyToMono(String::class.java)
# 2.1.3. Jetty
下面的示例展示了如何自定义 Jetty HttpClient
设置:
Java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
Kotlin
val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
默认情况下,HttpClient
创建自己的资源(Executor
,ByteBufferPool
,Scheduler
),这些资源在进程退出或调用stop()
之前一直处于活动状态。
你可以在 Jetty 客户机(和服务器)的多个实例之间共享资源,并通过声明类型为JettyResourceFactory
的 Spring 管理的 Bean 来确保在关闭 Spring ApplicationContext
时关闭资源,如下例所示:
Java
@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 。 |
Kotlin
@Bean
fun resourceFactory() = JettyResourceFactory()
@Bean
fun webClient(): WebClient {
val httpClient = HttpClient()
// Further customizations...
val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)
return WebClient.builder().clientConnector(connector).build() (2)
}
1 | 在资源工厂中使用JettyClientHttpConnector 构造函数。 |
---|---|
2 | 将连接器插入WebClient.Builder 。 |
# 2.1.4.HttpComponents
下面的示例展示了如何定制 Apache HttpComponentsHttpClient
设置:
Java
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin
val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()
# 2.2.retrieve()
retrieve()
方法可用于声明如何提取响应。例如:
Java
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()
或者只得到身体:
Java
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
Kotlin
val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()
要获取已解码对象的流:
Java
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
Kotlin
val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>()
默认情况下,4xx 或 5xx 响应会导致WebClientResponseException
,包括用于特定 HTTP 状态代码的子类。要自定义错误响应的处理,请使用onStatus
处理程序,如下所示:
Java
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);
Kotlin
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()
# 2.3.交换
Kotlin 中的exchangeToMono()
和exchangeToFlux()
方法(或awaitExchange { }
和exchangeToFlow { }
)对于需要更多控制的更高级情况是有用的,例如根据响应状态对响应进行不同的解码:
Java
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
Kotlin
val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Person>()
}
else {
throw response.createExceptionAndAwait()
}
}
当使用上述方法时,在返回的Mono
或Flux
完成后,将检查响应体,如果没有使用它,则释放它,以防止内存和连接泄漏。因此,响应不能在更下游的地方被解码。如果需要,由提供的函数声明如何解码响应。
# 2.4.请求主体
请求主体可以从ReactiveAdapterRegistry
处理的任何异步类型进行编码,例如Mono
或 Kotlin 协程Deferred
,如下例所示:
Java
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()
还可以对对象流进行编码,如下例所示:
Java
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);
Kotlin
val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>()
或者,如果你有实际值,你可以使用bodyValue
快捷方式,如下例所示:
Java
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val person: Person = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.awaitBody<Unit>()
# 2.4.1.表单数据
要发送表单数据,可以提供MultiValueMap<String, String>
作为主体。请注意,内容由FormHttpMessageWriter
自动设置为application/x-www-form-urlencoded
。下面的示例展示了如何使用MultiValueMap<String, String>
:
Java
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
Kotlin
val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>()
还可以使用BodyInserters
在线提供表单数据,如下例所示:
Java
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);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.awaitBody<Unit>()
# 2.4.2.多部分数据
要发送多部分数据,你需要提供一个MultiValueMap<String, ?>
,其值要么是表示部分内容的Object
实例,要么是表示部分内容和标题的HttpEntity
实例。MultipartBodyBuilder
提供了一个方便的 API 来准备多部分请求。下面的示例展示了如何创建MultiValueMap<String, ?>
:
Java
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();
Kotlin
val builder = MultipartBodyBuilder().apply {
part("fieldPart", "fieldValue")
part("filePart1", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
在大多数情况下,你不必为每个部分指定Content-Type
。内容类型是根据用于序列化它的HttpMessageWriter
自动确定的,或者在Resource
的情况下,根据文件扩展名自动确定的。如果有必要,你可以通过重载的构建器part
方法之一,显式地为每个部分提供MediaType
。
一旦准备好MultiValueMap
,将其传递给WebClient
的最简单方法是通过body
方法,如下例所示:
Java
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
Kotlin
val builder: MultipartBodyBuilder = ...
client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.awaitBody<Unit>()
如果MultiValueMap
包含至少一个非String
值,该值也可以表示常规的表单数据(即application/x-www-form-urlencoded
),则无需将Content-Type
设置为multipart/form-data
。当使用MultipartBodyBuilder
时总是这样,这确保了HttpEntity
包装器。
作为MultipartBodyBuilder
的替代方案,你还可以通过内置的BodyInserters
提供内联样式的多部分内容,如下例所示:
Java
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);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*
client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.awaitBody<Unit>()
# 2.5.过滤器
你可以通过WebClient.Builder
注册一个客户端过滤器(ExchangeFilterFunction
),以便拦截和修改请求,如下例所示:
Java
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
Kotlin
val client = WebClient.builder()
.filter { request, next ->
val filtered = ClientRequest.from(request)
.header("foo", "bar")
.build()
next.exchange(filtered)
}
.build()
这可以用于跨领域的关注,例如身份验证。下面的示例使用一个过滤器通过静态工厂方法进行基本身份验证:
Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
Kotlin
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build()
可以通过更改现有的WebClient
实例来添加或删除过滤器,从而生成一个不影响原始实例的新WebClient
实例。例如:
Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
Kotlin
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
WebClient
是围绕过滤器链的一个薄的外观,然后是ExchangeFunction
。它提供了一个工作流,用于发出请求,对来自更高级别的对象进行编码,并有助于确保始终使用响应内容。当过滤器以某种方式处理响应时,必须格外小心,以始终使用其内容,或以其他方式将其向下游传播到WebClient
,这将确保相同的结果。下面是一个过滤器,它处理UNAUTHORIZED
状态代码,但确保释放任何响应内容(无论是否期望):
Java
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}
Kotlin
fun renewTokenFilter(): ExchangeFilterFunction? {
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
next.exchange(request!!).flatMap { response: ClientResponse ->
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
[email protected] response.releaseBody()
.then(renewToken())
.flatMap { token: String? ->
val newRequest = ClientRequest.from(request).build()
next.exchange(newRequest)
}
} else {
[email protected] Mono.just(response)
}
}
}
}
# 2.6.属性
你可以向请求添加属性。如果你希望通过筛选链传递信息并影响给定请求的筛选器的行为,这是很方便的。例如:
Java
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);
}
Kotlin
val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}
.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.awaitBody<Unit>()
请注意,你可以在WebClient.Builder
级别全局配置defaultRequest
回调,它允许你将属性插入到所有请求中,例如,可以在 Spring MVC 应用程序中使用它来基于ThreadLocal
数据填充请求属性。
# 2.7.上下文
Attributes提供了一种将信息传递到过滤器链的方便方式,但它们只会影响当前的请求。如果你想要传递传播到嵌套的其他请求的信息,例如通过flatMap
,或者在之后执行,例如通过concatMap
,那么你将需要使用反应器Context
。
反应器Context
需要在反应链的末端填充,以便应用于所有操作。例如:
Java
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));
# 2.8.同步使用
WebClient
可以以同步方式使用,方法是在末尾对结果进行阻塞:
Java
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();
Kotlin
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}
但是,如果需要进行多个调用,那么更有效的方法是避免单个地阻塞每个响应,而是等待合并的结果:
Java
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();
Kotlin
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())
}
以上只是一个例子。还有很多其他模式和操作员可以将反应性管道组合在一起,从而进行许多远程调用,可能是一些嵌套的、相互依赖的调用,并且直到最后都不会阻塞。
使用Flux 或Mono ,你应该永远不需要在 Spring MVC 或 Spring WebFlux 控制器中进行阻塞。只需从控制器方法返回得到的反应性类型。同样的原理也适用于 Kotlin 协程和 Spring WebFlux,只需使用悬挂函数或在你的 Flow 控制器中返回方法。 |
---|
# 2.9.测试
要测试使用WebClient
的代码,可以使用模拟 Web 服务器,例如OKHTTP MockWebServer (opens new window)。要查看它的使用示例,请查看 Spring Framework 测试套件中的[WebClientIntegrationTests
](https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-webflux/SRC/test/java/org/springframework/web/active/function/client/webclientintegrationtests.java)或[<gtr="2055"/>](https:/giaster/tsquare/kmaster/okthub/samples/static-server)存储库中的
# 3. WebSockets
参考文档的这一部分涵盖了对 Reactive-Stack WebSocket 消息传递的支持。
# 3.1. WebSocket 介绍
WebSocket 协议RFC 6455 (opens new window)提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。
WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1 | Upgrade 标头。 |
---|---|
2 | 使用Upgrade 连接。 |
具有 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 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。
关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,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),通过Header 上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。
# 3.1.2.何时使用 WebSockets
WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。
例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。正是低延迟、高频率和高音量的组合为 WebSocket 的使用提供了最佳的条件。
还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递Upgrade
头,或者是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公众的应用程序使用 WebSocket 是一个更直接的决策。
# 3.2. WebSocket API
Spring 框架提供了一个 WebSocket API,你可以使用它编写处理 WebSocket 消息的客户端和服务器端应用程序。
# 3.2.1.服务器
要创建 WebSocket 服务器,你可以首先创建WebSocketHandler
。下面的示例展示了如何做到这一点:
爪哇
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
public class MyWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
// ...
}
}
Kotlin
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}
然后你可以将它映射到一个 URL:
爪哇
@Configuration
class WebConfig {
@Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/path", new MyWebSocketHandler());
int order = -1; // before annotated controllers
return new SimpleUrlHandlerMapping(map, order);
}
}
Kotlin
@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
}
如果使用WebFlux 配置,则没有更多的事情要做,或者如果不使用 WebFlux 配置,则需要声明WebSocketHandlerAdapter
,如下所示:
爪哇
@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
Kotlin
@Configuration
class WebConfig {
// ...
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}
# 3.2.2.WebSocketHandler
handle
的WebSocketHandler
方法接受WebSocketSession
并返回Mono<Void>
,以指示会话的应用程序处理完成时。会话通过两个流处理,一个用于入站消息,另一个用于出站消息。下表描述了处理流的两种方法:
WebSocketSession method | 说明 |
---|---|
Flux<WebSocketMessage> receive() | 提供对入站消息流的访问,并在连接关闭时完成。 |
Mono<Void> send(Publisher<WebSocketMessage>) | 获取传出消息的源,写入消息,并返回一个Mono<Void> ,当源完成并写入完成时,该完成。 |
aWebSocketHandler
必须将入站和出站流组合成一个统一的流,并返回一个Mono<Void>
,该流反映了该流的完成。根据应用程序的需求,统一流在以下情况下完成:
入站消息流或出站消息流已完成。
入站流完成(即连接关闭),而出站流是无限的。
在选定的点上,通过
close
的WebSocketSession
方法。
当入站和出站消息流组合在一起时,不需要检查连接是否打开,因为反应流信号结束活动。入站流接收完成或错误信号,出站流接收取消信号。
处理程序的最基本实现是处理入站流的实现。下面的示例展示了这样的实现:
爪哇
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive() (1)
.doOnNext(message -> {
// ... (2)
})
.concatMap(message -> {
// ... (3)
})
.then(); (4)
}
}
1 | 访问入站消息流。 |
---|---|
2 | 对每条信息都做些什么。 |
3 | 执行使用消息内容的嵌套异步操作。 |
4 | 返回在接收完成时完成的Mono<Void> 。 |
Kotlin
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive() (1)
.doOnNext {
// ... (2)
}
.concatMap {
// ... (3)
}
.then() (4)
}
}
1 | 访问入站消息流。 |
---|---|
2 | 对每条信息都做些什么。 |
3 | 执行使用消息内容的嵌套异步操作。 |
4 | 返回在接收完成时完成的Mono<Void> 。 |
对于嵌套的异步操作,你可能需要在使用池数据缓冲区的底层 服务器(例如 Netty)上调用 message.retain() 。否则,数据缓冲区可能会在有机会读取数据之前释放。有关更多背景信息,请参见数据缓冲区和编解码器。 |
---|
以下实现合并了入站和出站流:
爪哇
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive() (1)
.doOnNext(message -> {
// ...
})
.concatMap(message -> {
// ...
})
.map(value -> session.textMessage("Echo " + value)); (2)
return session.send(output); (3)
}
}
1 | 处理入站消息流。 |
---|---|
2 | 创建出站消息,生成一个合并的流。 |
3 | 返回一个Mono<Void> ,它在我们继续接收时不完成。 |
Kotlin
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> ,当任一流结束时完成。 |
Kotlin
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 核心部分在数据缓冲区和编解码器一节中对此有更多的说明。要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。
在 Netty 上运行时,如果应用程序希望保留输入数据缓冲区,则必须使用DataBufferUtils.retain(dataBuffer)
,以确保它们不会被释放,然后在使用缓冲区时使用DataBufferUtils.release(dataBuffer)
。
# 3.2.4.握手
WebSocketHandlerAdapter
委托给WebSocketService
。默认情况下,这是HandshakeWebSocketService
的一个实例,它对 WebSocket 请求执行基本检查,然后对正在使用的服务器使用RequestUpgradeStrategy
。目前,有对反应堆网状物、 Tomcat、 Jetty 和 Undertow 的内置支持。
HandshakeWebSocketService
公开了一个sessionAttributePredicate
属性,该属性允许设置Predicate<String>
以从WebSession
中提取属性,并将它们插入到WebSocketSession
的属性中。
# 3.2.5.服务器配置
每个服务器的RequestUpgradeStrategy
公开了特定于底层 WebSocket 服务器引擎的配置。当使用 WebFlux 爪哇 Config 时,你可以自定义这些属性,如WebFlux 配置的相应部分所示,或者如果不使用 WebFlux Config,则使用以下方法:
爪哇
@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);
}
}
Kotlin
@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 并限制对 WebSocket 端点的访问的最简单方法是让你的WebSocketHandler
实现CorsConfigurationSource
并返回一个CorsConfiguration
,其中包含允许的源代码、标头和其他详细信息。如果无法做到这一点,还可以在SimpleUrlHandler
上设置corsConfigurations
属性,以通过 URL 模式指定 CORS 设置。如果指定了这两个参数,则使用combine
上的CorsConfiguration
方法将它们合并。
# 3.2.7.客户
Spring WebFlux 提供了一个WebSocketClient
抽象,其中包含用于反应器 Netty、 Tomcat、 Jetty、 Undertow 和标准 爪哇(即 JSR-356)的实现。
Tomcat 客户机实际上是标准 爪哇 One 的扩展,它在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());
Kotlin
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
session.receive()
.doOnNext(::println)
.then()
}
一些客户机,例如 Jetty,实现了Lifecycle
,并且需要在可以使用它们之前停止并启动它们。所有客户端都具有与底层 WebSocket 客户端的配置相关的构造函数选项。
# 4. 测试
spring-test
模块提供了ServerHttpRequest
、ServerHttpResponse
和ServerWebExchange
的模拟实现。有关模拟对象的讨论,请参见Spring Web Reactive。
[WebTestClient
](Testing.html#WebTestClient)构建在这些模拟请求和响应对象上,以提供对无需 HTTP 服务器的 WebFlux 应用程序的测试支持。对于端到端集成测试,也可以使用WebTestClient
。
# 5. RSocket
本节描述 Spring Framework 对 RSocket 协议的支持。
# 5.1.概述
WebSocket RSocket 是一种用于在 TCP、 WebSocket 和其他字节流传输上进行多路复用、双工通信的应用程序协议,它使用以下交互模型之一:
Request-Response
—发送一条消息,接收一条返回。Request-Stream
—发送一条消息并接收一系列消息。Channel
——双向发送消息流。Fire-and-Forget
—发送单向消息。
一旦建立了初始连接,“客户端”与“服务器”的区别就会丢失,因为双方变得对称,并且双方都可以启动上述交互之一。这就是为什么在协议中将参与方称为“请求者”和“响应者”,而上述交互称为“请求流”或简称“请求”。
这些是 RSocket 协议的关键特性和优点:
跨越网络边界的反应流 (opens new window)语义——对于
Request-Stream
和Channel
之类的流媒体请求,背压信号在请求者和响应者之间传输,从而允许请求者在源处减慢响应者的速度,从而减少对网络层拥塞控制的依赖,以及在网络级别或任何级别上对缓冲的需求。请求节流——这个特性在
LEASE
帧之后被命名为“租赁”,该帧可以从每一端发送,以限制另一端在给定时间内允许的请求总数。租约定期续签。会话恢复——这是为失去连接而设计的,并且需要保持某些状态。状态管理对于应用程序是透明的,并且与背压相结合工作得很好,背压可以在可能的情况下停止生产者并减少所需的状态量。
大消息的分片和重新组装。
KeepAlive(心跳)。
RSocket 在多种语言中都有实现 (opens new window)。爪哇 库 (opens new window)是建立在项目反应堆 (opens new window)上的,而反应堆网状结构 (opens new window)是用于传输的。这意味着来自应用程序中的反应流发布者的信号通过 RSocket 在整个网络中透明地传播。
# 5.1.1.《议定书》
RSocket 的优点之一是它在有线上具有定义良好的行为,以及易于读取的规格 (opens new window)以及一些协议extensions (opens new window)。因此,阅读规范是一个好主意,独立于语言实现和更高级别的框架 API。本节提供了一个简明的概述,以建立一些上下文。
连接
最初,客户端通过一些低级别的流媒体传输(例如 TCP 或 WebSocket)连接到服务器,并向服务器发送SETUP
帧,以设置连接的参数。
服务器可能会拒绝SETUP
帧,但通常在它被发送(对于客户端)和接收(对于服务器)之后,双方都可以开始进行请求,除非SETUP
表示使用租赁语义来限制请求的数量,在这种情况下,双方都必须等待来自另一端的LEASE
帧以允许进行请求。
提出要求
一旦建立了连接,双方可以通过REQUEST_RESPONSE
、REQUEST_STREAM
、REQUEST_CHANNEL
或REQUEST_FNF
中的一个帧发起请求。这些帧中的每一个都从请求者向响应者传送一条消息。
然后响应者可以返回带有响应消息的PAYLOAD
帧,并且在REQUEST_CHANNEL
的情况下,请求者还可以发送带有更多请求消息的PAYLOAD
帧。
当请求涉及诸如Request-Stream
和Channel
之类的消息流时,响应者必须尊重来自请求者的需求信号。需求被表示为大量的消息。初始需求在REQUEST_STREAM
和REQUEST_CHANNEL
框架中指定。后续的需求是通过REQUEST_N
帧来表示的。
每一方也可以通过METADATA_PUSH
帧发送元数据通知,这些通知与任何单独的请求无关,而是与整个连接有关。
消息格式
RSocket 消息包含数据和元数据。元数据可用于发送路由、安全令牌等。数据和元数据可以采用不同的格式。每个类型的 MIME 类型都在SETUP
框架中声明,并应用于给定连接上的所有请求。
虽然所有消息都可以具有元数据,但通常的元数据例如路由是每个请求的,因此仅包含在请求的第一个消息中,即具有一个框架REQUEST_RESPONSE
,REQUEST_STREAM
,REQUEST_CHANNEL
,或REQUEST_FNF
。
协议扩展定义了应用程序中使用的通用元数据格式:
复合元数据 (opens new window)--多个独立格式化的元数据条目。
Routing (opens new window)—请求的路径。
# 5.1.2.爪哇 实现
RSocket 的爪哇 实现 (opens new window)是建立在项目反应堆 (opens new window)之上的。TCP 和 WebSocket 的传输建立在反应堆网状结构 (opens new window)上。作为一种反应流库,Reactor 简化了协议的实现工作。对于应用程序来说,使用Flux
和Mono
声明运算符和透明背压支持是很自然的。
RSocket 爪哇 中的 API 有意地是最小的和基本的。它专注于协议特性,并将应用程序编程模型(例如 RPC Codegen vs Other)作为更高级别的独立关注点。
主契约io.rsocket.rsocket (opens new window)用Mono
表示对单个消息的承诺、Flux
消息流和io.rsocket.Payload
实际消息进行建模,并将对数据和元数据的访问作为字节缓冲区。RSocket
契约是对称使用的。对于请求,给应用程序一个RSocket
来执行请求。对于响应,应用程序实现RSocket
来处理请求。
这并不意味着要做一个全面的介绍。 Spring 在大多数情况下,应用程序将不必直接使用其 API。然而,独立于 Spring 来观察或实验 RSocket 可能是重要的。RSocket 爪哇 存储库包含许多示例应用程序 (opens new window),它们演示了它的 API 和协议特性。
# 5.1.3. Spring 支持
spring-messaging
模块包含以下内容:
Rsocketrequester—Fluent API 通过带有数据和元数据编码/解码的
io.rsocket.RSocket
进行请求。附加注释的响应者—
@MessageMapping
用于响应的注释处理程序方法。
spring-web
模块包含Encoder
和Decoder
实现,例如 Jackson 的 cbor/json,以及 RSocket 应用程序可能需要的 Protobuf。它还包含PathPatternParser
,可以插入该参数以进行有效的路由匹配。
Spring Boot2.2 支持在 TCP 或 WebSocket 上站立 RSocket 服务器,包括在 WebFlux 服务器中公开 RSocket over WebSocket 的选项。对于RSocketRequester.Builder
和RSocket战略
也有客户机支持和自动配置。有关更多详细信息,请参见 Spring 引导引用中的RSocket 部分 (opens new window)。
Spring 安全性 5.2 提供了 RSocket 支持。
Spring 集成 5.2 提供了入站和出站网关,以与 RSocket 客户端和服务器进行交互。有关更多详细信息,请参见 Spring 集成参考手册。
Spring 云网关支持 RSocket 连接。
# 5.2.Rsocketrequester
RSocketRequester
提供了一个 Fluent API 来执行 RSocket 请求,接受并返回数据和元数据的对象,而不是低级别的数据缓冲区。它可以被对称地使用,用于从客户机发出请求和从服务器发出请求。
# 5.2.1.客户请求者
要在客户端获得RSocketRequester
,就需要连接到一个服务器,该服务器需要发送一个带有连接设置的 RSocketSETUP
帧。RSocketRequester
提供了一个构建器,该构建器帮助准备一个io.rsocket.core.RSocketConnector
,包括SETUP
框架的连接设置。
这是连接默认设置的最基本方式:
爪哇
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);
URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);
Kotlin
val requester = RSocketRequester.builder().tcp("localhost", 7000)
URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)
上面的连接不是立即的。当提出请求时,将透明地建立并使用共享连接。
# 连接设置
RSocketRequester.Builder
提供了以下自定义初始化SETUP
框架的方法:
dataMimeType(MimeType)
—为连接上的数据设置 MIME 类型。metadataMimeType(MimeType)
—为连接上的元数据设置 MIME 类型。setupData(Object)
—要包含在SETUP
中的数据。setupRoute(String, Object…)
—将元数据中的路由包含在SETUP
中。setupMetadata(Object, MimeType)
—要包含在SETUP
中的其他元数据。
对于数据,默认的 MIME 类型是从第一个配置的Decoder
派生的。对于元数据,默认的 MIME 类型是复合元数据 (opens new window),它允许每个请求有多个元数据值和 MIME 类型对。通常情况下,这两种情况都不需要改变。
SETUP
框架中的数据和元数据是可选的。在服务器端,@ConnectMapping方法可用于处理连接的开始和SETUP
框架的内容。元数据可用于连接级别的安全性。
# Strategies
RSocketRequester.Builder
接受RSocketStrategies
来配置请求者。你将需要使用它来为数据和元数据值的(反)序列化提供编码器和解码器。默认情况下,只注册来自spring-core
的String
、byte[]
和ByteBuffer
的基本编解码器。添加spring-web
可以访问更多可以按以下方式注册的内容:
爪哇
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
RSocketRequester requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000);
Kotlin
val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000)
RSocketStrategies
是为重复使用而设计的。在一些场景中,例如客户端和服务器在相同的应用程序中,可以优选地在 Spring 配置中对其进行声明。
# 客户响应者
RSocketRequester.Builder
可用于配置来自服务器的请求的响应程序。
你可以使用带注释的处理程序进行客户端响应,该处理程序基于服务器上使用的相同基础设施,但以编程方式注册,如下所示:
爪哇
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();
SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.tcp("localhost", 7000);
1 | 如果存在spring-web ,则使用PathPatternRouteMatcher ,以进行有效的路由匹配。 |
---|---|
2 | 从具有@MessageMaping 和/或@ConnectMapping 方法的类创建响应器。 |
3 | 登记响应者。 |
Kotlin
val strategies = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher()) (1)
.build()
val responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(responder) } (3)
.tcp("localhost", 7000)
1 | 如果存在spring-web ,则使用PathPatternRouteMatcher ,以进行有效的路由匹配。 |
---|---|
2 | 从具有@MessageMaping 和/或@ConnectMapping 方法的类创建响应器。 |
3 | 登记响应者。 |
注意,上面只是为客户端响应者的程序化注册设计的一个快捷方式。对于客户机响应者处于 Spring 配置中的替代场景,你仍然可以将RSocketMessageHandler
声明为 Spring Bean,然后按以下方式应用:
爪哇
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.tcp("localhost", 7000);
Kotlin
import org.springframework.beans.factory.getBean
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.tcp("localhost", 7000)
对于上述情况,还可能需要使用setHandlerPredicate
中的RSocketMessageHandler
来切换到用于检测客户端响应者的不同策略,例如基于诸如@RSocketClientResponder
VS 的默认@Controller
的自定义注释。在使用客户机和服务器,或者在同一个应用程序中有多个客户机的情况下,这是必要的。
有关编程模型的更多信息,请参见附加注释的响应者。
# 高级
RSocketRequesterBuilder
提供了一个回调,以公开底层io.rsocket.core.RSocketConnector
的更多配置选项,用于保持活动间隔、会话恢复、拦截器等。你可以按以下方式配置该级别的选项:
爪哇
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.tcp("localhost", 7000);
Kotlin
val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}
.tcp("localhost", 7000)
# 5.2.2.服务器请求者
要从服务器向连接的客户机发出请求,需要从服务器获得连接的客户机的请求者。
在附加注释的响应者中,@ConnectMapping
和@MessageMapping
方法支持RSocketRequester
参数。使用它来访问连接的请求者。请记住,@ConnectMapping
方法本质上是SETUP
框架的处理程序,在开始请求之前必须对其进行处理。因此,从一开始就必须将请求与处理分离开来。例如:
爪哇
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { (1)
// ...
});
return ... (2)
}
1 | 异步启动请求,与处理无关。 |
---|---|
2 | 执行处理并返回完成Mono<Void> 。 |
Kotlin
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
// ...
}
}
/// ... (2)
}
1 | 异步启动请求,与处理无关。 |
---|---|
2 | 在挂起功能中执行处理。 |
# 5.2.3.请求
一旦有了client或server请求者,你可以按以下方式进行请求:
爪哇
ViewBox viewBox = ... ;
Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlux(AirportLocation.class); (3)
1 | 指定要包含在请求消息的元数据中的路由。 |
---|---|
2 | 为请求消息提供数据。 |
3 | 声明预期的响应。 |
Kotlin
val viewBox: ViewBox = ...
val locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlow<AirportLocation>() (3)
1 | 指定要包含在请求消息的元数据中的路由。 |
---|---|
2 | 为请求消息提供数据。 |
3 | 声明预期的响应。 |
交互类型是由输入和输出的基数隐式确定的。上面的示例是Request-Stream
,因为发送了一个值并接收了一个值流。在大多数情况下,只要输入和输出的选择与 RSocket 交互类型以及响应者期望的输入和输出类型相匹配,就不需要考虑这个问题。无效组合的唯一示例是多对一。
data(Object)
方法还接受任何活性流Publisher
,包括Flux
和Mono
,以及在ReactiveAdapterRegistry
中注册的任何其他值的生成器。对于产生相同类型的值的多值Publisher
,例如Flux
,可以考虑使用重载的data
方法之一,以避免对每个元素进行类型检查和Encoder
查找:
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
data(Object)
步骤是可选的。对于不发送数据的请求,跳过它:
爪哇
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
.retrieveMono(AirportLocation.class);
Kotlin
import org.springframework.messaging.rsocket.retrieveAndAwait
val location = requester.route("find.radar.EWR")
.retrieveAndAwait<AirportLocation>()
如果使用复合元数据 (opens new window)(默认值),并且如果已注册的Encoder
支持这些值,则可以添加额外的元数据值。例如:
Java
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);
Kotlin
import org.springframework.messaging.rsocket.retrieveFlow
val requester: RSocketRequester = ...
val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")
val locations = requester.route("locate.radars.within")
.metadata(securityToken, mimeType)
.data(viewBox)
.retrieveFlow<AirportLocation>()
对于Fire-and-Forget
,使用send()
方法,返回Mono<Void>
。请注意,Mono
仅表示消息已成功发送,而不表示消息已被处理。
对于Metadata-Push
,使用带有sendMetadata()
返回值的Mono<Void>
方法。
# 5.3.附加注释的响应者
RSocket 响应器可以实现为@MessageMapping
和@ConnectMapping
方法。@MessageMapping
方法处理单个请求,而@ConnectMapping
方法处理连接级事件(设置和元数据推送)。带注释的响应器是对称支持的,用于从服务器端响应和从客户端响应。
# 5.3.1.服务器响应者
要在服务器端使用带注释的响应器,将RSocketMessageHandler
添加到你的 Spring 配置中,以检测@Controller
带有@MessageMapping
和@ConnectMapping
的 bean 方法:
Java
@Configuration
static class ServerConfig {
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.routeMatcher(new PathPatternRouteMatcher());
return handler;
}
}
Kotlin
@Configuration
class ServerConfig {
@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
routeMatcher = PathPatternRouteMatcher()
}
}
然后通过 Java RSocket API 启动一个 RSocket 服务器,并为响应者插入RSocketMessageHandler
,如下所示:
Java
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
CloseableChannel server =
RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.block();
Kotlin
import org.springframework.beans.factory.getBean
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val server = RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.awaitSingle()
RSocketMessageHandler
默认支持composite (opens new window)和routing (opens new window)元数据。如果需要切换到不同的 MIME 类型或注册其他元数据 MIME 类型,则可以设置其MetadataExtractor。
你需要设置元数据和数据格式所需支持的Encoder
和Decoder
实例。你可能需要spring-web
模块来实现编解码。
默认情况下,SimpleRouteMatcher
用于通过AntPathMatcher
匹配路由。我们建议插入spring-web
中的PathPatternRouteMatcher
以进行有效的路线匹配。RSocket 路由可以是分层的,但不是 URL 路径。这两个路由匹配器都被配置为默认使用“.”作为分隔符,并且没有像 HTTP URL 那样的 URL 解码。
RSocketMessageHandler
可以通过RSocketStrategies
进行配置,如果你需要在同一进程中在客户机和服务器之间共享配置,这可能会很有用:
Java
@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();
}
}
Kotlin
@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
一旦server或client响应者配置到位,@MessageMapping
方法可按以下方式使用:
Java
@Controller
public class RadarsController {
@MessageMapping("locate.radars.within")
public Flux<AirportLocation> radars(MapRequest request) {
// ...
}
}
Kotlin
@Controller
class RadarsController {
@MessageMapping("locate.radars.within")
fun radars(request: MapRequest): Flow<AirportLocation> {
// ...
}
}
上面的@MessageMapping
方法响应具有“locate.radars.within”路由的请求-流交互。它支持灵活的方法签名,可以选择使用以下方法参数:
Method Argument | 说明 |
---|---|
@Payload | 请求的有效载荷。这可以是异步类型的一个具体值,如Mono 或Flux 。** 注:** 注释的使用是可选的。一个方法参数不是简单的类型 ,也不是任何其他受支持的参数,假定它是预期的有效负载。 |
RSocketRequester | 向远端发出请求的请求者。 |
@DestinationVariable | 基于映射模式中的变量从路由中提取的值,例如@MessageMapping("find.radar.{id}") 。 |
@Header | 按照MetadataExtractor中所述,为提取而注册的元数据值。 |
@Headers Map<String, Object> | 按照MetadataExtractor中所述,为提取而注册的所有元数据值。 |
返回值应该是一个或多个要序列化为响应有效负载的对象。这可以是异步类型,如Mono
或Flux
,具体值,或void
或无值异步类型,如Mono<Void>
。
@MessageMapping
方法支持的 RSocket 交互类型是由输入的基数(即@Payload
参数)的输出,其中基数表示如下:
Cardinality | 说明 |
---|---|
1 | 要么是显式的值,要么是单值异步类型,如Mono<T> 。 |
Many | 一种多值异步类型,如Flux<T> 。 |
0 | 对于输入,这意味着该方法没有@Payload 参数。对于输出,这是,这是 void 或无值异步类型,例如Mono<Void> 。 |
下表显示了所有输入和输出基数组合以及相应的交互类型:
Input Cardinality | Output Cardinality | 交互类型 |
---|---|---|
0, 1 | 0 | 先开火后遗忘、请求-响应 |
0, 1 | 1 | 请求-响应 |
0, 1 | Many | 请求流 |
Many | 0, 1, Many | 请求通道 |
# 5.3.4.@ConnectMapping
@ConnectMapping
处理 RSocket 连接开始时的SETUP
框架,以及通过METADATA_PUSH
框架的任何后续元数据推送通知,即metadataPush(Payload)
中的io.rsocket.RSocket
。
@ConnectMapping
方法支持与@MessageMapping相同的参数,但基于来自SETUP
和METADATA_PUSH
框架的元数据和数据。@ConnectMapping
可以使用一种模式,将处理范围缩小到在元数据中具有路由的特定连接,或者如果没有声明任何模式,则所有连接都匹配。
@ConnectMapping
方法不能返回数据,必须以void
或Mono<Void>
作为返回值进行声明。如果处理返回一个新连接的错误,那么该连接将被拒绝。在向RSocketRequester
请求连接时,不能停止处理。详见服务器请求者。
# 5.4.MetadataExtractor
响应者必须解释元数据。复合元数据 (opens new window)允许独立格式化的元数据值(例如用于路由、安全性、跟踪),每个值都具有自己的 MIME 类型。应用程序需要一种方法来配置元数据来支持 MIME 类型,以及一种方法来访问提取的值。
MetadataExtractor
是一种契约,用于获取序列化的元数据并返回经过解码的名称-值对,然后可以通过名称像头一样访问这些对,例如通过注释处理程序方法中的@Header
。
DefaultMetadataExtractor
可以给出Decoder
实例来解码元数据。开箱即用,它内置了对“message/x.rsocket.routing.v0” (opens new window)的支持,并将其解码为String
,并保存在“route”键下。对于任何其他 MIME 类型,你需要提供Decoder
,并按照以下方式注册 MIME 类型:
Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
复合元数据可以很好地组合独立的元数据值。但是,请求者可能不支持复合元数据,或者可能选择不使用它。为此,DefaultMetadataExtractor
可能需要自定义逻辑来将解码后的值映射到输出映射。下面是一个使用 JSON 进行元数据处理的示例:
Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
MimeType.valueOf("application/vnd.myapp.metadata+json"),
new ParameterizedTypeReference<Map<String,String>>() {},
(jsonMap, outputMap) -> {
outputMap.putAll(jsonMap);
});
Kotlin
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
使用已配置的解码器创建提取器,并只需使用回调来定制注册,如下所示:
Java
RSocketStrategies strategies = RSocketStrategies.builder()
.metadataExtractorRegistry(registry -> {
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
// ...
})
.build();
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract
val strategies = RSocketStrategies.builder()
.metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
registry.metadataToExtract<Foo>(fooMimeType, "foo")
// ...
}
.build()
# 6. 反应库
spring-webflux
依赖于reactor-core
,并在内部使用它来组成异步逻辑并提供反应流支持。通常,WebFlux API 返回Flux
或Mono
(因为这些 API 是内部使用的),并宽容地接受任何反应流Publisher
实现作为输入。使用Flux
对Mono
是很重要的,因为它有助于表示基数——例如,预期是单个还是多个异步值,这对于做出决策(例如,在编码或解码 HTTP 消息时)是必不可少的。
对于带注释的控制器,WebFlux 透明地适应应用程序选择的反应库。这是在[ReactiveAdapterRegistry
](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/reactiveadapterregistry.html)的帮助下完成的。注册中心内置了对 RXJava3、 Kotlin 协程和 SmallRye Mutiny 的支持,但你也可以注册其他第三方适配器。
在 Spring Framework5.3.11 中,对 RXJava1 和 2 的支持是不受欢迎的,下面是 RXJava 自己的 EOL 建议和对 RXJava3 的升级建议。 |
---|
对于功能 API(例如功能端点、WebClient
和其他),WebFlux API 的一般规则适用于-Flux
和Mono
作为返回值,并将反应流Publisher
作为输入。当Publisher
(无论是自定义的还是来自另一个反应库)被提供时,它只能被视为具有未知语义(0..n)的流。但是,如果语义是已知的,则可以用Flux
或Mono.from(Publisher)
来包装它,而不是传递 RAWPublisher
。
例如,给定一个不是Mono
的Publisher
,JacksonJSON 消息编写器需要多个值。如果媒体类型意味着一个无限的流(例如,application/json+stream
),则值是单独写入和刷新的。否则,值将被缓冲到列表中,并呈现为 JSON 数组。
← Servlet 堆栈上的 Web 整合 →