0%

SpringFramework官方文档翻译-Web on Reactive Stack

Web on Reactive Stack

这一部分文档介绍了对构建在响应流API上的可在非阻塞服务器(如Netty、Undertow和Servlet 3.1+容器)上运行的反应堆栈web应用程序的支持。个别章节涵盖了Spring WebFlux框架、响应式web客户端、对测试的支持以及响应式库。有关Servlet堆栈web应用程序,请参阅Servlet堆栈上的web

SpringWebFlux

Spring框架中包含的原始web框架Spring web MVC是专门为Servlet API和Servlet容器构建的。随后在5.0版本中添加了响应堆栈web框架Spring WebFlux。它是完全无阻塞的,支持响应流回压,并在Netty、Undertow和Servlet 3.1+容器上运行。

这两个web框架反映了它们的源模块的名称(Spring-webmvcSpring-webflux),并在Spring框架中共存。每个模块都是可选的。应用程序可以使用其中一个或另一个模块,或者在某些情况下同时使用这两个模块——例如,Spring MVC控制器和响应式WebClient。

概述

为什么创建 Spring WebFlux?

部分原因是需要使用非阻塞web堆栈来处理少量线程的并发性,并使用更少的硬件资源进行伸缩。Servlet 3.1确实为非阻塞I/O提供了一个API。但是,使用它将远离Servlet API的其他部分,其中契约是同步的(过滤器、Servlet)或阻塞的(getParameter、getPart)。这就是新的公共API作为跨任何非阻塞运行时的基础的动机。这一点很重要,因为服务器(比如Netty)已经在异步、非阻塞空间中建立了良好的功能。

答案的另一部分是函数式编程。与Java 5中添加注释创造机会(如带注释的REST控制器或单元测试)一样,Java 8中添加lambda表达式为Java中的函数api创造机会。这对于允许异步逻辑声明式组合的非阻塞应用程序和延续风格api(由CompletableFuture和ReactiveX推广)来说是一个福音。在编程模型级别,Java 8支持Spring WebFlux在带注释的控制器之外提供功能性web端点。

定义“Reactive”

我们触及“non-blocking”和“功能性”但是 reactive 是什么意思?

术语“reactive”指的是围绕对变化做出反应的编程模型 - 对 I/O events 做出反应的网络组件,对鼠标 events 做出反应的 UI 控制器等。从这个意义上讲,non-blocking 是 reactive,因为我们现在处于一种模式,即在操作完成或数据可用时对通知作出反应。

我们在 Spring 团队中还有另一个重要的机制是“reactive”,这就是 non-blocking 背压。在同步,命令式 code 中,阻塞 calls 是一种自然形式的背压,迫使调用者等待。在 non-blocking code 中,控制 events 的速率变得很重要,这样快速的 producer 就不会压倒它的目的地。

Reactive Streams 是一个小规格(在 Java 9 中也是采用),用于定义具有背压的异步组件之间的交互。对于 example,data repository(充当出版者)可以生成 HTTP 服务器(充当订阅)然后可以写入响应的数据。 Reactive Streams 的主要目的是让订阅者控制发布者生成数据的速度或速度。

常见问题:如果发布者不能放慢速度怎么办? Reactive Streams 的目的只是建立机制和边界。如果发布者不能减速,则必须决定是缓冲,丢弃还是失败。

ReactiveAPI

Reactive Streams 在互操作性方面发挥着重要作用。它对 libraries 和基础架构组件很感兴趣,但作为 application API 不太有用,因为它太 low-level。 Applications 需要一个 higher-level 和更丰富的功能 API 来组成异步逻辑 - 类似于 Java 8 Stream API,但不仅仅是集合。这是 reactive libraries 扮演的角色。

Reactor是 Spring WebFlux 的 reactive library 选择。它提供助焊剂 API 类型,通过与 ReactiveX operators 的词汇对齐的一组丰富的 operators 来处理 0..1(Mono)和 0..N(Flux)的数据序列。 Reactor 是 Reactive Streams library,因此,它的所有 operators 都支持 non-blocking 背压。 Reactor 非常关注 server-side Java。它是与 Spring 密切合作开发的。

WebFlux 要求 Reactor 作为核心依赖项,但它可以通过 Reactive Streams 与其他 reactive libraries 互操作。作为一般规则,WebFlux API 接受普通Publisher作为输入,在内部使其适应 Reactor 类型,使用它,并返回FluxMono作为输出。因此,您可以传递任何Publisher作为输入,并且可以对输出应用操作,但是您需要调整输出以与另一个 reactive library 一起使用。只要可行(对于 example,带注释的控制器),WebFlux 就会透明地适应 RxJava 或其他 reactive library 的使用。有关详细信息,请参阅Reactive Libraries

编程模型

spring-web模块包含作为 Spring WebFlux 基础的 reactive 基础,包括 HTTP 抽象,Reactive Streams 适配器用于支持的服务器,编解码器,以及与 Servlet API 相当但具有 non-blocking contracts 的核WebHandler API

在此基础上,Spring WebFlux 提供了两种编程模型的选择:

  • 带注解的控制器:与 Spring MVC 一致,并基于spring-web模块的相同注解。 Spring MVC 和 WebFlux 控制器都支持 reactive(Reactor 和 RxJava)return 类型,因此,要区分它们并不容易。一个值得注意的区别是 WebFlux 还支持 reactive @RequestBody arguments。
  • 功能 Endpoints:Lambda-based,轻量级和函数式编程 model。您可以将此视为一个小的 library 或一组实用程序,application 可以用它来路由和处理请求。与带注释的控制器的最大区别在于,application 负责从头到尾的请求处理,而不是通过 annotations 声明意图并被回调。

适用性

Spring MVC 或 WebFlux?

这是一个自然而然的问题,但却是一个不合理的二分法。实际上,两者共同努力扩大可用选项的范围。两者的设计是为了保持连续性和一致性,它们可以并排使用,每一方的反馈都有利于双方。下图显示了两者之间的关系,它们在 common 中的含义以及每个对象的唯一性:

spring mvc 和 webflux venn

我们建议您考虑以下具体要点:

  • 如果你有一个工作正常的 Spring MVC application,则无需更改。命令式编程是编写,理解和调试 code 的最简单方法。您有 libraries 的最大选择,因为从历史上看,大多数都是阻塞的。
  • 如果您已经在购买 non-blocking web 堆栈,Spring WebFlux 提供与此空间中的其他人相同的执行 model 优势,并且还提供服务器选择(Netty,Tomcat,Jetty,Undertow 和 Servlet 3.1 容器),这是一种编程选择 models(带注解的控制器和函数 web endpoints),以及 reactive libraries(Reactor,RxJava 或其他)的选择。
  • 如果您对使用 Java 8 lambdas 或 Kotlin 的轻量级,功能 web framework 感兴趣,可以使用 Spring WebFlux 功能 web endpoints。对于较小的应用程序或微服务而言,这也是一个不错的选择,其需求较少,可以从更高的透明度和控制中受益。
  • 在微服务架构中,您可以将 applications 与 Spring MVC 或 Spring WebFlux 控制器或 Spring WebFlux 功能 endpoints 混合使用。在两个框架中支持相同的基于注解编程 model 使得更容易 re-use 知识,同时也为正确的 job 选择正确的工具。
  • 评估 application 的一种简单方法是检查其依赖关系。如果您要使用阻塞持久性 API(JPA,JDBC)或网络 API,则 Spring MVC 至少是 common 体系结构的最佳选择。 Reactor 和 RxJava 在单独的线程上执行阻塞 calls 在技术上是可行的,但你不会充分利用 non-blocking web 堆栈。
  • 如果你有 Spring MVC application 与 calls 到 remote 服务,请尝试 reactive WebClient。您可以直接从 Spring MVC 控制器方法 return reactive 类型(Reactor,RxJava,或其他)。每次呼叫的延迟或 calls 之间的相互依赖性越大,其益处就越大。 Spring MVC 控制器也可以调用其他 reactive 组件。
  • 如果你有一个庞大的团队,请记住转向 non-blocking,功能和声明性编程时的陡峭学习曲线。在没有完整切换的情况下启动的实用方法是使用 reactive WebClient。除此之外,从小处着手衡量效益。我们希望,对于广泛的应用,这种转变是不必要的。如果您不确定要查找哪些好处,请首先了解 non-blocking I/O 如何工作(对于示例,single-threaded Node.js 上的并发)及其效果。

服务器

Tomcat,Jetty,Servlet 3.1 容器以及_Net和 Undertow 等 non-Servlet 运行时支持 Spring WebFlux。所有服务器都适用于 low-level,common API,以便跨服务器支持 higher-level 编程模型

Spring WebFlux 没有启动或停止服务器的 built-in 支持。但是,很容易从 Spring configuration 中应用程序,和运行以及 code 的几行 lines。

Spring Boot 有一个自动执行这些步骤的 WebFlux starter。默认情况下,starter 使用 Netty,但通过更改 Maven 或 Gradle 依赖项可以轻松切换到 Tomcat,Jetty 或 Undertow。 Spring Boot 默认为 Netty,因为它在异步 non-blocking 空间中使用得更广泛,让 client 和服务器共享资源。

Tomcat 和 Jetty 可以与 Spring MVC 和 WebFlux 一起使用。但请记住,它们的使用方式非常不同。 Spring MVC 依赖于 Servlet 阻塞 I/O,并允许 applications 在需要时直接使用 Servlet API。 Spring WebFlux 依赖 Servlet 3.1 non-blocking I/O 并在 low-level 适配器后面使用 Servlet API,不会公开直接使用。

对于 Undertow,Spring WebFlux 直接使用 Undertow API 而不使用 Servlet API。

性能

Performance 具有许多特征和含义。 Reactive 和 non-blocking 通常不会使 applications run 更快。在某些情况下,它们可以(例如,如果使用WebClient在 parallel 中执行 remote calls)。总的来说,non-blocking 方式需要做更多的工作,并且可以稍微增加所需的处理时间。

reactive 和 non-blocking 的 key 预期好处是能够使用少量固定数量的线程进行扩展,而不是 memory。这使得 applications 在负载下更具弹性,因为它们以更可预测的方式扩展。但是,为了观察这些好处,您需要有一些延迟(包括慢速和不可预测的网络混合 I/O)。这就是 reactive 堆栈开始显示其优势的地方,差异可能是戏剧性的。

并发Model

Spring MVC 和 Spring WebFlux 都支持带注释的控制器,但并发 model 和阻塞和线程的默认假设存在 key 差异。

在 Spring MVC(和 servlet applications)中,假设 applications 可以阻塞当前线程(例如,remote calls),因此,servlet 容器使用大型线程池来吸收请求期间的潜在阻塞处理。

在 Spring WebFlux(以及一般的 non-blocking 服务器)中,假设 applications 不会阻塞,因此 non-blocking 服务器使用一个小的 fixed-size 线程池(event loop workers)来处理请求。

“缩放”和“少量线程”听起来可能相互矛盾,但永远不会阻塞当前线程(并依赖于回调)意味着您不需要额外的线程,因为没有阻塞 calls 吸收。

调用阻止 API

如果你确实需要使用阻止 library 怎么办? Reactor 和 RxJava 都提供publishOn operator 来继续处理不同的线程。这意味着有一个简单的逃生舱口。但请记住,阻塞 API 不适合这种并发 model。

可变 State

在 Reactor 和 RxJava 中,您通过 operators 声明逻辑,并且在运行时,形成 reactive 管道,其中数据在不同阶段按顺序处理。这样做的好处是它可以使 applications 免于必须保护 mutable state,因为该管道中的 application code 永远不会同时被调用。

Threading Model

您希望在服务器上看到哪些线程运行 Spring WebFlux?

  • 在“vanilla”Spring WebFlux 服务器上(例如,没有数据访问或其他可选依赖项),您可以期望服务器有一个线程,而其他几个用于请求处理(通常与 CPU 核心数一样多)。但是,Servlet 容器可能以更多线程开始(对于示例,Tomcat 上为 10),以支持 servlet(阻塞)I/O 和 servlet 3.1(non-blocking)I/O 用法。
  • reactive WebClient以 event 循环样式运行。因此,您可以看到与此相关的少量固定数量的处理线程(对于 example,reactor-http-nio-与 Reactor Netty 连接器)。但是,如果 Reactor Netty 同时用于 client 和 server,则默认情况下两者共享 event 循环资源。
  • Reactor 和 RxJava 提供线程池抽象,称为调度程序,与publishOn operator 一起使用,用于将处理切换到不同的线程池。调度程序具有建议特定并发策略的名称 - 例如,“ parallel”(对于 CPU-bound 使用有限数量的线程工作)或“elastic”(对于具有大量线程的 I/O-bound 工作)。如果你看到这样的线程,这意味着一些 code 正在使用特定的线程池Scheduler策略。
  • 数据访问 libraries 和其他第三方依赖项也可以创建和使用自己的线程。

配置

Spring Framework 不支持启动和停止服务器。要为服务器配置 threading model,需要使用 server-specific configuration API,或者,如果使用 Spring Boot,请检查每个服务器的 Spring Boot configuration 选项。你可以直接配置 WebClient。对于所有其他 libraries,请参阅其各自的文档。

Reactive核心

spring-web模块包含以下对 reactive web applications 的基础支持:

  • 对于服务器请求处理,有两个级别的支持。
  • HttpHandler:用于 HTTP 请求处理的基本 contract non-blocking I/O 和 Reactive Streams 背压,以及 Reactor Netty,Undertow,Tomcat,Jetty 和任何 Servlet 3.1 容器的适配器。
  • WebHandler API:稍高的 level,general-purpose web API 用于请求处理,在此基础上构建了具体的编程模型,如带注释的控制器和功能 endpoints。
  • 对于 client 方面,有一个基本的ClientHttpConnector contract 来执行带有 non-blocking I/O 和 Reactive Streams 背压的 HTTP 请求,以及反应堆 Netty和 reactive Jetty HtpClient的适配器。 applications 中使用的较高 level Web 客户端建立在这个基本的 contract 上。
  • 对于 client 和 server,编解码器用于序列化和反序列化 HTTP 请求和响应内容。

HttpHandler

HttpHandler是一个简单的 contract,它有一个方法来处理请求和响应。它是故意最小的,它的主要目的是成为不同 HTTP 服务器 API 的最小抽象。

以下 table 描述了支持的服务器 API:

服务器名称 使用的服务器 API Reactive Streams 支持
Netty Netty API 反应堆 Netty
Undertow Undertow API spring-web:Undertow to Reactive Streams bridge
Tomcat Servlet 3.1 non-blocking I/O; Tomcat API 读写 ByteBuffers vs byte [98] spring-web:Servlet 3.1 non-blocking I/O 到 Reactive Streams bridge
码头 Servlet 3.1 non-blocking I/O; Jetty API 写 ByteBuffers vs byte [99] spring-web:Servlet 3.1 non-blocking I/O 到 Reactive Streams bridge
Servlet 3.1 容器 Servlet 3.1 non-blocking I/O spring-web:Servlet 3.1 non-blocking I/O 到 Reactive Streams bridge

以下 table 描述了服务器依赖性(另请参见支持的版本):

服务器名称 Group id Artifact name
反应堆 Netty io.projectreactor.netty reactor-netty
Undertow io.undertow undertow-core
Tomcat org.apache.tomcat.embed tomcat-embed-core
码头 org.eclipse.jetty jetty-server,jetty-servlet

下面的 code 片段显示了对每个服务器 API 使用HttpHandler适配器:

Reactor Netty

1
2
3
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create(host, port).newHandler(adapter).block();

Undertow

1
2
3
4
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

Tomcat

1
2
3
4
5
6
7
8
9
10
11
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();

Jetty

1
2
3
4
5
6
7
8
9
10
11
12
13
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();

Servlet 3.1 容器

要将 WAR 部署到任何 Servlet 3.1 容器,可以在 WAR 中扩展并包含AbstractReactiveWebInitializer。 class 用ServletHttpHandlerAdapter包装HttpHandler并将其注册为Servlet

WebHandlerAPI

org.springframework.web.server包构建在HttpHandler contract 上,以提供 general-purpose web API,用于通过多个WebExceptionHandler,多个网页过滤和一个WebHandler component 链处理请求。链可以与WebHttpHandlerBuilder放在一起,只需指向 Spring ApplicationContext,其中组件是auto-detected,and/or,通过向构建器注册组件。

虽然HttpHandler有一个简单的目标来抽象使用不同的 HTTP 服务器,但WebHandler API 旨在提供 web applications 中常用的更广泛的 features 集合,例如:

  • 用户 session 属性。
  • 请求属性。
  • 已解决请求的LocalePrincipal
  • 访问已分析和缓存的表单数据。
  • multipart 数据的抽象。
  • 和更多..

特殊的bean类型

table 下面的 table 列出了WebHttpHandlerBuilder可以在 Spring ApplicationContext 中的组件,或者可以直接用它注册的组件:

Bean name Bean 类型 计数 描述
WebExceptionHandler 0..N WebFilter实例链和目标WebHandler提供 exceptions 的处理。有关更多详细信息,请参阅Exceptions
WebFilter 0..N 将拦截样式逻辑应用于过滤器链的 rest 和目标WebHandler之前和之后。有关更多详细信息,请参阅过滤器
webHandler WebHandler 1 请求的处理程序。
webSessionManager WebSessionManager 0..1 通过ServerWebExchange上的方法公开的WebSession实例的 manager。 DefaultWebSessionManager默认情况下。
serverCodecConfigurer ServerCodecConfigurer 0..1 用于访问HttpMessageReader实例以解析表单数据和 multipart 数据,然后通过ServerWebExchange上的方法公开。 ServerCodecConfigurer.create()默认情况下。
localeContextResolver LocaleContextResolver 0..1 通过ServerWebExchange上的方法暴露LocaleContext的解析器。 AcceptHeaderLocaleContextResolver默认情况下。
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 用于处理转发类型 headers,可以通过提取和删除它们或仅删除它们。默认情况下不使用。

WebHandlerAPI表单数据

ServerWebExchange公开以下方法来访问表单数据:

1
Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchange使用配置的HttpMessageReader将表单数据(application/x-www-form-urlencoded)解析为MultiValueMap。默认情况下,FormHttpMessageReader配置为由ServerCodecConfigurer bean 使用(请参阅Web Handler API)。

Multipart数据

与 Spring MVC 相同

ServerWebExchange公开以下方法来访问 multipart 数据:

1
Mono<MultiValueMap<String, Part>> getMultipartData();

DefaultServerWebExchange使用配置的HttpMessageReader<MultiValueMap<String, Part>>multipart/form-data内容解析为MultiValueMap。目前,Synchronoss NIO Multipart是唯一受支持的 third-party library,也是我们知道的_解析 multipart 请求的唯一 library。它通过ServerCodecConfigurer bean 启用(参见Web Handler API)。

要以流方式解析 multipart 数据,您可以使用从HttpMessageReader<Part>返回的Flux<Part>。例如,在带注释的控制器中,使用@RequestPart意味着按 name 对@相比之下,您可以使用@RequestBody将内容解码为Flux<Part>而无需收集到MultiValueMap

转发Headers

与 Spring MVC 相同

当请求通过代理(例如负载平衡器)时,host, port 和 scheme 可能会发生变化,从 client 的角度来看,这使得创建指向正确的 host,port 和 scheme 的链接成为一种挑战。

RFC 7239定义了代理可用于提供有关原始请求的信息的Forwarded HTTP 标头。还有其他 non-standard headers,包括X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

ForwardedHeaderTransformer是一个 component,它根据转发的 headers 修改请求的 host,port 和 scheme,然后删除那些 headers。您可以将其声明为

转发的 headers 有安全注意事项,因为 application 无法知道 headers 是由代理按预期添加还是由恶意 client 添加。这就是为什么应该配置信任边界的代理来删除来自外部的不受信任的转发流量。您还可以使用removeOnly=true配置ForwardedHeaderTransformer,在这种情况下,它会删除但不使用_header。

在 5.1 ForwardedHeaderFilter被弃用并被ForwardedHeaderTransformer取代,因此在创建交换之前可以更早地处理转发的 headers。如果仍然配置了过滤器,则将其从过滤器列表中取出,而使用ForwardedHeaderTransformer

过滤器

与 Spring MVC 相同

WebHandler API中,您可以使用WebFilter在过滤器处理链的 rest 和目标WebHandler之前和之后应用 interception-style 逻辑。当使用WebFlux 配置时,注册WebFilter就像将它声明为 Spring bean 和(可选)通过在 bean 声明上使用@Order或通过实现Ordered来表示优先级一样简单。

CORS

与 Spring MVC 相同

Spring WebFlux 通过控制器上的 annotations 为 CORS configuration 提供 fine-grained 支持。但是,当您将其与 Spring Security 一起使用时,我们建议依赖 built-in CorsFilter,必须在 Spring Security 的过滤器链之前进行排序。

有关详细信息,请参阅CORSCORS WebFilter部分。

异常

与 Spring MVC 相同

WebHandler API中,您可以使用WebExceptionHandler来处理WebFilter实例链和目标WebHandler中的 exceptions。当使用WebFlux 配置时,注册WebExceptionHandler就像将它声明为 Spring bean 和(可选)通过在 bean 声明上使用@Order或通过实现Ordered来表示优先级一样简单。

以下 table 描述了可用的WebExceptionHandler实现:

Exception Handler 描述
ResponseStatusExceptionHandler 通过设置对 exception 的 HTTP 状态 code 的响应来提供对ResponseStatusException类型的 exceptions 的处理。
WebFluxResponseStatusExceptionHandler ResponseStatusExceptionHandler的扩展,也可以确定任何 exception 上@ResponseStatus annotation 的 HTTP 状态 code。 此处理程序在WebFlux 配置中声明。

编解码器

与 Spring MVC 相同

spring-webspring-core模块支持使用 Reactive Streams 背压通过 non-blocking I/O 序列化和反序列化高级 level objects 的字节内容。以下描述了这种支持:

  • 编码器解码器是低 level contracts 来编码和解码独立于 HTTP 的内容。
  • HttpMessageReaderHttpMessageWriter是 contracts 来编码和解码 HTTP 消息内容。
  • 可以用EncoderHttpMessageWriter包装以使其适用于 web application,而Decoder可以用DecoderHttpMessageReader包装。
  • DataBuffer抽象不同的字节缓冲区表示(e.g. Netty ByteBufjava.nio.ByteBuffer,etc.)并且是所有编解码器都可以工作的。有关此 topic 的更多信息,请参阅“Spring Core”部分中的数据缓冲区和编解码器

spring-core模块提供byte[]ByteBufferDataBufferResourceString编码器和解码器 implementations。 spring-web模块提供 Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers 和其他编码器和解码器以及 web-only HTTP message reader 和 writer implementations,用于表单数据,multipart 内容,server-sent events 等。

ClientCodecConfigurerServerCodecConfigurer通常用于配置和自定义要在 application 中使用的编解码器。请参阅有关配置HTTP 消息编解码器的部分。

Jackson-JSON

当 Jackson library 存在时,都支持 JSON 和二进制 JSON(Smile)。

Jackson2Decoder的工作原理如下:

  • Jackson 的异步,non-blocking 解析器用于将字节块流聚合到TokenBuffer中,每个字节块代表一个 JSON object。
  • 每个TokenBuffer都传递给 Jackson 的ObjectMapper以创建更高的 level object。
  • 解码到 single-value 发布者(e.g. Mono)时,有一个TokenBuffer
  • 当解码为 multi-value 发布者(e.g. Flux)时,只要收到完全形成的 object 的足够字节,每个TokenBuffer就会传递给ObjectMapper。输入内容可以是 JSON array,如果 content-type 是“application/stream json”,则为line-delimited JSON

Jackson2Encoder的工作原理如下:

  • 对于单个 value 发布者(e.g. Mono),只需通过ObjectMapper序列化它。
  • 对于具有“application/json”的 multi-value 发布者,默认情况下使用Flux#collectToList()收集值,然后序列化生成的集合。
  • 对于具有流媒体类型(如application/stream+jsonapplication/stream+x-jackson-smile)的 multi-value 发布者,使用line-delimited JSON格式单独编码,写入和刷新每个 value。
  • 对于 SSE,每 event 调用Jackson2Encoder并刷新输出以确保无延迟地传递。

默认情况下,Jackson2EncoderJackson2Decoder都不支持String类型的元素。相反,默认假设是 string 或 strings 序列表示由CharSequenceEncoder呈现的序列化 JSON 内容。如果您需要从Flux<String>渲染 JSON array,请使用Flux#collectToList()并编码Mono<List<String>>

表单数据

FormHttpMessageReaderFormHttpMessageWriter支持解码和编码“application/x-www-form-urlencoded”内容。

在通常需要从多个位置访问表单内容的服务器端,ServerWebExchange提供了一个专用的getFormData()方法,该方法通过FormHttpMessageReader解析内容,然后缓存结果以便重复访问。请参阅WebHandler API部分中的表格数据

使用getFormData()后,将无法再从请求正文中读取原始原始内容。出于这个原因,预期应用程序将始终通过ServerWebExchange来访问缓存的表单数据,而不是从原始请求正文中读取。

Multipart

MultipartHttpMessageReaderMultipartHttpMessageWriter支持解码和编码“multipart/form-data”内容。反过来MultipartHttpMessageReader委托给另一个HttpMessageReader实际解析为Flux<Part>然后简单地将部分收集到MultiValueMap。目前Synchronoss NIO Multipart用于实际解析。

在可能需要从多个位置访问 multipart 表单内容的服务器端,ServerWebExchange提供了一个专用的getMultipartData()方法,该方法通过MultipartHttpMessageReader解析内容,然后缓存结果以便重复访问。请参阅WebHandler API部分中的Multipart 数据

使用getMultipartData()后,将无法再从请求正文中读取原始原始内容。出于这个原因,applications 必须始终使用getMultipartData()重复,map-like 访问部分,或者依赖SynchronossPartHttpMessageReader

Streaming

与 Spring MVC 相同

当流式传输到 HTTP 响应时(对于 example,text/event-streamapplication/stream+json),定期发送数据非常重要,以便可以更快地可靠地检测断开的客户端。这样的发送可以是 comment-only,空的 SSE event 或任何其他有效充当心跳的“no-op”数据。

DataBuffer

DataBuffer是 WebFlux 中字节缓冲区的表示。 reference 的 Spring 核心部分在数据缓冲区和编解码器部分有更多内容。要理解的 key 要点是在像 Netty 这样的服务器上,字节缓冲区被池化并且 reference 计数,并且必须在被消耗时释放以避免 memory 泄漏。

WebFlux applications 通常不需要关心这些问题,除非它们直接使用或生成数据缓冲区,而不是依赖于编解码器与更高级别的 objects 进行转换。或者除非他们选择创建自定义编解码器。对于此类情况,请查看数据缓冲区和编解码器中的信息,尤其是使用 DataBuffer部分。

Logging

与 Spring MVC 相同

Spring WebFlux 中的 DEBUG level logging 设计为紧凑,最小和 human-friendly。它侧重于高 value 位信息,这些信息对于仅在调试特定问题时有用的其他信息一次又一次有用。

TRACE level logging 通常遵循与 DEBUG 相同的原则(对于 example 也不应该是一个 firehose),但可以用于调试任何问题。此外,一些 log 消息可能在 TRACE vs DEBUG 中显示不同的 level 细节。

好 logging 来自使用日志的经验。如果您发现任何不符合既定目标的事件,请告诉我们。

日志Id

在 WebFlux 中,可以在多个线程上执行单个请求,并且线程 ID 对于关联属于特定请求的 log 消息没有用。这就是为什么 WebFlux log 消息默认以 request-specific ID 为前缀。

在服务器端,log ID 存储在ServerWebExchange属性(LOG_ID_ATTRIBUTE)中,而基于该 ID 的完全格式化的前缀可从ServerWebExchange#getLogPrefix()获得。在WebClient侧,log ID 存储在ClientRequest属性(LOG_ID_ATTRIBUTE)中,而完全格式化的前缀可从ClientRequest#logPrefix()获得。

敏感数据

与 Spring MVC 相同

DEBUGTRACE logging 可以 log 敏感信息。这就是默认情况下屏蔽表单参数和 headers 的原因,您必须完全显式启用它们的 logging。

followig example 显示了如何为 server-side 请求执行此操作:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}

以下 example 显示了如何为 client-side 请求执行此操作:

1
2
3
4
5
6
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build())
.build();

DispatcherHandler

与 Spring MVC 相同

Spring WebFlux 与 Spring MVC 类似,是围绕前端控制器 pattern 设计的,其中中心WebHandlerDispatcherHandler为请求处理提供了共享算法,而实际工作则由可配置的委托组件执行。这个 model 非常灵活,支持各种工作流程。

DispatcherHandler从 Spring configuration 中发现它需要的委托组件。它也被设计为 Spring bean 本身,并实现ApplicationContextAware以访问运行它的 context。如果声明的

WebFlux application 中的 Spring configuration 通常包含:

赋予WebHttpHandlerBuilder以 build 处理链,如下面的 example 所示:

1
2
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context);

结果HttpHandler准备好与服务器适配器一起使用。

特殊的Bean类型

与 Spring MVC 相同

DispatcherHandler委托特殊的 beans 来处理请求并呈现适当的响应。 “special beans”是指实现 WebFlux framework contracts 的 Spring-managed Object实例。那些通常带有 built-in contracts,但您可以自定义它们的 properties,扩展它们或替换它们。

以下 table lists 列出DispatcherHandler检测到的特殊 beans。请注意,在较低的 level 处还检测到一些其他 beans(请参阅 Web Handler API 中的特殊的 bean 类型)。

Bean 类型 说明
HandlerMapping 将请求映射到处理程序。映射基于某些标准,其详细信息因HandlerMapping implementation - 带注释的控制器,简单的 URL pattern 映射等而异。 主HandlerMapping __mplement 为RequestMappingHandlerMapping用于@RequestMapping注释方法,RouterFunctionMapping用于功能端点 routes,SimpleUrlHandlerMapping用于显式注册 URI 路径模式和WebHandler实例。
HandlerAdapter 无论实际调用处理程序如何,都帮助DispatcherHandler调用映射到请求的处理程序。例如,调用带注释的控制器需要解析 annotations。 HandlerAdapter的主要目的是保护DispatcherHandler免受这些细节的影响。
HandlerResultHandler 处理处理程序调用的结果并完成响应。见结果处理

WebFlux配置

与 Spring MVC 相同

Applications 可以声明 process 请求所需的基础结构 beans(列在Web Handler APIDispatcherHandler下)。但是,在大多数情况下,WebFlux 配置是最好的起点。它声明了所需的 beans 并提供了一个 higher-level configuration 回调 API 来自定义它。

Spring Boot 依赖于 WebFlux 配置来配置 Spring WebFlux,还提供了许多额外的方便选项。

Processing

与 Spring MVC 相同

DispatcherHandler处理请求如下:

  • 要求每个HandlerMapping找到匹配的处理程序,并使用第一个 match。
  • 如果找到一个处理程序,它将通过适当的HandlerAdapter执行,它将 return value 从执行中公开为HandlerResult
  • 通过直接写入响应或使用视图进行渲染,HandlerResult被赋予适当的HandlerResultHandler以完成处理。

结果处理

从处理程序的调用到HandlerAdapter的 return value 被包装为HandlerResult,以及一些额外的 context,并传递给声称支持它的第一个HandlerResultHandler。以下 table 显示了可用的HandlerResultHandler实现,所有这些都在WebFlux 配置中声明:

结果处理程序类型 Return 值 默认 Order
ResponseEntityResultHandler ResponseEntity,通常来自@Controller个实例。 0
ServerResponseResultHandler ServerResponse,通常来自功能 endpoints。 0
ResponseBodyResultHandler 处理@ResponseBody方法或@RestController classes 中的 return 值。 100
ViewResolutionResultHandler CharSequence视图模型Map渲染或任何其他Object被视为 model 属性。 另见查看分辨率 Integer.MAX_VALUE

Exceptions

与 Spring MVC 相同

HandlerAdapter返回的HandlerResult可以基于某些 handler-specific 机制公开函数以进行错误处理。在以下情况下调用此错误 function:

  • 处理程序(用于 example,@Controller)调用失败。
  • 通过HandlerResultHandler处理处理程序 return value 失败。

错误 function 可以将响应(对于 example,更改为错误状态)更改为 long,因为错误信号发生在从处理程序返回的 reactive 类型生成任何数据项之前。

这是@Controller classes 中@ExceptionHandler方法的支持方式。相比之下,Spring MVC 中对它的支持是建立在HandlerExceptionResolver上的。这通常无关紧要。但是,请记住,在 WebFlux 中,您不能使用@ControllerAdvice来处理在选择处理程序之前发生的 exceptions。

另请参阅“Annotated Controller”部分中的管理 Exceptions或 WebHandler API 部分中的Exceptions

View解析器

与 Spring MVC 相同

视图分辨率允许使用 HTML 模板和 model 呈现给浏览器,而无需将您与特定的视图技术联系起来。在 Spring WebFlux 中,视图解析通过专用的HandlerResultHandler支持,该HandlerResultHandler使用ViewResolver实例将 String(表示逻辑视图 name)map 映射到View实例。然后使用View来呈现响应。

Handling

与 Spring MVC 相同

传递给ViewResolutionResultHandlerHandlerResult包含来自处理程序的 return value 和包含在请求处理期间添加的属性的 model。 return value 将作为以下之一处理:

  • StringCharSequence:要通过已配置的ViewResolver __mplement 列表解析为View的逻辑视图 name。
  • void:根据请求路径选择默认视图 name,减去前导和尾部斜杠,并将其解析为View。如果未提供视图 name(对于 example,返回 model 属性)或 async return value(对于 example,Mono已完成为空),也会发生相同的情况。
  • 渲染:用于视图解析方案的 API。使用 code 完成浏览 IDE 中的选项。
  • ModelMap:要添加到请求的 model 的额外 model 属性。
  • 任何其他:任何其他 return value(由简单类型除外,由BeanUtils 的#isSimpleProperty确定)被视为要添加到 model 的 model 属性。除非存在处理程序方法@ModelAttribute annotation,否则属性 name 是使用公约从 class name 派生的。

model 可以包含异步,reactive 类型(对于 example,来自 Reactor 或 RxJava)。在渲染之前,AbstractView将此类 model 属性解析为具体值并更新 model。 Single-value reactive 类型被解析为单个 value 或没有 value(如果为空),而 multi-value reactive 类型(对于 example,Flux<T>)被收集并解析为List<T>

配置视图解析器就像在 Spring configuration 中添加ViewResolutionResultHandler bean 一样简单。 WebFlux 配置提供专用的 configuration API 以进行视图分辨率。

有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅查看技术

重定向

与 Spring MVC 相同

视图 name 中的特殊redirect:前缀允许您执行重定向。 UrlBasedViewResolver(和 sub-classes)将此识别为需要重定向的指令。视图 name 的 rest 是重定向 URL。

净效果与控制器返回RedirectViewRendering.redirectTo("abc").build()相同,但现在控制器本身可以按逻辑视图名称操作。视图 name(例如redirect:/some/resource)与当前 application 相关,而视图 name(例如redirect:http://example.com/arbitrary/path)重定向到绝对 URL。

内容协商

与 Spring MVC 相同

ViewResolutionResultHandler支持内容 negotiation。它将请求媒体类型与每个选定的View支持的媒体类型进行比较。使用支持所请求媒体 type(s 的第一个View

在 order 中支持 JSON 和 XML 等媒体类型,Spring WebFlux 提供HttpMessageWriterView,这是一个特殊的View,通过HttpMessageWriter呈现。通常,您可以通过WebFlux Configuration将它们配置为默认视图。如果它们匹配所请求的媒体类型,则始终选择并使用默认视图。

带注解的控制器

与 Spring MVC 相同

Spring WebFlux 提供 annotation-based 编程 model,其中@Controller@RestController组件使用 annotations 来表达请求映射,请求输入,处理 exceptions 等。带注释的控制器具有灵活的方法签名,不必扩展 base classes,也不必实现特定的接口。

以下清单显示了一个基本的 example:

1
2
3
4
5
6
7
8
@RestController
public class HelloController {

@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}

在前面的 example 中,该方法返回一个String以写入响应主体。

Controller注解

与 Spring MVC 相同

您可以使用标准的 Spring bean 定义来定义 controller beans。 @Controller构造型允许 auto-detection 并与 Spring 一般支持对齐,以检测 classpath 中的@Component classes 和 auto-registering bean 定义。它还充当带注释的 class 的构造型,表明它作为 web component 的角色。

要启用 auto-detection 这样的@Controller beans,可以将 component 扫描添加到 Java configuration 中,如下面的 example 所示:

1
2
3
4
5
6
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {

// ...
}
1 扫描org.example.web包。

@RestController是一个[复合注解由元注解@Controller@ResponseBody组成,表示一个控制器,其每个方法都继承 type-level @ResponseBody 注解,因此,直接写入响应主体,而不是视图解析器,并使用 HTML 模板进行渲染。

请求映射

与 Spring MVC 相同

@RequestMapping annotation 用于 map 对控制器方法的请求。它具有 match 的各种属性,包括 URL,HTTP 方法,请求参数,headers 和媒体类型。您可以在 class level 中使用它来表示共享映射,或者在方法 level 中使用它来缩小到特定的端点映射。

还有@RequestMapping的 HTTP 方法特定快捷方式变体:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

前面提到的注解是自定义注解,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法而不是使用@RequestMapping,默认情况下,它与所有 HTTP 方法匹配。在同一 time,class level 仍然需要@RequestMapping来表示共享映射。

以下 example 使用类型和方法 level 映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/persons")
class PersonController {

@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}

URI模式

与 Spring MVC 相同

您可以使用 glob 模式和通配符 map 请求:

  • ?匹配一个字符
  • *匹配路径段中的零个或多个字符
  • ** 匹配零个或多个路径段

您还可以声明 URI 变量并使用@PathVariable访问它们的值,如下面的 example 所示:

1
2
3
4
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}

您可以在 class 和方法级别声明 URI 变量,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {

@GetMapping("/pets/{petId}") (2)
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
1 Class-level URI 映射。
2 Method-level URI 映射。

URI 变量会自动转换为适当的类型,或者引发TypeMismatchException。默认情况下支持简单类型(intlongDate等),您可以注册对任何其他数据类型的支持。见类型转换DataBinder

URI 变量可以显式命名(对于 example,@PathVariable("customId")),但是如果名称相同则可以保留该详细信息,并使用调试信息或使用 Java 8 上的-parameters编译器 flag 编译 code。

语法{*varName}声明一个 URI 变量,该变量匹配零个或多个剩余路径段。对于 example /resources/{*path}匹配所有 files /resources/"path"变量捕获完整的相对路径。

语法{varName:regex}声明了一个 URI 变量,其正则表达式的语法为:{varName:regex}。对于 example,给定/spring-web-3.0.5 .jar的 URL,以下方法将提取 name,version 和文件扩展名:

1
2
3
4
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}

URI 路径模式还可以嵌入${…}占位符,这些占位符在启动时通过PropertyPlaceHolderConfigurer针对本地,系统,环境和其他 property 源进行解析。例如,您可以使用它来基于某些外部 configuration 参数化基本 URL。

Spring WebFlux 使用PathPatternPathPatternParser进行 URI 路径匹配支持。这两个 classes 都位于spring-web中,并且明确设计用于 web applications 中的 HTTP URL paths,其中在运行时匹配大量 URI 路径模式。

Spring WebFlux 不支持后缀 pattern 匹配 - 与 Spring MVC 不同,其中/person等映射也与/person.*匹配。对于 URL-based content negotiation,如果需要,我们建议使用查询参数,该参数更简单,更明确,并且不易受基于 URL 路径的攻击。

Pattern比较

与 Spring MVC 相同

当多个模式匹配 URL 时,必须对它们进行比较以找到最佳的 match。这是通过PathPattern.SPECIFICITY_COMPARATOR完成的,它会查找更具体的模式。

对于每个 pattern,根据 URI 变量和通配符的数量计算得分,其中 URI 变量得分低于通配符。总得分较低的 pattern 获胜。如果两个模式具有相同的分数,则选择的时间越长。

Catch-all 个模式(对于 example,**{*varName})将从评分中排除,并始终排在最后。如果两个模式都是 catch-all,则选择的时间越长。

可消费的媒体类型

与 Spring MVC 相同

您可以根据请求的Content-Type缩小请求映射,如下面的 example 所示:

1
2
3
4
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}

consumemes 属性还支持否定表达式 - 对于 example,!text/plain表示除text/plain之外的任何 content type。

您可以在 class level 声明共享的consumes属性。但是,与大多数其他请求映射属性不同,当在 class level 中使用时,method-level consumes属性会覆盖而不是扩展 class-level 声明。

MediaType提供常用媒体类型的常量 - 对于 example,APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE

可生产的媒体类型

与 Spring MVC 相同

您可以根据Accept请求标头和控制器方法生成的内容类型列表缩小请求映射,如下面的示例所示:

1
2
3
4
5
@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}

媒体类型可以指定字符集。支持否定表达式 - 例如,!text/plain表示除text/plain之外的任何 content type。

对于 JSON content type,即使RFC7159明确指出“没有为此注册定义 charset 参数”,也应指定 UTF-8 charset,因为某些浏览器要求它正确解释 UTF-8 特殊字符。

您可以在 class level 声明共享的produces属性。但是,与大多数其他请求映射属性不同,当在 class level 中使用时,method-level produces属性会覆盖而不是扩展 class level 声明。

MediaType提供常用媒体类型的常量 - e.g. APPLICATION_JSON_UTF8_VALUEAPPLICATION_XML_VALUE

参数和Headers

与 Spring MVC 相同

您可以根据查询参数条件缩小请求映射。您可以测试是否存在查询参数(myParam),缺少(!myParam)或特定 value(myParam=myValue)。以下示例测试带有 value 的参数:

1
2
3
4
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 检查myParam是否等于myValue

您也可以使用相同的请求标头条件,如下面的 example 显示:

1
2
3
4
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 检查myHeader是否等于myValue

HTTP头,OPTIONS

与 Spring MVC 相同

@GetMapping@RequestMapping(method=HttpMethod.GET)透明地支持 HTTP HEAD 以进行请求映射。控制器方法无需更改。应用于HttpHandler服务器适配器的响应 wrapper 确保将Content-Length标头设置为写入的字节数,而不实际写入响应。

默认情况下,通过将Allow响应标头设置为具有匹配 URL 模式的所有@RequestMapping方法中列出的 HTTP 方法列表来处理 HTTP OPTIONS。

对于没有 HTTP 方法声明的@RequestMappingAllow标头设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明支持的 HTTP 方法(对于 example,使用 HTTP 方法特定的变体 - @GetMapping@PostMapping等)。

您可以将@RequestMapping方法显式 map 到 HTTP HEAD 和 HTTP OPTIONS,但在 common 情况下这不是必需的。

自定义注解

与 Spring MVC 相同

Spring WebFlux 支持使用复合注解进行请求映射。这些是注释本身 meta-annotated 与@RequestMapping并且用于重新声明@RequestMapping属性的子集(或全部)以及更窄,更具体的目的。

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping是组合注释的示例。它们是提供的,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法而不是使用@RequestMapping,默认情况下,它与所有 HTTP 方法匹配。如果您需要_notample of annotations,请查看如何声明它们。

Spring WebFlux 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要 sub-classing RequestMappingHandlerMapping并覆盖getCustomMethodCondition方法,您可以在其中检查自定义属性并 return 您自己的RequestCondition

显式注册

与 Spring MVC 相同

您可以以编程方式注册 Handler 方法,这些方法可用于动态注册或高级案例,例如不同 URL 下的同一处理程序的不同实例。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 Inject 目标处理程序和控制器的处理程序映射。
2 准备请求映射元数据。
3 获取处理程序方法。
4 添加注册。

处理程序方法

与 Spring MVC 相同

@RequestMapping处理程序方法具有灵活的签名,可以从一系列受支持的控制器方法 arguments 和 return 值中进行选择。

方法Arguments

与 Spring MVC 相同

以下表格显示支持的控制器方法 arguments。

arguments 支持 Reactive 类型(Reactor,RxJava,或其他),需要阻塞 I/O(对于 example,读取请求主体)才能解析。这在“描述”列中标记。 活动类型不适用于不需要阻止的 arguments。

JDK 1.8 的java.util.Optional作为方法参数与 annotations 一起被支持,该注释具有required属性(对于 example,@RequestParam@RequestHeader和其他)并且等同于required=false

控制器方法参数 描述
ServerWebExchange 访问 HTTP 请求和响应,请求和 session 属性,checkNotModified方法等的完整ServerWebExchange - 容器。
ServerHttpRequest , ServerHttpResponse 访问 HTTP 请求或响应。
WebSession 访问 session。除非添加属性,否则这不会强制启动新的 session。支持 reactive 类型。
java.security.Principal 当前经过身份验证的用户 - 如果已知,可能是特定的Principal implementation class。支持 reactive 类型。
org.springframework.http.HttpMethod 请求的 HTTP 方法。
java.util.Locale 当前请求 locale,由最具体的LocaleResolver可用 - 确定 - 生效,配置为LocaleResolver/LocaleContextResolver
java.util.TimeZone + java.time.ZoneId 与当前请求关联的 time zone,由LocaleContextResolver确定。
@PathVariable 用于访问 URI 模板变量。见URI 模式
@MatrixVariable 用于访问 URI 路径段中的 name-value 对。见矩阵变量
@RequestParam 用于访问 Servlet 请求参数。参数值将转换为声明的方法参数类型。见@RequestParam。 请注意,使用@RequestParam是可选的 - 例如,设置其属性。请参阅本 table 后面的“任何其他参数”。
@RequestHeader 用于访问请求 headers。标头值将转换为声明的方法参数类型。见@RequestHeader
@CookieValue 访问 cookies。 Cookie 值将转换为声明的方法参数类型。见@CookieValue
@RequestBody 用于访问 HTTP 请求正文。使用HttpMessageReader实例将正文内容转换为声明的方法参数类型。支持 reactive 类型。见@RequestBody
HttpEntity<B> 用于访问请求 headers 和 body。正文用HttpMessageReader实例转换。支持 reactive 类型。见HttpEntity
@RequestPart 用于访问multipart/form-data请求中的部分。支持 reactive 类型。见Multipart ContentMultipart 数据
java.util.Maporg.springframework.ui.Modelorg.springframework.ui.ModelMap 用于访问 HTML 控制器中使用的 model,并作为视图呈现的一部分向模板公开。
@ModelAttribute 用于访问 model 中的现有属性(如果不存在则实例化),并应用数据 binding 和验证。见@ModelAttribute以及模型DataBinder。 请注意,使用@ModelAttribute是可选的 - 例如,设置其属性。请参阅本 table 后面的“任何其他参数”。
Errors , BindingResult 用于从命令 object(即@ModelAttribute参数)的验证和数据 binding 访问错误或从验证@RequestBody@RequestPart参数的错误。必须在经过验证的方法参数之后立即声明ErrorsBindingResult参数。
SessionStatus class-level @SessionAttributes 用于标记表单处理完成,它触发通过 class-level @SessionAttributes 注释声明的 session 属性的清除。有关详细信息,请参阅@SessionAttributes
UriComponentsBuilder 用于准备相对于当前请求的 host, port,scheme 和 path 的 URL。见URI 链接
@SessionAttribute 用于访问任何 session 属性 - 与 session 属性存储在 session 中的 model 属性相反。有关详细信息,请参阅@SessionAttribute
@RequestAttribute 用于访问请求属性。有关详细信息,请参阅@RequestAttribute
任何其他论点 如果方法参数与上述任何一个都不匹配,则默认情况下,如果它是一个简单类型(由BeanUtils 的#isSimpleProperty确定),则为@RequestParam,否则为@ModelAttribute

Return值

与 Spring MVC 相同

以下 table 显示支持的控制器方法 return 值。请注意,libraries 中的 reactive 类型(如 Reactor,RxJava,或其他)通常支持所有 return 值。

控制器方法 return value 描述
@ResponseBody return value 通过HttpMessageWriter实例编码并写入响应。见@ResponseBody
HttpEntity<B> , ResponseEntity<B> return value 指定完整响应,包括 HTTP headers,主体通过HttpMessageWriter实例编码并写入响应。见ResponseEntity
HttpHeaders 用 headers 返回响应,没有正文。
String 要使用ViewResolver实例解析并与隐式 model 一起使用的视图 name - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(描述为)以编程方式丰富 model。
View 一个View实例,用于与隐式 model 一起呈现 - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(描述为)以编程方式丰富 model。
java.util.Map , org.springframework.ui.Model 要添加到隐式 model 的属性,其中 view name 基于请求路径隐式确定。
@ModelAttribute 要添加到 model 的属性,其中 view name 基于请求路径隐式确定。 请注意@ModelAttribute是可选的。请参阅本 table 后面的“任何其他 return value”。
Rendering model 和视图渲染方案的 API。
void 具有void,可能是异步(对于 example,Mono<Void>), return 类型(或null return value)的方法被认为已完全处理了响应,如果它还具有ServerHttpResponseServerWebExchange参数或@ResponseStatus 注释。如果控制器进行了正 ETag 或lastModified时间戳检查,则同样也是 true。 // TODO:有关详情,请参阅控制器。 如果上面的 none 是 true,则void return 类型也可以为 REST 控制器指示“无响应主体”或为 HTML 控制器指定默认视图 name 选择。
Flux<ServerSentEvent>Observable<ServerSentEvent>或其他 reactive 类型 发出 server-sent events。当只需要写入数据时,可以省略ServerSentEvent wrapper(但是,必须通过produces属性在映射中请求或声明text/event-stream)。
任何其他 return value 如果 return value 与上述任何一个都不匹配,则默认情况下,它被视为 view name,如果它是Stringvoid(默认视图 name 选择适用),或者作为 model 属性添加到 model ,除非它是一个简单的类型,由BeanUtils 的#isSimpleProperty确定,在这种情况下它仍然没有得到解决。

类型转换

与 Spring MVC 相同

一些带注释的控制器方法 arguments 表示 String-based 请求输入(对于 example,@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue),如果参数声明为String以外的其他类型,则可能需要进行类型转换。

对于此类情况,将根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(例如intlongDate和其他)。可以通过WebDataBinder(参见DataBinder)或通过FormattingConversionService(参见Spring 字段格式)注册Formatters来自定义类型转换。

矩阵变量

与 Spring MVC 相同

RFC 3986讨论路径段中的 name-value 对。在 Spring WebFlux 中,我们将它们称为基于 Tim Berners-Lee 的“老帖子”的“矩阵变量”,但它们也可以称为 URI 路径参数。

矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔 - 对于 example,"/cars;color=red,green;year=2012"。也可以通过重复的变量名来指定多个值 - 对于 example,"color=red;color=green;color=blue"

与 Spring MVC 不同,在 WebFlux 中,URL 中矩阵变量的存在与否不会影响请求映射。换句话说,您不需要使用 URI 变量来屏蔽变量内容。也就是说,如果要从控制器方法访问矩阵变量,则需要将 URI 变量添加到期望矩阵变量的路径段中。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

// petId == 42
// q == 11
}

鉴于所有路径段都可以包含矩阵变量,您有时可能需要消除矩阵变量所在的路径变量的歧义,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
// 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
}

您可以定义矩阵变量,可以将其定义为可选,并指定默认 value,如下面的 example 所示:

1
2
3
4
5
6
7
// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

// q == 1
}

要获取所有矩阵变量,请使用MultiValueMap,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
// 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]
}

@RequestParam

与 Spring MVC 相同

您可以使用@RequestParam annotation 将查询参数绑定到控制器中的方法参数。以下 code 代码段显示了用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

Servlet API“请求参数”概念将查询参数,表单数据和多部分合并为一个。但是,在 WebFlux 中,每个都通过ServerWebExchange单独访问。虽然@RequestParam仅绑定到查询参数,但您可以使用 data binding 将查询参数,表单数据和多部分应用于命令 object

默认情况下,需要使用@RequestParam annotation 的方法参数,但您可以通过将@RequestParam的 flag 设置为false或通过java.util.Optional wrapper 声明参数来指定方法参数是可选的。

如果目标方法参数类型不是String,则会自动应用类型转换。见类型转换

Map<String, String>MultiValueMap<String, String>参数上声明@RequestParam annotation 时,map 将填充所有查询参数。

请注意,使用@RequestParam是可选的 - 例如,设置其属性。默认情况下,任何属于简单 value 类型的参数(由BeanUtils 的#isSimpleProperty确定)并且未被任何其他参数解析器解析的视图都被视为使用@RequestParam进行注释。

RequestHeader注解

与 Spring MVC 相同

您可以使用@RequestHeader annotation 将请求标头绑定到控制器中的方法参数。

以下 example 显示了带有 headers 的请求:

1
2
3
4
5
6
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

以下 example 获取Accept-EncodingKeep-Alive headers 的 value:

1
2
3
4
5
6
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 获取Accept-Encoging标头的 value。
2 获取Keep-Alive标头的 value。

如果目标方法参数类型不是String,则会自动应用类型转换。见类型转换

Map<String, String>MultiValueMap<String, String>HttpHeaders参数上使用@RequestHeader annotation 时,map 将填充所有标题值。

Built-in 支持可用于将 comma-separated string 转换为 array 或 strings 集合或类型转换系统已知的其他类型。对于 example,使用@RequestHeader("Accept")注释的方法参数可以是String类型,但也可以是String[]List<String>

CookieValue注解

与 Spring MVC 相同

您可以使用@CookieValue annotation 将 HTTP cookie 的 value 绑定到控制器中的方法参数。

以下 example 显示了一个带有 cookie 的请求:

1
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下 code sample 演示了如何获取 cookie value:

1
2
3
4
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 获取 cookie value。

如果目标方法参数类型不是String,则会自动应用类型转换。见类型转换

ModelAttribute注解

与 Spring MVC 相同

您可以在方法参数上使用@ModelAttribute annotation 来从 model 访问属性,或者如果不存在则将其实例化。 model 属性还覆盖了查询参数和表单字段的值,其名称为 match 到字段名称。这称为数据 binding,它使您不必处理解析和转换单个查询参数和表单字段。以下 example 绑定Pet的实例:

1
2
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 绑定Pet的实例。

前面的 example 中的Pet实例解析如下:

  • 如果已经通过模型添加了 model。
  • 从 HTTP session 到@SessionAttributes
  • 从默认构造函数的调用。
  • 从“主构造函数”调用 arguments match 查询参数或表单字段。参数名称通过字节码中的 JavaBeans @ConstructorProperties或 runtime-retained 参数名称确定。

获取 model 属性实例后,将应用数据 binding。 WebExchangeDataBinder class 将查询参数和表单字段的名称与目标Object上的字段名称相匹配。在必要时应用类型转换后填充匹配字段。有关数据 binding(和验证)的更多信息,请参阅验证。有关自定义数据 binding 的更多信息,请参阅DataBinder

数据绑定可能导致错误。默认情况下,会引发WebExchangeBindException,但是,要在控制器方法中检查此类错误,可以在@ModelAttribute旁边添加BindingResult参数,如下面的 example 所示:

1
2
3
4
5
6
7
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 添加BindingResult

您可以在数据 binding 之后通过添加javax.validation.Valid annotation 或 Spring 的@Validated annotation 自动应用验证(另请参阅Bean 验证Spring 验证)。以下 example 使用@Valid annotation:

1
2
3
4
5
6
7
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 在 model 属性参数上使用@Valid

与 Spring MVC 不同,Spring WebFlux 支持 model 中的 reactive 类型 - 对于 example,Mono<Account>io.reactivex.Single<Account>。您可以使用或不使用 reactive 类型 wrapper 声明@ModelAttribute参数,并且如果需要,它将相应地解析为实际的 value。但是,请注意,要使用BindingResult参数,必须在没有 reactive 类型 wrapper 的情况下声明@ModelAttribute参数,如前所示。或者,您可以通过 reactive 类型处理任何错误,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}

请注意,使用@ModelAttribute是可选的 - 例如,设置其属性。默认情况下,任何不是简单 value 类型(由BeanUtils 的#isSimpleProperty确定)并且未被任何其他参数解析器解析的参数都被视为使用@ModelAttribute进行注释。

SessionAttributes注解

与 Spring MVC 相同

@SessionAttributes用于在请求之间WebSession中存储 model 属性。它是 type-level annotation,声明特定控制器使用的 session 属性。这通常列出 model 属性的名称或 model 属性的类型,这些属性应该透明地存储在 session 中以供后续访问请求使用。

考虑以下 example:

1
2
3
4
5
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 使用@SessionAttributes annotation。

在第一个请求中,当 model 的 model 属性被添加到 model 时,它会自动提升并保存在WebSession中。它保持不变,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 annotation。
2 使用SessionStatus变量。

SessionAttribute注解

与 Spring MVC 相同

如果需要访问全局管理的 pre-existing session 属性(即,在控制器外部 - 对于 example,通过过滤器)并且可能存在或不存在,则可以在方法参数上使用@SessionAttribute annotation,如下所示 example 说明:

1
2
3
4
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 使用@SessionAttribute

对于需要添加或删除 session 属性的用例,请考虑将WebSession注入控制器方法。

要在 session 中临时存储 model 属性作为控制器工作流的一部分,请考虑使用SessionAttributes,如@SessionAttributes中所述。

RequestAttribute注解

与 Spring MVC 相同

@SessionAttribute类似,您可以使用@RequestAttribute annotation 访问之前创建的 pre-existing 请求属性(对于 example,通过WebFilter),如下面的 example 所示:

1
2
3
4
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 使用@RequestAttribute

MultipartContent

与 Spring MVC 相同

Multipart 数据中所述,ServerWebExchange提供对 multipart 内容的访问。在控制器中处理文件上载表单(对于 example,从浏览器)的最佳方法是通过数据绑定到命令 object,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyForm {

private String name;

private MultipartFile file;

// ...

}

@Controller
public class FileUploadController {

@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}

}

您还可以在 RESTful 服务方案中从 non-browser clients 提交 multipart 请求。以下 example 使用文件和 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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访问各个部分,如下面的示例所示:

1
2
3
4
5
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file) { (2)
// ...
}
1 使用@RequestPart获取元数据。
2 使用@RequestPart获取文件。

要反序列化原始部件内容(对于 example,为 JSON - 类似于@RequestBody),您可以声明具体目标Object,而不是Part,如下面的 example 所示:

1
2
3
4
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
1 使用@RequestPart获取元数据。

您可以使用@RequestPartjavax.validation.Valid或 Spring 的@Validated annotation 组合,这会导致应用标准 Bean 验证。默认情况下,验证错误会导致WebExchangeBindException,这会转换为 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内本地处理验证错误,如下面的示例所示:

1
2
3
4
5
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, (1)
BindingResult result) { (2)
// ...
}
1 使用@Valid 注释。
2 使用BindingResult参数。

要将所有 multipart 数据作为MultiValueMap访问,您可以使用@RequestBody,如下面的 example 所示:

1
2
3
4
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
1 使用@RequestBody

要以流方式顺序访问 multipart 数据,您可以使用@RequestBody而不是Flux<Part>,如下面的 example 所示:

1
2
3
4
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
1 使用@RequestBody

RequestBody注解

与 Spring MVC 相同

您可以使用@RequestBody annotation 将请求主体读取并反序列化为ObjectHttpMessageReader。以下 example 使用@RequestBody参数:

1
2
3
4
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}

与 Spring MVC 不同,在 WebFlux 中,@RequestBody方法参数支持 reactive 类型和完全 non-blocking 读取和(client-to-server)流式传输。以下 example 使用Mono

1
2
3
4
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}

您可以使用WebFlux 配置HTTP 消息编解码器选项来配置或自定义消息 readers。

您可以将@RequestBodyjavax.validation.Valid或 Spring 的@Validated annotation 结合使用,这会导致应用标准 Bean 验证。默认情况下,验证错误会导致WebExchangeBindException,这会转换为 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内本地处理验证错误。以下 example 使用BindingResult参数:

1
2
3
4
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}

HttpEntity

与 Spring MVC 相同

HttpEntity与使用@RequestBody或多或少完全相同,但它基于一个容器 object,它暴露了 request headers 和 body。以下 example 使用HttpEntity

1
2
3
4
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}

ResponseBody注解

与 Spring MVC 相同

您可以在方法上使用@ResponseBody annotation 通过HttpMessageWriter将 return 序列化到响应正文。以下 example 显示了如何执行此操作:

1
2
3
4
5
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}

class level 也支持@ResponseBody,在这种情况下,它由所有控制器方法继承。这是@RestController的效果,它只不过标有@Controller@ResponseBody

@ResponseBody支持 reactive 类型,这意味着您可以 return Reactor 或 RxJava 类型,并将它们生成的异步值呈现给响应。有关其他详细信息,请参阅JSON 渲染

您可以将@ResponseBody方法与 JSON 序列化视图结合使用。有关详细信息,请参阅Jackson JSON

您可以使用WebFlux 配置HTTP 消息编解码器选项来配置或自定义消息编写。

ResponseEntity

与 Spring MVC 相同

ResponseEntity就像@ResponseBody但有状态和 headers。例如:

1
2
3
4
5
6
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}

WebFlux 支持使用单个 value reactive 类型为主体生成ResponseEntity异步,and/or 单和 multi-valuereactive 类型。

JacksonJSON

Spring 支持 Jackson JSON library。

Jackson Views

与 Spring MVC 相同

Spring WebFlux 为Jackson 的序列化视图提供 built-in 支持,允许只渲染Object中所有字段的子集。要将其与@ResponseBodyResponseEntity控制器方法一起使用,可以使用 Jackson 的@JsonView annotation 来激活序列化视图 class,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@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;
}
}

@JsonView允许 array 视图 classes,但每个控制器方法只能指定一个。如果需要激活多个视图,请使用复合接口。

Model

与 Spring MVC 相同

您可以使用@ModelAttribute annotation:

  • 方法参数 in @RequestMapping方法中,从 model 创建或访问 Object 并通过WebDataBinder将其绑定到请求。
  • 作为@Controller@ControllerAdvice classes 中的 method-level annotation,帮助在任何@RequestMapping方法调用之前初始化 model。
  • @RequestMapping方法上将 return value 标记为 model 属性。

本节讨论@ModelAttribute方法,或前面列表中的第二个 item。控制器可以有任意数量的@ModelAttribute方法。所有这些方法都在同一控制器中的@RequestMapping方法之前调用。也可以通过@ControllerAdvice在控制器之间共享@ModelAttribute方法。有关详细信息,请参阅控制器建议部分。

@ModelAttribute方法具有灵活的方法签名。它们支持许多与@RequestMapping方法相同的 arguments(除了@ModelAttribute本身以及与请求体相关的任何事物)。

以下 example 使用@ModelAttribute方法:

1
2
3
4
5
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}

以下 example 仅添加一个属性:

1
2
3
4
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}

如果未明确指定 name,则会根据类型选择默认 name,如约定的 javadoc 中所述。您始终可以使用重载的addAttribute方法或@ModelAttribute上的 name 属性(对于 return value)分配显式 name。

与 Spring MVC 不同,Spring WebFlux 显式支持 model 中的 reactive 类型(对于 example,Mono<Account>io.reactivex.Single<Account>)。如果在没有 wrapper 的情况下声明@ModelAttribute参数,则可以在@RequestMapping调用的 time 处将此类异步 model 属性透明地解析(并且 model 更新)为其实际值,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@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) {
// ...
}

此外,具有 reactive 类型 wrapper 的任何 model 属性在视图呈现之前被解析为其实际值(并且 model 已更新)。

您还可以在@RequestMapping方法上使用@ModelAttribute作为 method-level annotation,在这种情况下,@RequestMapping方法的 return value 将被解释为 model 属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非 return value 是String否则将被解释为 view name。 @ModelAttribute还可以帮助自定义 model 属性 name,如下面的 example 所示:

1
2
3
4
5
6
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}

DataBinder

与 Spring MVC 相同

@Controller@ControllerAdvice classes 可以有@InitBinder方法来初始化WebDataBinder的实例。反过来,这些用于:

  • 将请求参数(即表单数据或查询)绑定到 model object。
  • String -based 请求值(例如请求参数,路径变量,headers,cookies 等)转换为控制器方法 arguments 的目标类型。
  • 在呈现 HTML 表单时将 model object 值格式化为String值。

@InitBinder方法可以注册 controller-specific java.bean.PropertyEditor或 Spring ConverterFormatter组件。此外,您可以使用WebFlux Java configuration在全局共享的FormattingConversionService中注册ConverterFormatter类型。

@InitBinder方法支持许多与@RequestMapping方法相同的 arguments,但@ModelAttribute(命令 object)arguments 除外。通常,它们使用WebDataBinder参数声明,用于注册,以及void return value。以下 example 使用@InitBinder annotation:

1
2
3
4
5
6
7
8
9
10
11
12
@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 annotation。

或者,当通过共享FormattingConversionService使用Formatter -based 设置时,您可以使用相同的方法并注册 controller-specific Formatter实例,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class FormController {

@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}

// ...
}
1 添加自定义格式化程序(在本例中为DateFormatter)。

管理Exceptions

与 Spring MVC 相同

@Controller@ControllerAdvice classes 可以有@ExceptionHandler方法来处理来自控制器方法的 exceptions。以下 example 包含这样的处理程序方法:

1
2
3
4
5
6
7
8
9
10
@Controller
public class SimpleController {

// ...

@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
1 声明@ExceptionHandler

exception 可以对传播的 top-level exception(也就是抛出的直接IOException)进行匹配,或者对wrapper exception 中的直接原因进行匹配(对于 example,包裹在IllegalStateException中)。

对于匹配 exception 类型,最好将目标 exception 声明为方法参数,如前面的 example 所示。或者,annotation 声明可以将 exception 类型缩小为 match。我们通常建议在参数签名中尽可能具体,并在@ControllerAdvice上使用相应的 order 优先声明主根 exception 映射。有关详细信息,请参阅MVC 部分

WebFlux 中的@ExceptionHandler方法支持与@RequestMapping方法相同的 arguments 和 return 值方法,以及请求 body-和@ModelAttribute -related 方法 arguments 的 exception。

方法支持_Sp_ _FF 中的@ExceptionHandler方法。有关详细信息,请参阅DispatcherHandler

REST-API异常

与 Spring MVC 相同

REST 服务的 common 要求是在响应正文中包含错误详细信息。 Spring Framework 不会自动执行此操作,因为响应正文中错误详细信息的表示形式为 application-specific。但是,@RestController可以使用@ExceptionHandler方法和ResponseEntity return value 来设置响应的状态和正文。此类方法也可以在@ControllerAdvice classes 中声明,以便全局应用它们。

请注意,Spring WebFlux 没有 Spring MVC ResponseEntityExceptionHandler的等效项,因为 WebFlux 仅引发ResponseStatusException(或其子类),并且不需要将它们转换为 HTTP 状态 code。

ControllerAdvice

与 Spring MVC 相同

通常,@ExceptionHandler@InitBinder@ModelAttribute方法适用于声明它们的@Controller class(或 class 层次结构)。如果您希望此类方法更全局地应用(跨控制器),则可以在标有@ControllerAdvice@RestControllerAdvice的 class 中声明它们。

@ControllerAdvice标有@Component,这意味着这样的 classes 可以通过component 扫描注册为 Spring beans。 @RestControllerAdvice也是 meta-annotation 标有@ControllerAdvice@ResponseBody,这实质上意味着通过消息转换(与视图解析器或模板渲染相比)将@ExceptionHandler方法呈现给响应主体。

在启动时,@RequestMapping和@ExceptionHandler方法的基础结构类检测带有@ControllerAdvice注释的Spring bean,然后在运行时应用它们的方法。全局@ExceptionHandler方法(来自@ControllerAdvice)被应用在本地方法(来自@Controller)之后。相比之下,全局@ModelAttribute和@InitBinder方法会在局部方法之前应用。

默认情况下,@ControllerAdvice方法适用于每个请求(即所有控制器),但您可以通过 annotation 上的属性将其缩小到控制器的子集,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
// 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 {}

前面的 selectors 在运行时进行评估,如果您广泛使用它们,可能会对 performance 产生负面影响。有关更多详细信息,请参阅@ControllerAdvice javadoc。

功能性Endpoints

Spring WebFlux 包含 WebFlux.fn,一个轻量级函数式编程 model,其中函数用于 route 和处理请求,contracts 是为不变性而设计的。它是 annotation-based 编程 model 的替代品,但是在相同的Reactive Core基础上运行。

概述

在 WebFlux.fn 中,HTTP 请求使用HandlerFunction:函数处理,该函数接受ServerRequest并返回延迟的ServerResponse(i.e .Mono<ServerResponse>)。作为响应 object 的请求都具有不可变的 contracts,它提供对 HTTP 请求和响应的 JDK 8-friendly 访问。 HandlerFunction相当于 annotation-based 编程 model 中@RequestMapping方法的主体。

传入的请求被路由到处理程序 function,其中RouterFunction:功能需要ServerRequest并返回延迟的HandlerFunction(i.e.Mono<HandlerFunction>)。当 router function 匹配时,返回一个 handler function;否则是一个空的单声道。 RouterFunction相当于@RequestMapping annotation,但主要区别在于 router 函数不仅提供数据,还提供行为。

RouterFunctions.route()提供了一个便于创建 router 的 router 构建器,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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) {
// ...
}
}

运行RouterFunction的一种方法是将其转换为HttpHandler并通过 built-in 服务适配器之一安装它:

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

大多数 applications 可以通过 WebFlux Java configuration 运行,请参见运行服务器

HandlerFunction

ServerRequestServerResponse是不可变接口,提供对 HTTP 请求和响应的 JDK 8-friendly 访问。请求和响应都对身体流提供了Reactive Streams背压。请求体用 Reactor FluxMono表示。响应正文用任何 Reactive Streams Publisher表示,包括FluxMono。有关更多信息,请参阅Reactive Libraries

ServerRequest

ServerRequest提供对 HTTP 方法,URI,headers 和查询参数的访问,同时通过body方法提供对正文的访问。

以下 example 将请求正文提取到Mono<String>

1
Mono<String> string = request.bodyToMono(String.class);

以下 example 将主体提取到Flux<Person>,其中Person objects 从某些序列化形式(如 JSON 或 XML)解码:

1
Flux<Person> people = request.bodyToFlux(Person.class);

前面的示例是使用更通用的ServerRequest.body(BodyExtractor)的快捷方式,它接受BodyExtractor功能策略接口。实用程序 class BodyExtractors提供对许多实例的访问。例如,前面的例子也可以写成如下:

1
2
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

以下 example 显示了如何访问表单数据:

1
Mono<MultiValueMap<String, String> map = request.body(BodyExtractors.toFormData());

以下 example 显示了如何将 multipart 数据作为 map 访问:

1
Mono<MultiValueMap<String, Part> map = request.body(BodyExtractors.toMultipartData());

以下 example 显示了如何以流方式访问多个部分,一个在 time 时间:

1
Flux<Part> parts = request.body(BodyExtractos.toParts());

ServerResponse

ServerResponse提供对 HTTP 响应的访问,因为它是不可变的,所以您可以使用build方法来创建它。您可以使用构建器来设置响应状态,添加响应_header 或提供正文。以下 example 使用 JSON 内容创建 200(OK)响应:

1
2
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

以下 example 显示了如何使用Location标头 build 201(CREATED)响应并且没有正文:

1
2
URI location = ...
ServerResponse.created(location).build();

HandlerClasses

我们可以将 handler function 编写为 lambda,如下面的 example 所示:

1
2
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body(fromObject("Hello World"));

这很方便,但在 application 中我们需要多个函数,多个内联 lambda 可能会变得混乱。因此,将相关的处理函数组合成一个处理程序 class 是很有用的,它在 annotation-based application 中具有与@Controller类似的作用。对于 example,以下 class 公开 reactive Person repository:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.ServerResponse.ok;
import static org.springframework.web.reactive.function.BodyInserters.fromObject;

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).body(fromObject(person)))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
1 listPeople是一个 handler function,它将 repository 中找到的所有Person objects 作为 JSON 返回。
2 createPerson是一个处理程序 function,它存储请求体中包含的新Person。请注意PersonRepository.savePerson(Person)返回Mono<Void>:一个空Mono,当该人从请求中读取并存储时,它会发出完成信号。因此,我们使用build(Publisher<Void>)方法在收到完成信号时(即保存Person时)发送响应。
3 getPerson是一个 handler function,它返回一个由id path 变量标识的人。我们从 repository 中检索Person并创建一个 JSON 响应(如果找到它)。如果找不到,我们使用switchIfEmpty(Mono<T>)来_retret 404 Not Found 响应。

RouterFunction

Router 函数用于将请求路由到相应的HandlerFunction。通常,您不会自己编写 router 函数,而是使用RouterFunctions utility class 上的方法创建一个。 RouterFunctions.route()(无参数)为 creating router function 提供 fluent 构建器,而RouterFunctions.route(RequestPredicate, HandlerFunction)提供了创建 router 的直接方法。

通常,建议使用route()构建器,因为它为典型的映射方案提供了方便的 short-cuts,而不需要 hard-to-discover 静态导入。例如,router function 构建器提供方法GET(String, HandlerFunction)来为 GET 请求创建映射;和POST(String, HandlerFunction)用于 POST。

除了 HTTP method-based 映射之外,route 构建器还提供了一种在映射到请求时引入其他谓词的方法。对于每个 HTTP 方法,都有一个重载变量,它将RequestPredicate作为参数,但可以表示其他约束。

断言

您可以编写自己的RequestPredicate,但RequestPredicates实用程序 class 根据请求路径,HTTP 方法,content-type 等提供常用的 implementations。以下 example 使用请求谓词来基于Accept标头创建约束:

1
2
3
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> Response.ok().body(fromObject("Hello World")));

您可以使用以下命令组合多个请求谓词:

  • RequestPredicate.and(RequestPredicate) - 两者都必须 match。
  • RequestPredicate.or(RequestPredicate) - 要么 match。

来自RequestPredicates的许多谓词都是由组成的。例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String)组成。上面显示的 example 也使用两个请求谓词,因为构建器在内部使用RequestPredicates.GET,并使用accept谓词组合它。

Routes

order 函数在 order 中计算:如果第一个 route 不匹配,则计算第二个,依此类推。因此,在通用之前声明更具体的 routes 是有意义的。请注意,此行为与 annotation-based programming model 不同,后者会自动选择“最具体”的控制器方法。

使用 router function 构建器时,所有已定义的 routes 都组成一个从build()返回的RouterFunction。还有其他方法可以组合多个 router 函数:

  • RouterFunctions.route()构建器上add(RouterFunction)
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - 嵌套RouterFunctions.route()RouterFunction.and()快捷方式。

以下 example 显示了四个 routes 的组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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是在其他地方创建的 router function,并添加到构建的 route 中。

嵌套Routes

对于一组 router 函数来说,共享谓词(例如共享路径)是 common。在上面的 example 中,共享谓词将是与/person匹配的路径谓词,由三个 routes 使用。使用 annotations 时,您可以使用 maps 到/person的 type-level @RequestMapping 注释来删除此重复项。在 WebFlux.fn 中,可以通过 router function 构建器上的path方法共享路径谓词。例如,通过使用嵌套的 routes,可以通过以下方式改进上述 example 的最后几行行:

1
2
3
4
5
6
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();

请注意,path的第二个参数是一个 consumer,它接受一个 router 构建器。

尽管 path-based 嵌套是最常见的,但您可以使用构建器上的nest方法嵌套在任何类型的谓词上。以上仍然包含共享Accept -header 谓词形式的一些重复。我们可以通过nest方法和accept一起使用来进一步改进:

1
2
3
4
5
6
7
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();

运行服务器

你如何在 HTTP 服务器中运行 router function?一个简单的选项是使用以下方法之一将 router function 转换为HttpHandler

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

然后,您可以使用返回的HttpHandler与多个服务器适配器一起使用HttpHandler来执行 server-specific 指令。

Spring Boot 也使用的一个更典型的选项是通过WebFlux 配置运行DispatcherHandler -based,它使用 Spring configuration 来声明 process 请求所需的组件。 WebFlux Java configuration 声明以下基础结构组件以支持功能 endpoints:

  • RouterFunctionMapping:在 Spring configuration 中检测一个或多个RouterFunction<?> beans,通过RouterFunction.andOther和 routes 请求将它们组合到生成的组合RouterFunction中。
  • HandlerFunctionAdapter:简单的适配器,允许DispatcherHandler调用映射到请求的HandlerFunction
  • ServerResponseResultHandler:通过调用ServerResponsewriteTo方法处理调用HandlerFunction的结果。

前面的组件允许函数 endpoints 适合DispatcherHandler请求处理生命周期,并且(可能)与带注释的控制器并行运行(如果有的话)。它也是 Spring Boot WebFlux starter 启用功能 endpoints 的方式。

以下 example 显示了 WebFlux Java configuration(有关如何运行它,请参阅DispatcherHandler):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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...
}
}

过滤处理程序函数

您可以使用 routing function 构建器上的beforeafterfilter方法过滤处理函数。使用 annotations,您可以使用@ControllerAdviceServletFilter或两者来实现类似的功能。过滤器将应用于构建器构建的所有 routes。这意味着嵌套的 routes 中定义的过滤器不适用于“top-level”routes。例如,请考虑以下 example:

1
2
3
4
5
6
7
8
9
10
11
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 routes。
2 记录响应的after过滤器将应用于所有 routes,包括嵌套的 routes。

router 构建器上的filter方法采用HandlerFilterFunction:函数,它接受ServerRequestHandlerFunction并返回ServerResponse。 handler function 参数表示链中的下一个元素。这通常是路由到的处理程序,但如果应用了多个,它也可以是另一个过滤器。

现在我们可以为 route 添加一个简单的安全过滤器,假设我们有SecurityManager可以确定是否允许特定路径。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();

前面的 example 演示了调用next.handle(ServerRequest)是可选的。我们只允许在允许访问时执行 handler function。

除了在 router function 构建器上使用filter方法之外,还可以通过RouterFunction.filter(HandlerFilterFunction)将过滤器应用于现有的 router function。

功能 endpoints 的 CORS 支持通过专用的CorsWebFilter提供。

URI链接

与 Spring MVC 相同

本节介绍 Spring Framework 中可用于准备 URI 的各种选项。

UriComponents

Spring MVC 和 Spring WebFlux

UriComponentsBuilder有助于使用变量从 URI 模板中构建 URI,如下面的 example 所示:

1
2
3
4
5
6
7
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("http://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 Build a UriComponents
5 展开变量并获取URI

前面的 example 可以合并为一个链并用buildAndExpand缩短,如下面的 example 所示:

1
2
3
4
5
6
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();

您可以通过直接转到 URI(这意味着编码)来进一步缩短它,如下面的 example 所示:

1
2
3
4
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");

您可以使用完整的 URI 模板进一步缩短它,如下面的 example 所示:

1
2
3
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");

UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder实现UriBuilder。您可以使用UriBuilderFactory创建UriBuilderUriBuilderFactoryUriBuilder一起提供了一种可插入机制,可以根据共享配置(例如基本 URL,编码首选项和其他详细信息)从 URI 模板中构建 URI。

您可以使用UriBuilderFactory配置RestTemplateWebClient以自定义 URI 的准备。 DefaultUriBuilderFactoryUriBuilderFactory的默认_impleration,它在内部使用UriComponentsBuilder并公开共享 configuration 选项。

以下 example 显示了如何配置RestTemplate

1
2
3
4
5
6
7
8
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

以下 example 配置WebClient

1
2
3
4
5
6
7
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

此外,您还可以直接使用DefaultUriBuilderFactory。它类似于使用UriComponentsBuilder但是,它不是静态工厂方法,而是一个包含 configuration 和 preferences 的实际实例,如下面的 example 所示:

1
2
3
4
5
6
String baseUrl = "http://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");

URI编码

Spring MVC 和 Spring WebFlux

UriComponentsBuilder在两个级别公开编码选项:

这两个选项都使用转义的八位字节替换 non-ASCII 和非法字符。但是,第一个选项还会替换出现在 URI 变量中的保留含义的字符。

考虑“;”,这在路径中是合法的但具有保留意义。第一个选项取代“;”在 URI 变量中使用“%3B”但在 URI 模板中没有。相比之下,第二个选项永远不会替换“;”,因为它是路径中的合法字符。

对于大多数情况,第一个选项可能会给出预期结果,因为它将 URI 变量视为完全编码的不透明数据,而选项 2 仅在 URI 变量故意包含保留字符时才有用。

以下 example 使用第一个选项:

1
2
3
4
5
6
7
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"

您可以通过直接转到 URI(这意味着编码)来缩短前面的 example,如下面的 example 所示:

1
2
3
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")

您可以使用完整的 URI 模板进一步缩短它,如下面的示例所示:

1
2
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")

WebClientRestTemplate通过UriBuilderFactory策略在内部扩展和编码 URI 模板。两者都可以配置自定义策略。如下例所示:

1
2
3
4
5
6
7
8
9
10
String baseUrl = "http://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();

DefaultUriBuilderFactory 实现在内部使用UriComponentsBuilder来扩展和编码 URI 模板。作为工厂,它提供了一个单独的位置来配置编码方法,基于以下编码模式之一:

  • TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode()(对应于前面列表中的第一个选项)pre-encode URI 模板,并在扩展时严格编码 URI 变量。
  • VALUES_ONLY:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过UriUtils#encodeUriUriVariables对 URI 变量应用严格编码。
  • URI_COMPONENTS:使用UriComponents#encode()(对应于前面列表中的第二个选项),在扩展 URI 变量后对 URI component value 进行编码。
  • NONE:未应用编码。

出于历史原因和向后兼容性,RestTemplate设置为EncodingMode.URI_COMPONENTSWebClient依赖于DefaultUriBuilderFactory中的默认 value,它在 5.0.x 中从EncodingMode.URI_COMPONENTS更改为 5.1 中的EncodingMode.TEMPLATE_AND_VALUES

CORS

与 Spring MVC 相同

Spring WebFlux 允许您处理 CORS(Cross-Origin 资源共享)。本节介绍如何执行此操作。

介绍

与 Spring MVC 相同

出于安全原因,浏览器禁止 AJAX calls 到当前源之外的资源。例如,您可以将您的银行帐户放在一个标签中,将 evil.com 放在另一个标签中。来自 evil.com 的脚本不应该使用您的凭据向您的银行 API 发出 AJAX 请求 - 例如,从您的帐户中提取资金!

Cross-Origin 资源共享(CORS)是由大多数浏览器实现的W3C 规范,它允许您指定授权的 cross-domain 请求类型,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的变通方法。

Processing

与 Spring MVC 相同

CORS 规范区分了预检,简单和实际请求。要了解 CORS 的工作原理,您可以阅读本文等许多其他内容,或者查看规范以获取更多详细信息。

Spring WebFlux HandlerMapping implementations 为 CORS 提供 built-in 支持。成功将请求映射到处理程序后,HandlerMapping检查给定请求和处理程序的 CORS configuration 并采取进一步操作。直接处理预检请求,同时拦截,验证简单和实际的 CORS 请求,并设置所需的 CORS 响应头。

在 order 中启用 cross-origin 请求(即,Origin标头存在且与请求的 host 不同),您需要有一些显式声明的 CORS configuration。如果未找到匹配的 CORS configuration,则拒绝预检请求。没有 CORS headers 被添加到简单和实际 CORS 请求的响应中,因此浏览器拒绝它们。

每个HandlerMapping可以_11单独使用 URL pattern-based CorsConfiguration映射。在大多数情况下,applications 使用 WebFlux Java configuration 来声明这样的映射,这会导致单个 global map 传递给所有HadlerMappping __mplement。

您可以将HandlerMapping level 上的 global CORS configuration 与更多 fine-grained,handler-level CORS configuration 结合使用。对于 example,带注释的控制器可以使用 class-或 method-level @CrossOrigin 注释(其他处理程序可以实现CorsConfigurationSource)。

组合 global 和 local configuration 的规则通常是加法的 - 例如,所有 global 和所有本地起源。对于只能接受单个 value 的属性,例如allowCredentialsmaxAge,本地会覆盖 global value。有关详细信息,请参阅CorsConfiguration#combine(CorsConfiguration)

要从源中了解更多信息或进行高级自定义,请参阅:

  • CorsConfiguration
  • CorsProcessorDefaultCorsProcessor
  • AbstractHandlerMapping

CrossOrigin注解

与 Spring MVC 相同

@CrossOrigin 注解在带注释的控制器方法上启用 cross-origin 请求,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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) {
// ...
}
}

默认情况下,@CrossOrigin允许:

  • 所有起源。
  • 所有 headers。
  • 控制器方法映射到的所有 HTTP 方法。

默认情况下不启用allowedCredentials,因为它会建立一个公开敏感 user-specific 信息的信任 level(例如 cookies 和 CSRF 令牌),并且只应在适当的时候使用。

maxAge设置为 30 分钟。

class level 也支持@CrossOrigin,并且由所有方法继承。以下 example 指定某个域并_set maxAge到一小时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@CrossOrigin(origins = "http://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) {
// ...
}
}

您可以在 class 和方法 level 中使用@CrossOrigin,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin("http://domain2.com") (2)
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
1 在 class level 上使用@CrossOrigin
2 在 level 方法中使用@CrossOrigin

全局配置

与 Spring MVC 相同

除了 fine-grained,controller method-level configuration 之外,您可能还想定义一些 global CORS configuration。您可以在任何HandlerMapping上单独设置 URL-based CorsConfiguration映射。但是,大多数 applications 都使用 WebFlux Java configuration 来实现这一点。

默认情况下 global configuration 启用以下内容:

  • 所有起源。
  • 所有 headers。
  • GETHEADPOST方法。

默认情况下不启用allowedCredentials,因为它会建立一个公开敏感 user-specific 信息的信任 level(例如 cookies 和 CSRF 令牌),并且只应在适当的时候使用。

maxAge设置为 30 分钟。

要在 WebFlux Java configuration 中启用 CORS,可以使用CorsRegistry回调,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);

// Add more mappings...
}
}

CORS过滤器

与 Spring MVC 相同

您可以通过 built-in CorsWebFilter应用 CORS 支持,这非常适合功能 endpoints

要配置过滤器,可以声明CorsWebFilter bean 并将CorsConfigurationSource传递给其构造函数,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
CorsWebFilter corsFilter() {

CorsConfiguration config = new CorsConfiguration();

// Possibly...
// config.applyPermitDefaultValues()

config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

return new CorsWebFilter(source);
}

Web安全

与 Spring MVC 相同

Spring Security项目支持保护 web applications 免受恶意攻击。请参阅 Spring Security reference 文档,包括:

View技术

与 Spring MVC 相同

在 Spring WebFlux 中使用视图技术是可插拔的。您是否决定使用 Thymeleaf,FreeMarker 或其他一些视图技术主要是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。我们假设您已经熟悉View解析器

Thymeleaf

与 Spring MVC 相同

Thymeleaf 是一个现代的 server-side Java 模板引擎,它强调自然的 HTML 模板,可以通过 double-clicking 在浏览器中预览,这对于 UI 模板的独立工作非常有用(例如,设计者),而不需要运行服务器。 Thymeleaf 提供了广泛的 features,并且积极开发和维护。有关更完整的介绍,请参阅Thymeleaf项目主页。

与 Spring WebFlux 的 Thymeleaf integration 由 Thymeleaf 项目管理。 configuration 涉及一些 bean 声明,例如SpringResourceTemplateResolverSpringWebFluxTemplateEngineThymeleafReactiveViewResolver。有关更多详细信息,请参阅Thymeleaf Spring和 WebFlux integration 公告

FreeMarker

与 Spring MVC 相同

Apache FreeMarker是一个模板引擎,用于生成从 HTML 到电子邮件和其他人的任何类型的文本输出。 Spring Framework 有一个 built-in integration 用于将 Spring WebFlux 与 FreeMarker 模板一起使用。

View配置

与 Spring MVC 相同

以下 example 显示了如何将 FreeMarker 配置为视图技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

您的模板需要存储在FreeMarkerConfigurer指定的目录中,如前面的 example 所示。给定前面的 configuration,如果控制器返回视图 name,welcome,解析器将查找classpath:/templates/freemarker/welcome.ftl模板。

FreeMarker配置

与 Spring MVC 相同

您可以通过在FreeMarkerConfigurer bean 上设置相应的 bean properties,将 FreeMarker 的“设置”和“SharedVariables”直接传递给 FreeMarker Configuration object(由 Spring 管理)。 freemarkerSettings property 需要java.util.Properties object,而freemarkerVariables property 需要java.util.Map。以下 example 显示了如何使用FreeMarkerConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

// ...

@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
Map<String, Object> variables = new HashMap<>();
variables.put("xml_escape", new XmlEscape());

FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
configurer.setFreemarkerVariables(variables);
return configurer;
}
}

有关适用于Configuration object 的设置和变量的详细信息,请参阅 FreeMarker 文档。

表单处理

与Spring MVC相同

Spring提供了用于jsp的标记库,其中包含< Spring:bind/>元素。该元素主要让表单显示来自表单支持对象的值,并显示来自web层或业务层验证器的失败验证的结果。在FreeMarker中,Spring也支持相同的功能,还提供了用于生成表单输入元素本身的方便宏。

绑定宏

Web MVC

在FreeMarker的spring-webflux.jar文件中维护了一组标准的宏,因此它们始终可用于经过适当配置的应用程序。

在Spring模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围,这使得所有宏对于调用代码和用户模板都是可见的。下面的部分只关注需要从模板中直接调用的宏。如果您希望直接查看宏代码,则该文件名为spring。在org.springframework.web.reactive.result.view.freemarker包中。

有关绑定支持的更多细节,请参见Spring MVC的简单绑定

表单宏

有关Spring对FreeMarker模板的表单宏支持的详细信息,请参阅Spring MVC文档的以下部分。

脚本视图

与 Spring MVC 相同

Spring Framework 有一个 built-in integration,可以使用 Spring WebFlux 和任何可以在JSR-223 Java 脚本引擎上运行的模板 library。以下 table 显示了我们在不同脚本引擎上测试的模板 libraries:

集成任何其他脚本引擎的基本规则是它必须实现ScriptEngineInvocable接口。

需求

与 Spring MVC 相同

您需要在 classpath 上安装脚本引擎,其详细信息因脚本引擎而异:

  • Nashorn JavaScript 引擎随 Java 8 一起提供。强烈建议使用最新的更新版本。
  • 应该添加JRuby作为 Ruby 支持的依赖项。
  • 应该添加Jython作为 Python 支持的依赖项。
  • 应为 Kotlin 脚本支持添加org.jetbrains.kotlin:kotlin-script-util依赖项和包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory line 的META-INF/services/javax.script.ScriptEngineFactory文件。有关详细信息,请参阅这个 example

你需要有脚本模板 library。对 Javascript 执行此操作的一种方法是通过WebJars

脚本模板

与 Spring MVC 相同

您可以声明ScriptTemplateConfigurer bean 来指定要使用的脚本引擎,要加载的脚本 files,要调用渲染模板的 function,等等。以下 example 使用 Mustache 模板和 Nashorn JavaScript 引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}
}

使用以下参数调用render function:

  • String template:模板内容
  • Map model:视图 model
  • RenderingContext renderingContextRenderingContext可以访问 application context,locale,模板加载器和 URL(自 5.0 以来)

Mustache.render()本身与此签名兼容,因此您可以直接调用它。

如果您的模板技术需要一些自定义,您可以提供实现自定义 render function 的脚本。例如,Handlerbars需要在使用它们之前编译模板,并且需要填充工具 in order 来模拟 server-side 脚本引擎中不可用的某些浏览器工具。以下 example 显示了如何设置自定义 render function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvc
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;
}
}

将 non-thread-safe property 设置为false是必需的,因为 non-thread-safe 脚本引擎的模板 libraries 不是为并发而设计的,例如 Handlebars 或 Nactorn 上的 React running。在这种情况下,由于这个 bug,需要 Java 8u60 或更高版本。

polyfill.js仅定义 Handlebars 正确运行所需的window object,如下面的代码段所示:

1
var window = {};

这个基本的render.js implementation 在使用之前编译模板。 production ready implementation 还应该 store 并重用缓存的模板或 pre-compiled 模板。这可以在脚本端完成,也可以完成所需的任何自定义(管理 example 的模板引擎 configuration)。以下 example 显示了如何编译模板:

1
2
3
4
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}

查看 Spring Framework 单元测试,Java资源,了解更多 configuration 示例。

JSON和XML

与 Spring MVC 相同

出于内容谈判目的,根据 client 请求的 content type,能够在使用 HTML 模板或其他格式(如 JSON 或 XML)呈现 model 之间进行交替是很有用的。为了支持这样做,Spring WebFlux 提供HttpMessageWriterView,您可以使用它来插入spring-web中的任何可用编解码器,例如Jackson2JsonEncoderJackson2SmileEncoderJaxb2XmlEncoder

与其他视图技术不同,HttpMessageWriterView不需要ViewResolver,而是配置作为默认视图。您可以配置一个或多个此类默认视图,包装不同的HttpMessageWriter实例或Encoder实例。匹配请求的 content type 的那个在运行时使用。

在大多数情况下,model 包含多个属性。要确定要序列化的序列,可以使用 model 属性的 name 配置HttpMessageWriterView以用于呈现。如果 model 只包含一个属性,则使用该属性。

HTTP缓存

与 Spring MVC 相同

HTTP 缓存可以显着改善 web application 的 performance。 HTTP 缓存围绕Cache-Control响应头和随后的条件请求 headers,例如Last-ModifiedETagCache-Control建议私有(对于 example,浏览器)和 public(对于 example,代理)缓存如何缓存和 re-use 响应。如果内容未更改,ETag标头用于生成条件请求,该请求可能导致 304(NOT_MODIFIED)没有正文。 ETag可以被视为Last-Modified标题的更复杂的继承者。

本节介绍 Spring WebFlux 中可用的 HTTP 缓存相关选项。

CacheControl

与 Spring MVC 相同

CacheControl支持配置与Cache-Control标头相关的设置,并且在许多地方被接受为参数:

虽然RFC 7234描述了Cache-Control响应头的所有可能的指令,但CacheControl类型采用了一种使用 case-oriented 方法,该方法专注于 common 场景,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
// 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();

控制器

与 Spring MVC 相同

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的lastModifiedETagvalue 需要先计算才能与条件请求 headers 进行比较。控制器可以将ETagCache-Control设置添加到ResponseEntity,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
@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);
}

如果与条件请求 headers 的比较表明内容未更改,则前面的 example 将使用空主体发送 304(NOT_MODIFIED)响应。否则,ETagCache-Control headers 将添加到响应中。

您还可以在控制器中对条件请求 headers 进行检查,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

long eTag = ... (1)

if (exchange.checkNotModified(eTag)) {
return null; (2)
}

model.addAttribute(...); (3)
return "myViewName";
}
1 Application-specific 计算。
2 响应已设置为 304(NOT_MODIFIED)。没有进一步处理。
3 继续请求处理。

有三种变体可用于检查针对eTag值,lastModified值或两者的条件请求。对于条件GETHEAD请求,您可以将响应设置为 304(NOT_MODIFIED)。对于条件POSTPUTDELETE,您可以将响应设置为 409(PRECONDITION_FAILED)以防止并发修改。

静态资源

与 Spring MVC 相同

您应该使用Cache-Control和条件响应 headers 为静态资源提供最佳的 performance。请参阅有关配置静态资源的部分。

WebFlux配置

与 Spring MVC 相同

WebFlux Java configuration 声明了使用带注释的控制器或函数 endpoints 处理请求所需的组件,并且它提供了一个 API 来自定义 configuration。这意味着您不需要了解 Java configuration 创建的底层 beans。但是,如果您想了解它们,可以在WebFluxConfigurationSupport中查看它们,或者阅读更多关于它们在特殊的 Bean 类型中的内容。

对于 configuration API 中未提供的更高级自定义,您可以通过高级配置模式完全控制 configuration。

启用WebFlux配置

与 Spring MVC 相同

您可以在 Java 配置中使用@EnableWebFlux annotation,如下面的 example 所示:

1
2
3
4
@Configuration
@EnableWebFlux
public class WebConfig {
}

前面的 example 注册了一些 Spring WebFlux 基础设施 beans并适应了 classpath 上可用的依赖项 - 适用于 JSON,XML 和其他。

WebFlux配置API

与 Spring MVC 相同

在 Java configuration 中,您可以实现WebFluxConfigurer接口,如下面的 example 所示:

1
2
3
4
5
6
7
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

// Implement configuration methods...

}

转换,格式化

与 Spring MVC 相同

默认情况下,会安装NumberDate类型的格式化程序,包括对@NumberFormat@DateTimeFormat 注释的支持。如果 class 路径中存在 Joda-Time,则还会安装对 Joda-Time 格式 library 的完全支持。

以下 example 显示了如何注册自定义格式化程序和转换器:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}

}

有关何时使用FormatterRegistrar implementations 的详细信息,请参阅FormatterRegistrar SPIFormattingConversionServiceFactoryBean

验证

与 Spring MVC 相同

默认情况下,如果 class 路径上存在Bean 验证(对于 example,Hibernate Validator),则LocalValidatorFactoryBean将注册为 global 验证器,以便在@Controller method arguments 上与@ValidValidated一起使用。

在 Java configuration 中,您可以自定义 global Validator实例,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public Validator getValidator(); {
// ...
}

}

请注意,您还可以在本地注册Validator implementations,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Controller
public class MyController {

@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}

}

如果需要在某处注入LocalValidatorFactoryBean,请创建 bean 并在 order 中使用@Primary标记它以避免与 MVC 配置中声明的冲突。

ContentType解析器

与 Spring MVC 相同

您可以配置 Spring WebFlux 如何从请求中确定@Controller实例的请求媒体类型。默认情况下,仅选中Accept标头,但您也可以启用查询 parameter-based 策略。

以下 example 显示了如何自定义请求的 content type 解析:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}

HTTP消息编解码器

与 Spring MVC 相同

以下 example 显示了如何自定义读取和写入请求和响应主体的方式:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// ...
}
}

ServerCodecConfigurer提供了一组默认的 readers 和 writers。您可以使用它来添加更多 readers 和 writers,自定义默认值,或完全替换默认值。

对于 Jackson JSON 和 XML,请考虑使用Jackson2ObjectMapperBuilder,它使用以下方法自定义 Jackson 的默认 properties:

如果在 classpath 上检测到以下 well-known 模块,它还会自动注册:

视图解析器

与 Spring MVC 相同

以下 example 显示了如何配置视图分辨率:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}

ViewResolverRegistry具有 Spring Framework 集成的视图技术的快捷方式。以下 example 使用 FreeMarker(还需要配置基础 FreeMarker 视图技术):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

您还可以插入任何ViewResolver implementation,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ViewResolver resolver = ... ;
registry.viewResolver(resolver);
}
}

要支持内容谈判并通过视图解析(除了 HTML)呈现其他格式,您可以基于HttpMessageWriterView 实现配置一个或多个默认视图,该视图接受来自spring-web的任何可用编解码器。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();

Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}

// ...
}

有关与 Spring WebFlux 集成的视图技术的更多信息,请参阅视图技术

静态资源

与 Spring MVC 相同

此选项提供了一种从资源 -based 位置列表中提供静态资源的便捷方法。

在下一个示例中,给定以/resources开头的请求,相对路径用于在 classpath 上查找和提供相对于/static的静态资源。资源的使用期限为 one-year,以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还会评估Last-Modified标头,如果存在,则返回304 status code。以下列表显示了 example:

1
2
3
4
5
6
7
8
9
10
11
12
@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));
}

}

资源处理程序还支持ResourceResolver 实现和ResourceTransformer 实现链,可用于创建工具链以使用优化的资源。

您可以根据从内容计算的 MD5 哈希,固定的 application version 或其他信息,将VersionResourceResolver用于版本化资源 URL。对于一些值得注意的 exceptions(例如与模块加载器一起使用的 JavaScript 资源),ContentVersionStrategy(MD5 哈希)是一个不错的选择。

以下 example 显示了如何在 Java configuration 中使用VersionResourceResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}

}

您可以使用ResourceUrlProvider来 rewrite URL 并应用完整的解析器和变换器链(对于 example,为 insert 版本)。 WebFlux configuration 提供ResourceUrlProvider,以便可以将其注入其他人。

与 Spring MVC 不同,目前,在 WebFlux 中,没有办法透明地编写静态资源 URL,因为没有可以使用 non-blocking 链解析器和变换器的视图技术。仅提供本地资源时,解决方法是直接使用ResourceUrlProvider(对于 example,通过自定义元素)和阻止。

请注意,当同时使用EncodedResourceResolver(对于 example,Gzip,Brotli 编码)和VersionedResourceResolver时,必须在该 order 中注册它们,以确保始终基于未编码的文件可靠地计算 content-based 版本。

WebJarsResourceResolver也通过WebJarsResourceResolver支持,并在 class 路径上存在org.webjars:webjars-locator时自动注册。解析器可以 re-write URL 包含 jar 的 version,也可以匹配到没有版本的传入 URL(对于 example,/jquery/jquery.min.js/jquery/1.2.0/jquery.min.js)。

路径匹配

与 Spring MVC 相同

您可以自定义与路径匹配相关的选项。有关各个选项的详细信息,请参阅PathMatchConfigurer javadoc。以下 example 显示了如何使用PathMatchConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}

}

Spring WebFlux 依赖于名为RequestPath的请求路径的解析表示,用于访问已解码的路径段值,并删除分号内容(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,您无需指示是否解码请求路径,也不需要删除分号内容以进行路径匹配。

Spring WebFlux 也不支持后缀 pattern 匹配,不像 Spring MVC,我们也不再依赖它。

高级配置模式

与 Spring MVC 相同

@EnableWebFlux进口DelegatingWebFluxConfiguration

  • 为 WebFlux applications 提供默认的 Spring configuration
  • 检测并委托WebFluxConfigurer implementations 来自定义 configuration。

对于高级模式,您可以删除@EnableWebFlux并直接从DelegatingWebFluxConfiguration扩展而不是实现WebFluxConfigurer,如下面的 example 所示:

1
2
3
4
5
6
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

// ...

}

您可以在WebConfig中保留现有方法,但现在您也可以从 base class 覆盖 bean 声明,并且在 classpath 上仍然有任意数量的其他WebMvcConfigurer 实现。

HTTP/2

与 Spring MVC 相同

Servlet 4 容器需要支持 HTTP/2,而 Spring Framework 5 与 Servlet API 兼容 4.从编程 model 的角度来看,没有特定的 applications 需要做的事情。但是,有一些与 server configuration 相关的注意事项。有关更多详细信息,请参阅HTTP/2 维基页面

目前,Spring WebFlux 不支持 HTTP/2 与 Netty。也没有支持以编程方式将资源推送到 client。

WebClient

Spring WebFlux 包含 reactive,non-blocking WebClient用于 HTTP 请求。 client 有一个函数式 fluent API,其中 reactive 类型用于声明性组合,请参阅Reactive Libraries。 WebFlux client 和服务器依赖相同的 non-blocking 编解码器来编码和解码请求和响应内容。

内部WebClient委托给 HTTP client library。默认情况下,它使用反应堆 Netty,Jetty reactive HtpClient有 built-in 支持,其他可以通过ClientHttpConnector插入。

配置

创建WebClient的最简单方法是通过一个静态工厂方法:

  • WebClient.create()
  • WebClient.create(String baseUrl)

上述方法使用 Reactor Netty HttpClient和默认设置,并期望io.projectreactor.netty:reactor-netty在 classpath 上。

您还可以将WebClient.builder()与其他选项一起使用:

  • uriBuilderFactory:自定义UriBuilderFactory以用作基本 URL。
  • 每个请求都defaultHeader:Headers。
  • defaultCookie:Cookies 为每个请求。
  • defaultRequestConsumer来自定义每个请求。
  • filter:Client 过滤每个请求。
  • exchangeStrategies:HTTP 消息 reader/writer 自定义。
  • clientConnector:HTTP client library 设置。

以下 example 配置HTTP 编解码器

1
2
3
4
5
6
7
8
9
ExchangeStrategies strategies = ExchangeStrategies.builder()
.codecs(configurer -> {
// ...
})
.build();

WebClient client = WebClient.builder()
.exchangeStrategies(strategies)
.build();

构建后,WebClient实例是不可变的。但是,您可以克隆它并 build 修改后的副本而不会影响原始实例,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
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

MaxInMemorySize

Spring WebFlux为在编解码器中缓冲内存中的数据设置了限制,以避免应用程序内存问题。默认情况下,它被配置为256KB,如果这对您的用例来说还不够,您将看到以下内容:

1
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

你可以用下面的代码样例在所有默认的编解码器上配置这个限制:

1
2
3
4
5
6
7
WebClient webClient = WebClient.builder()
.exchangeStrategies(builder ->
builder.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();

ReactorNetty

要自定义 Reactor Netty 设置,只需提供 pre-configured HttpClient

1
2
3
4
5
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();

资源

默认情况下,HttpClient参与reactor.netty.http.HttpResources中保存的 global Reactor Netty 资源,包括 event 循环线程和连接池。这是推荐的模式,因为固定的共享资源是 event 循环并发的首选。在这种模式下, global 资源保持 active,直到 process 退出。

如果服务器与 process 同步,则通常不需要显式关闭。但是,如果服务器可以启动或停止 in-process(对于示例,Spring MVC application 部署为 WAR),您可以使用globalResources=true(默认值)声明类型为ReactorResourceFactory的 Spring-managed bean,以确保关闭 Reactor Netty global 资源当 Spring ApplicationContext关闭时,如下面的 example 所示:

1
2
3
4
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}

您也可以选择不参与 global Reactor Netty 资源。但是,在此模式下,您需要确保所有 Reactor Netty client 和服务器实例都使用共享资源,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setGlobalResources(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 创建独立于 global 资源的资源。
2 ReactorClientHttpConnector构造函数与资源工厂一起使用。
3 将连接器插入WebClient.Builder

超时

配置连接超时:

1
2
3
4
5
import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));

配置读 and/or 写超时值:

1
2
3
4
5
6
7
8
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10))));

Jetty

以下 example 显示了如何自定义 Jetty HttpClient设置:

1
2
3
4
5
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

WebClient webClient = WebClient.builder().clientConnector(connector).build();

默认情况下,HttpClient创建自己的资源(ExecutorByteBufferPoolScheduler),这些资源在 process 退出或stop()被调用之前保持 active。

您可以在 Jetty client(和服务器)的多个实例之间共享资源,并通过声明类型JettyResourceFactory的 Spring-managed bean 来确保在关闭 Spring ApplicationContext时关闭资源,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

Consumer<HttpClient> customizer = client -> {
// Further customizations...
};

ClientHttpConnector connector =
new JettyClientHttpConnector(resourceFactory(), customizer); (1)

return WebClient.builder().clientConnector(connector).build(); (2)
}
1 JettyClientHttpConnector构造函数与资源工厂一起使用。
2 将连接器插入WebClient.Builder

retrieve()

retrieve()方法是获取响应主体并对其进行解码的最简单方法。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
WebClient client = WebClient.create("http://example.org");

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);

您还可以获取从响应中解码的 objects 流,如下面的示例所示:

1
2
3
4
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);

默认情况下,具有 4xx 或 5xx 状态代码的响应会导致WebClientResponseException或其某个 HTTP 状态特定 sub-classes,例如WebClientResponseException.BadRequestWebClientResponseException.NotFound等。您还可以使用onStatus方法自定义生成的 exception,如下面的 example 所示:

1
2
3
4
5
6
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxServerError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);

当使用onStatus时,如果预期响应具有内容,则onStatus回调应该使用它。如果没有,内容将自动耗尽,以确保释放资源。

exchange()

exchange()方法提供比retrieve方法更多的控制。以下 example 等同于retrieve(),但也提供对ClientResponse的访问:

1
2
3
4
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));

在此 level 中,您还可以创建一个完整的ResponseEntity

1
2
3
4
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));

请注意(与retrieve()不同),exchange(),4xx 和 5xx 响应没有自动错误信号。您必须检查状态 code 并决定如何继续。

使用exchange()时,必须始终使用ClientResponse的任何bodytoEntity方法来确保释放资源并避免 HTTP 连接池的潜在问题。如果没有预期的响应内容,您可以使用bodyToMono(Void.class)。但是,如果响应确实包含内容,则连接将关闭,并且不会放回池中。

请求体

请求正文可以从Object编码,如下面的示例所示:

1
2
3
4
5
6
7
8
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);

您还可以编写一个 objects 流,如下面的 example 所示:

1
2
3
4
5
6
7
8
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);

或者,如果您有实际的 value,则可以使用syncBody快捷方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
Person person = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.syncBody(person)
.retrieve()
.bodyToMono(Void.class);

表单数据

要发送表单数据,您可以提供MultiValueMap<String, String>作为正文。请注意,FormHttpMessageWriter会自动将内容设置为application/x-www-form-urlencoded。以下 example 显示了如何使用MultiValueMap<String, String>

1
2
3
4
5
6
7
MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
.uri("/path", id)
.syncBody(formData)
.retrieve()
.bodyToMono(Void.class);

您还可以使用BodyInserters提供表单数据 in-line,如下面的 example 所示:

1
2
3
4
5
6
7
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);

Multipart数据

要发送 multipart 数据,您需要提供MultiValueMap<String, ?>,其值为表示部件内容的Object实例或表示部件的内容和_header 的HttpEntity实例。 MultipartBodyBuilder提供了一个方便的 API 来准备 multipart 请求。以下 example 显示了如何创建MultiValueMap<String, ?>

1
2
3
4
5
6
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));

MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,您不必为每个零件指定Content-Type。 content type 是根据所选的HttpMessageWriter自动确定的,以便序列化它,如果是Resource,则根据文件扩展名自动确定。如有必要,您可以通过其中一个重载的构建器part方法显式提供MediaType以用于每个部件。

准备好MultiValueMap后,将其传递给WebClient的最简单方法是通过syncBody方法,如下面的 example 所示:

1
2
3
4
5
6
7
MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
.uri("/path", id)
.syncBody(builder.build())
.retrieve()
.bodyToMono(Void.class);

如果MultiValueMap包含至少一个非String value,它也可以表示常规表单数据(即application/x-www-form-urlencoded),则无需将Content-Type设置为multipart/form-data。使用MultipartBodyBuilder时始终如此,这确保了HttpEntity wrapper。

作为MultipartBodyBuilder的替代,您还可以通过 built-in BodyInserters提供 multipart 内容 inline-style,如下面的 example 所示:

1
2
3
4
5
6
7
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);

Client过滤器

您可以通过WebClient.Builder in order 注册 client 过滤器(ExchangeFilterFunction)来拦截和修改请求,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
WebClient client = WebClient.builder()
.filter((request, next) -> {

ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();

return next.exchange(filtered);
})
.build();

这可用于 cross-cutting 问题,例如身份验证。以下 example 使用过滤器通过静态工厂方法进行基本身份验证:

1
2
3
4
5
// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();

过滤器全局应用于每个请求。要更改特定请求的过滤器行为,您可以向ClientRequest添加请求属性,然后链中的所有过滤器都可以访问这些属性,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();

client.get().uri("http://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);

}

您还可以复制现有的WebClient,插入新过滤器,或删除已注册的过滤器。以下 example,在索引 0 处插入基本身份验证筛选器:

1
2
3
4
5
6
7
// static import of ExchangeFilterFunctions.basicAuthentication

WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();

同步使用

通过在结果末尾进行阻塞,可以以同步方式使用WebClient:

1
2
3
4
5
6
7
8
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();
1
2
3
4
5
6
7
8
9
10
val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}

val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}

但是,如果需要进行多次通话,则可以避免单独阻止每个响应,而等待合并的结果,这样会更有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
1
2
3
4
5
6
7
8
9
10
11
12
13
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())
}

以上仅是一个示例。 还有许多其他模式和运算符可用于构建响应式管道,该管道可进行许多远程调用(可能是嵌套的,相互依赖的),而不会阻塞到最后。

Note

使用FluxMono,您永远不必阻塞Spring MVC或Spring WebFlux控制器。 只需从controller方法返回返回的反应类型。 相同的原则适用于Kotlin Coroutines和Spring WebFlux,只需在控制器方法中使用暂停功能或返回Flow即可。

测试

要测试使用WebClient的 code,可以使用 mock web 服务器,例如OkHttp MockWebServer。要查看其使用的 example,请查看 Spring Framework 测试套件中的WebClientIntegrationTests或 OkHttp repository 中的static-server sample。

WebSockets

与 Servlet 堆栈中的相同

reference 文档的这一部分涵盖了对 reactive-stack WebSocket 消息传递的支持。

WebSocket简介

WebSocket 协议RFC 6455提供了一种标准方法,可以通过单个 TCP 连接在 client 和服务器之间建立 full-duplex,two-way 通信 channel。它是来自 HTTP 的不同 TCP 协议,但设计为使用端口 80 和 443 并允许 re-use 现有防火墙规则通过 HTTP 工作。

WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用 HTTP Upgrade标头进行升级,或者在这种情况下,切换到 WebSocket 协议。以下 example 显示了这样的交互:

1
2
3
4
5
6
7
8
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 状态 code:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 协议切换

成功握手后,HTTP 升级请求所基于的 TCP socket 将保持打开状态,以便 client 和服务器继续发送和接收消息。

有关 WebSockets 如何工作的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节,或者 Web 上的任何介绍和教程。

请注意,如果 WebSocket 服务器在 web 服务器(e.g. nginx)后面运行,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果 application 在云环境中运行,请检查与 WebSocket 支持相关的云提供程序的说明。

HTTP与WebSocket

尽管 WebSocket 被设计为 HTTP-compatible 并以 HTTP 请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和 application 编程模型。

在 HTTP 和 REST 中,application 被建模为多个 URL。要与 application 进行交互,clients 访问这些 URL,request-response 样式。 Servers 根据 HTTP URL,方法和 headers 将请求路由到相应的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有 application 消息都在同一 TCP 连接上流动。这指向一个完全不同的异步,event-driven,messaging architecture。

WebSocket 也是一种 low-level 传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非 client 和服务器就消息语义达成一致,否则无法对路由进行 route 或 process。

WebSocket clients 和服务器可以通过 HTTP 握手请求上的Sec-WebSocket-Protocol标头协商使用 higher-level,消息传递协议(对于 example,STOMP)。如果没有,他们需要提出自己的惯例。

何时使用WebSockets

WebSockets可以使网页具有动态性和交互性。但是,在许多情况下,结合使用Ajax和HTTP流或长时间轮询可以提供一种简单有效的解决方案。

例如,新闻,邮件和社交订阅源需要动态更新,但是每隔几分钟这样做是完全可以的。另一方面,协作,游戏和金融应用程序需要更接近实时。

仅延迟并不是决定因素。如果消息量相对较少(例如,监视网络故障),则HTTP流或轮询可以提供有效的解决方案。低延迟,高频率和高音量的结合才是使用WebSocket的最佳案例。

还请记住,在Internet上,控件之外的限制性代理可能会阻止WebSocket交互,这可能是因为未将它们配置为传递Upgrade标头,或者是因为它们关闭了长期处于空闲状态的连接。这意味着与面向公众的应用程序相比,将WebSocket用于防火墙内部的应用程序是一个更直接的决定。

WebSocketAPI

与 Servlet 堆栈中的相同

Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的 client-和 server-side applications。

服务器

与 Servlet 堆栈中的相同

要创建 WebSocket 服务器,可以先创建WebSocketHandler。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
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) {
// ...
}
}

然后,您可以将其映射到 URL 并添加WebSocketHandlerAdapter,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
static class WebConfig {

@Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/path", new MyWebSocketHandler());

SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1); // before annotated controllers
return mapping;
}

@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}

WebSocketHandler

WebSocketHandlerhandle方法接受WebSocketSession并返回Mono<Void>以指示 session 的 application 处理何时完成。 session 通过两个流处理,一个用于入站消息,一个用于出站消息。以下 table 描述了处理流的两种方法:

WebSocketSession方法 描述
Flux<WebSocketMessage> receive() 提供对入站消息流的访问,并在关闭连接时完成。
Mono<Void> send(Publisher<WebSocketMessage>) 获取传出消息的源,写入消息,并返回在源完成并写入完成时完成的Mono<Void>

WebSocketHandler必须将入站和出站流组成一个统一的流,并返回反映该流完成的Mono<Void>。根据 application 要求,统一流程在以下情况下完成:

  • 入站或出站消息流完成。
  • 入站流完成(即连接已关闭),而出站流是无限的。
  • 在选定的点上,通过WebSocketSessionclose方法。

当入站和出站消息流组合在一起时,无需检查连接是否打开,因为 Reactive Streams 信号终止活动。入站流接收完成或错误信号,并且出站流接收取消信号。

处理程序的最基本的 implementation 是处理入站流的。以下 example 显示了这样的 implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>

对于嵌套的异步操作,您可能需要在使用池数据缓冲区的基础服务器上调用message.retain()(对于 example, Netty)。否则,可能在您有机会读取数据之前释放数据缓冲区。有关更多背景信息,请参阅数据缓冲区和编解码器

以下 implementation 组合了入站和出站流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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>

入站和出站流可以是独立的,只有完成才能连接,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 加入流并返回,当流结束时完成。

DataBuffer

DataBuffer是 WebFlux 中字节缓冲区的表示。 reference 的 Spring 核心部分在数据缓冲区和编解码器部分有更多内容。要理解的 key 要点是在像 Netty 这样的服务器上,字节缓冲区被池化并且 reference 计数,并且必须在被消耗时释放以避免 memory 泄漏。

当在 Netty 上运行时,applications 必须使用DataBufferUtils.retain(dataBuffer),如果他们希望保持 order 中的输入数据缓冲区以确保它们不被释放,并且随后在消耗缓冲区时使用DataBufferUtils.release(dataBuffer)

信号交换

与 Servlet 堆栈中的相同

WebSocketHandlerAdapter委托给WebSocketService。默认情况下,这是HandshakeWebSocketService的一个实例,它对 WebSocket 请求执行基本检查,然后对正在使用的服务器使用RequestUpgradeStrategy。目前,对于 Reactor Netty,Tomcat,Jetty 和 Undertow 有 built-in 支持。

HandshakeWebSocketService公开sessionAttributePredicate property,允许设置Predicate<String>WebSession中提取属性,并将它们插入到WebSocketSession的属性中。

服务器配置

与 Servlet 堆栈中的相同

每个服务器的RequestUpgradeStrategy公开了可用于底层 WebSocket 引擎的 WebSocket-related configuration 选项。在 Tomcat 上 running 时,以下 example sets WebSocket 选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
static 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);
}
}

检查服务器的升级策略以查看可用的选项。目前,只有 Tomcat 和 Jetty 公开了这样的选项。

CORS

与 Servlet 堆栈中的相同

配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让WebSocketHandler实现CorsConfigurationSource和@如果您不能这样做,您还可以在SimpleUrlHandler上设置corsConfigurations property 以通过 URL pattern 指定 CORS 设置。如果同时指定了两者,则使用CorsConfiguration上的combine方法将它们组合在一起。

Client

Spring WebFlux 为 Reactor Netty,Tomcat,Jetty,Undertow 和标准 Java(即 JSR-356)提供WebSocketClient抽象和 implementations。

Tomcat client 实际上是标准 Java 的扩展,在WebSocketSession处理中具有一些额外的功能,以利用 Tomcat-specific API 来暂停接收消息以获得背压。

要启动 WebSocket session,您可以创建 client 的实例并使用其execute方法:

1
2
3
4
5
6
7
WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());

某些客户端(例如 Jetty)实现Lifecycle,需要先停止并启动它们才能使用它们。所有 clients 都具有与底层 WebSocket client 的 configuration 相关的构造函数选项。

Testing

在 Spring MVC 中也是如此

spring-test模块提供了ServerHttpRequestServerHttpResponseServerWebExchange的 mock implementations。有关 mock objects 的讨论,请参阅Spring Web Reactive

WebTestClient建立在这些 mock 请求和响应 objects 的基础上,以支持在没有 HTTP 服务器的情况下测试 WebFlux applications。您也可以使用WebTestClient进行 end-to-end integration 测试。

RSocket

本节描述了Spring Framework对RSocket协议的支持。

总览

RSocket是一种应用协议,用于使用以下交互模型之一通过TCP,WebSocket和其他字节流传输进行多路复用,双工通信:

  • Request-Response -发送一条消息并收到一条回信。

  • Request-Stream-发送一条消息并接收回来的消息流。

  • Channel-双向发送消息流。

  • Fire-and-Forget—发送单向消息。

一旦建立了初始连接,由于双方变得对称,并且双方都可以发起上述交互之一,因此“客户端”与“服务器”的区别将消失。 这就是为什么在协议中将参与方称为“请求者”和“响应者”,而将上述交互称为“请求流”或简称为“请求”的原因。

这些是RSocket协议的关键功能和优势:

  • 跨网络边界的Reactive Streams语义-对于诸如Request-StreamChannel之类的流请求,背压信号在请求者和响应者之间传播,从而允许请求者放慢源处的响应者的速度,从而减少了对网络层拥塞控制的依赖,以及 需要在网络级别或任何级别进行缓冲。
  • 请求限制-该功能在LEASE帧之后被称为“租赁”,可以从两端发送以限制给定时间内另一端允许的请求总数。 租约会定期更新。
  • 会话恢复-这是为断开连接而设计的,需要维护某些状态。 状态管理对于应用程序是透明的,并且可以与背压结合使用,从而可以在可能的情况下停止生产者并减少所需的状态量。
  • 大邮件的碎片化和重组。
  • Keepalive(心跳)。

RSocket具有多种语言的实现Java库基于Project ReactorReactor Netty进行传输。 这意味着来自应用程序中的Reactive Streams Publishers的信号通过RSocket在网络上透明地传播。

协议

RSocket的优点之一是它在线路上具有定义明确的行为,并且易于阅读的规范以及某些协议扩展。因此,独立于语言实现和更高级别的框架API,阅读规范是一个好主意。本节提供简要概述,以建立一些上下文。

连接中

最初,客户端通过一些低级流传输(例如TCP或WebSocket)连接到服务器,然后将SETUP帧发送到服务器以设置连接参数。

服务器可以拒绝SETUP帧,但是通常在发送(对于客户端)和接收(对于服务器)之后,双方都可以开始发出请求,除非SETUP指示使用租赁语义来限制请求的数量。在这种情况下,双方都必须等待另一端的LEASE帧以允许发出请求。

发出请求

一旦建立连接,双方就可以通过帧REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNELREQUEST_FNF中的一个来发起请求。这些帧中的每一个都将一条消息从请求者传送到响应者。

然后,响应者可以返回带有响应消息的PAYLOAD帧,并且在REQUEST_CHANNEL的情况下,请求者还可以发送带有更多请求消息的PAYLOAD帧。

当请求涉及消息流(例如Request-StreamChannel)时,响应者必须遵守来自请求者的需求信号。需求表示为许多消息。初始需求在REQUEST_STREAM和REQUEST_CHANNEL帧中指定。后续需求通过REQUEST_N帧发出信号。

每一端还可以通过METADATA_PUSH帧发送元数据通知,该元数据通知与任何单独的请求无关,而与整个连接有关。

讯息格式

RSocket消息包含数据和元数据。元数据可用于发送路由,安全令牌等。数据和元数据的格式可以不同。每个MIME类型都在SETUP框架中声明,并应用于给定连接上的所有请求。

尽管所有消息都可以具有元数据,但是通常每个请求都包含诸如路由之类的元数据,因此仅包含在请求的第一条消息中,即与帧REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNELREQUEST_FNF中的一个一起使用。

协议扩展定义了用于应用程序的通用元数据格式:

  • 复合元数据-多个独立格式化的元数据条目。
  • 路由-请求的路由。

Java实现

RSocket的Java实现基于Project Reactor构建。 TCP和WebSocket的传输建立在Reactor Netty上。作为反应流库,Reactor简化了实现协议的工作。对于应用程序,自然而然的选择是将Flux和Mono与声明性运算符和透明背压支持一起使用。

RSocket Java中的API故意是最小且基本的。它着重于协议功能,并将应用程序编程模型(例如RPC代码生成与其他)作为更高层次的独立关注点。

主合同io.rsocket.RSocket对四种请求交互类型进行建模,其中Mono表示单个消息的承诺,Flux表示消息流,而io.rsocket.Payload通过访问实际数据消息来访问数据和元数据作为字节缓冲区。 RSocket合同是对称使用的。对于请求,将为应用程序提供一个RSocket来执行请求。为了响应,应用程序实现了RSocket来处理请求。

这并不意味着要进行全面介绍。在大多数情况下,Spring应用程序将不必直接使用其API。但是,独立于Spring查看或试验RSocket可能很重要。 RSocket Java存储库包含许多示例应用程序,以演示其API和协议功能。

Spring支持

spring-messaging模块包含以下内容:

  • RSocketRequester-一种流式API,可通过io.rsocket.RSocket进行请求,并进行数据和元数据编码/解码。
  • 带注解的响应程序— — @MessageMapping用于响应的带注释的处理程序方法。

spring-web模块包含RSocket应用程序可能需要的EncoderDecoder实现,例如Jackson CBOR / JSON和Protobuf。它还包含PathPatternParser,可以将其插入以进行有效的路由匹配。

Spring Boot 2.2支持通过TCP或WebSocket站立RSocket服务器,包括在WebFlux服务器中通过WebSocket公开RSocket的选项。 RSocketRequester.Builder和RSocketStrategies也有客户端支持和自动配置。有关更多详细信息,请参见Spring Boot参考中的RSocket部分

Spring Security 5.2提供了RSocket支持。

Spring Integration 5.2提供了入站和出站网关以与RSocket客户端和服务器进行交互。有关更多详细信息,请参见《 Spring Integration参考手册》。

Spring Cloud Gateway支持RSocket连接。

RSocketRequester

RSocketRequester提供了一个流畅的API来执行RSocket请求,接受和返回数据和元数据的对象,而不是底层数据缓冲区。 它可以对称地用于从客户端发出请求和从服务器发出请求。

Client Requester

要在客户端获得RSocketRequester,需要连接到服务器,同时准备并发送初始RSocket SETUP帧。 RSocketRequester为此提供了一个生成器。 在内部,它基于io.rsocket.core.RSocketConnector

这是使用默认设置进行连接的最基本方法:

1
2
3
4
5
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectTcp("localhost", 7000);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.connectWebSocket(URI.create("https://example.org:8080/rsocket"));
1
2
3
4
5
6
7
8
import org.springframework.messaging.rsocket.connectTcpAndAwait
import org.springframework.messaging.rsocket.connectWebSocketAndAwait

val requester = RSocketRequester.builder()
.connectTcpAndAwait("localhost", 7000)

val requester = RSocketRequester.builder()
.connectWebSocketAndAwait(URI.create("https://example.org:8080/rsocket"))

以上是推迟的。 实际连接和使用请求者:

1
2
3
4
5
6
7
8
9
10
// Connect asynchronously
RSocketRequester.builder().connectTcp("localhost", 7000)
.subscribe(requester -> {
// ...
});

// Or block
RSocketRequester requester = RSocketRequester.builder()
.connectTcp("localhost", 7000)
.block(Duration.ofSeconds(5));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Connect asynchronously
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

private var requester: RSocketRequester? = null

private suspend fun requester() = requester ?:
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000).also { requester = it }

suspend fun doSomething() = requester().route(...)
}

// Or block
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

private val requester = runBlocking {
RSocketRequester.builder().connectTcpAndAwait("localhost", 7000)
}

suspend fun doSomething() = requester.route(...)
}

连接设置

RSocketRequester.Builder提供了以下自定义初始SETUP框架的功能:

  • dataMimeType(MimeType)-设置连接数据的mime类型。
  • metadataMimeType(MimeType)-设置连接上元数据的mime类型。
  • setupData(Object)-要包含在SETUP中的数据。
  • setupRoute(String,Object …))-在元数据中路由以包含在SETUP中。
  • setupMetadata(Object,MimeType)-其他要包含在SETUP中的元数据。

对于数据,默认的mime类型是从第一个配置的Decoder派生的。对于元数据,默认的mime类型是复合元数据,它允许每个请求有多个元数据值和mime类型对。通常,两者都不需要更改。

SETUP框架中的数据和元数据是可选的。在服务器端,@ConnectMapping方法可用于处理连接的开始和SETUP框架的内容。元数据可用于连接级别的安全性。

策略

RSocketRequester.Builder接受RSocketStrategies来配置请求者。您需要使用它来提供编码器和解码器,以对数据和元数据值进行(反)序列化。默认情况下,仅注册spring-core中用于Stringbyte[]ByteBuffer的基本编解码器。添加spring-web可以访问更多可以注册的内容,如下所示:

1
2
3
4
5
6
7
8
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcp("localhost", 7000);
1
2
3
4
5
6
7
8
9
10
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()

val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.connectTcpAndAwait("localhost", 7000)

RSocketStrategies设计为可重复使用。 在某些情况下,例如 客户和服务器在同一应用程序中,最好在Spring配置中声明它。

Client Responders

RSocketRequester.Builder可用于配置响应器以响应来自服务器的请求。

您可以根据服务器上使用的相同基础结构,使用带注释的处理程序进行客户端响应,但需要通过程序注册,如下所示:

1
2
3
4
5
6
7
8
9
10
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();

SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.connectTcp("localhost", 7000);
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
.routeMatcher(PathPatternRouteMatcher()) 1
.build()

val responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); 2

val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(responder) } 3
.connectTcpAndAwait("localhost", 7000)
  1. 如果存在spring-web,请使用PathPatternRouteMatcher,以提高效率
    路线匹配。
  2. 使用@MessageMaping和/或@ConnectMapping方法从类中创建响应者。
  3. 注册响应者。

请注意,以上只是设计用于客户端响应程序的程序化注册的快捷方式。 对于客户端响应者处于Spring配置的替代方案,您仍然可以将RSocketMessageHandler声明为Spring bean,然后按以下方式应用:

1
2
3
4
5
6
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.connectTcp("localhost", 7000);
1
2
3
4
5
6
7
8
9
import org.springframework.beans.factory.getBean
import org.springframework.messaging.rsocket.connectTcpAndAwait

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.connectTcpAndAwait("localhost", 7000)

对于上述情况,您可能还需要使用RSocketMessageHandler中的setHandlerPredicate来切换到用于检测客户端响应程序的其他策略,例如 基于自定义注释,例如@RSocketClientResponder与默认@Controller。 在客户端和服务器或同一应用程序中有多个客户端的情况下,这是必需的。

有关编程模型的更多信息,请参见带注解的响应者

高级

RSocketRequesterBuilder提供了一个回调,以公开底层的io.rsocket.core.RSocketConnector,以提供有关keepalive间隔,会话恢复,拦截器等的更多配置选项。 您可以按以下方式在该级别上配置选项:

1
2
3
4
5
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.connectTcp("localhost", 7000);
1
2
3
4
5
6
import org.springframework.messaging.rsocket.connectTcpAndAwait

val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}.connectTcpAndAwait("localhost", 7000)

Server Requester

从服务器向连接的客户端发出请求是从服务器获取连接客户端的请求者的问题。

带注解的响应者中,@ConnectMapping@MessageMapping方法支持RSocketRequester参数。 使用它来访问连接的请求者。 请记住,@ConnectMapping方法本质上是SETUP框架的处理程序,必须在请求开始之前对其进行处理。 因此,一开始的请求必须与处理分离。 例如:

1
2
3
4
5
6
7
8
9
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { 1
// ...
});
return ... 2
}
1
2
3
4
5
6
7
8
9
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { 1
// ...
}
}
/// ... 2
}
  1. 独立于处理,异步启动请求。
  2. 在挂起功能中执行处理。

    Requests

    有了客户端或服务器请求者后,可以按以下方式发出请求:
1
2
3
4
5
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") 1
.data(viewBox) 2
.retrieveFlux(AirportLocation.class); 3
1
2
3
4
5
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)方法还接受任何Reactive Streams Publisher,包括FluxMono,以及在ReactiveAdapterRegistry中注册的任何其他值的生产者。 对于产生相同类型值的多值发布器(例如Flux),请考虑使用一种重载数据方法,以避免对每个元素进行类型检查和编码器查找:

1
2
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);

data(Object)步骤是可选的。 跳过不发送数据的请求:

1
2
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
.retrieveMono(AirportLocation.class);
1
2
3
4
import org.springframework.messaging.rsocket.retrieveAndAwait

val location = requester.route("find.radar.EWR")
.retrieveAndAwait<AirportLocation>()

如果使用复合元数据(默认设置)并且注册的编码器支持这些值,则可以添加额外的元数据值。 例如:

1
2
3
4
5
6
7
8
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);
1
2
3
4
5
6
7
8
9
10
11
12
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>()

对于“即发即弃”,请使用返回Mono <Void>send()方法。 请注意,Mono仅指示消息已成功发送,而不是已被处理。

带注解的响应者

RSocket响应器可以实现为@MessageMapping@ConnectMapping方法。 @MessageMapping方法处理单个请求,而@ConnectMapping方法处理连接级事件(设置和元数据推送)。 对称支持带注释的响应者,用于从服务器端响应和从客户端端响应。

服务器响应者

要在服务器端使用带注释的响应者,请将RSocketMessageHandler添加到您的Spring配置中,以使用@MessageMapping和@ConnectMapping方法检测@Controller Bean:

1
2
3
4
5
6
7
8
9
10
@Configuration
static class ServerConfig {

@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.routeMatcher(new PathPatternRouteMatcher());
return handler;
}
}
1
2
3
4
5
6
7
8
@Configuration
class ServerConfig {

@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
routeMatcher = PathPatternRouteMatcher()
}
}

然后通过Java RSocket API启动RSocket服务器,并为响应者插入RSocketMessageHandler,如下所示:

1
2
3
4
5
6
7
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.block();
1
2
3
4
5
6
7
8
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val server = RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.awaitFirst()

默认情况下,RSocketMessageHandler支持复合路由元数据。 如果需要切换到其他mime类型或注册其他元数据mime类型,则可以设置其MetadataExtractor

您需要设置支持元数据和数据格式所需的EncoderDecoder实例。 您可能需要spring-web模块来实现编解码器。

默认情况下,SimpleRouteMatcher用于通过AntPathMatcher匹配路由。 我们建议从spring-web插入PathPatternRouteMatcher,以实现有效的路由匹配。 RSocket路由可以是分层的,但不是URL路径。 两个路由匹配器都配置为使用“。” 默认为分隔符,并且没有HTTP网址那样的URL解码。

RSocketMessageHandler可以通过RSocketStrategies进行配置,如果您需要在同一过程中在客户端和服务器之间共享配置,这可能会很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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()
}

Client Responders

需要在RSocketRequester.Builder中配置客户端的带注解的响应者。 有关详细信息,请参阅客户端响应程序

@MessageMapping

服务器客户端响应器配置到位后,可以按以下方式使用@MessageMapping方法:

1
2
3
4
5
6
7
8
@Controller
public class RadarsController {

@MessageMapping("locate.radars.within")
public Flux<AirportLocation> radars(MapRequest request) {
// ...
}
}
1
2
3
4
5
6
7
8
@Controller
class RadarsController {

@MessageMapping("locate.radars.within")
fun radars(request: MapRequest): Flow<AirportLocation> {
// ...
}
}

上面的@MessageMapping方法响应具有路由“ locate.radars.within”的请求-流交互。 它支持灵活的方法签名,并可以选择使用以下方法参数:

Method Argument Description
@Payload 请求的有效负载。 这可以是诸如Mono或Flux之类的异步类型的具体值。

注意:注释的使用是可选的。 并非简单类型并且不是其他任何受支持参数的方法参数都假定为预期的有效负载。
RSocketRequester 向远端发出请求的请求者。
@DestinationVariable 根据映射模式中的变量从路线提取的值,例如 @MessageMapping(“ find.radar。{id}”)。
@Header MetadataExtractor中所述注册要提取的元数据值。
@Headers Map<String, Object> MetadataExtractor中所述,注册所有用于提取的元数据值。

返回值应该是一个或多个要序列化为响应有效负载的对象。 可以是诸如Mono或Flux的异步类型,具体值,也可以是void或无值的异步类型,例如Mono<Void>

@MessageMapping方法支持的RSocket交互类型由输入(即@Payload参数)和输出的基数确定,其中基数表示以下内容:

基数 Description
1 Either an explicit value, or a single-value asynchronous type such as Mono<T>.
Many A multi-value asynchronous type such as Flux<T>.
0 For input this means the method does not have an @Payload argument.For output this is void or a no-value asynchronous type such as Mono<Void>.

下表显示了所有输入和输出基数组合以及相应的交互类型:

Input Cardinality Output Cardinality Interaction Types
0, 1 0 Fire-and-Forget, Request-Response
0, 1 1 Request-Response
0, 1 Many Request-Stream
Many 0, 1, Many Request-Channel

@ConnectMapping

@ConnectMapping在RSocket连接开始时处理SETUP帧,并通过METADATA_PUSH帧(即io.rsocket.RSocket中的metadataPush(Payload))处理任何后续的元数据推送通知。

@ConnectMapping方法支持与@MessageMapping相同的参数,但是基于SETUPMETADATA_PUSH帧中的元数据和数据。 @ConnectMapping可以具有将处理范围缩小到元数据中具有路由的特定连接的模式,或者,如果未声明任何模式,则所有连接都匹配。

@ConnectMapping方法无法返回数据,必须使用voidMono<Void>作为返回值进行声明。 如果处理为新连接返回错误,则连接被拒绝。 不得阻止向RSocketRequester发出连接请求的处理。 有关详细信息,请参见服务器请求程序

MetadataExtractor

响应者必须解释元数据。 复合元数据允许独立格式化的元数据值(例如,用于路由,安全性和跟踪),每个元数据值都有自己的mime类型。 应用程序需要一种配置要支持的元数据MIME类型的方法,以及一种访问提取值的方法。

MetadataExtractor是一种协议,用于获取序列化的元数据并返回解码的名称/值对,然后可以按名称对标题进行访问,例如通过带注释的处理程序方法中的@Header进行访问。

可以给DefaultMetadataExtractor解码器实例以解码元数据。 开箱即用,它具有对“message/x.rsocket.routing.v0”的内置支持,它可以解码为String并保存在“ route”键下。 对于任何其他mime类型,您需要提供一个Decoder并注册mime类型,如下所示:

1
2
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
1
2
3
4
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")

复合元数据很好地结合了独立的元数据值。 但是,请求者可能不支持复合元数据,或者可以选择不使用它。 为此,DefaultMetadataExtractor可能需要自定义逻辑以将解码后的值映射到输出映射。 这是将JSON用于元数据的示例:

1
2
3
4
5
6
7
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
MimeType.valueOf("application/vnd.myapp.metadata+json"),
new ParameterizedTypeReference<Map<String,String>>() {},
(jsonMap, outputMap) -> {
outputMap.putAll(jsonMap);
});
1
2
3
4
5
6
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)
}

通过RSocketStrategies配置MetadataExtractor时,可以让RSocketStrategies.Builder使用配置的解码器创建提取器,并简单地使用回调自定义注册,如下所示:

1
2
3
4
5
6
RSocketStrategies strategies = RSocketStrategies.builder()
.metadataExtractorRegistry(registry -> {
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
// ...
})
.build();
1
2
3
4
5
6
7
8
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
.metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
registry.metadataToExtract<Foo>(fooMimeType, "foo")
// ...
}
.build()

Reactive库

spring-webflux依赖于reactor-core并在内部使用它来组成异步逻辑并提供 Reactive Streams 支持。通常,WebFlux API return FluxMono(因为它们在内部使用)并且宽松地接受任何 Reactive Streams Publisher implementation 作为输入。 FluxMono的使用很重要,因为它有助于表达基数 - 例如,是否需要单个或多个异步值,这对于做出决策是必不可少的(例如,在编码或解码 HTTP 消息时)。

对于带注释的控制器,WebFlux 透明地适应 application 选择的 reactive library。这是在ReactiveAdapterRegistry的帮助下完成的,它为 reactive library 和其他异步类型提供了可插入的支持。注册表具有 built-in 支持 RxJava 和CompletableFuture,但您也可以注册其他人。

对于功能 API(例如功能 EndpointsWebClient和其他),WebFlux API 的一般规则适用 - FluxMono作为 return 值,Reactive Streams Publisher作为输入。当提供Publisher(无论是自定义还是来自另一个 reactive library)时,它只能被视为具有未知语义的流(0..N)。但是,如果语义已知,则可以使用FluxMono.from(Publisher)包装它,而不是传递原始Publisher

对于 example,给定不是Mono,Jackson JSON 消息 writer 需要多个值。如果媒体类型意味着无限流(对于 example,application/json+stream),则会单独写入和刷新值。否则,将值缓冲到列表中并呈现为 JSON array。