Web on Reactive Stack
文檔的此部分涵蓋對基於 Reactive Streams API構建的反應堆Web應用程序的支持,該API可在非阻塞服務器,例如Netty,Undertow和Servlet 3.1+容器。各個章節涵蓋了Spring WebFlux框架,響應式WebClient,對測試的支持以及響應式庫。對於Servlet堆棧Web應用程序,請參閱Servlet堆棧上的Web。
1. Spring WebFlux
Spring框架中包含的原始Web框架Spring Web MVC是專門為Servlet API和Servlet容器而構建的。反應性堆棧Web框架Spring WebFlux在更高版本5.0中添加。它是完全無阻塞的,支持 Reactive Streams 背壓,並且可以在Netty,Undertow和Servlet 3.1+容器等服務器上運行。
這兩個Web框架都反映了其源模塊的名稱(spring-webmvc和spring-webflux),並在Spring框架中並存。每個模塊都是可選的。應用程序可以使用一個模塊或另一個模塊,或者在某些情況下同時使用這兩個模塊,例如,帶有響應式WebClient的Spring MVC控制器。
1.1. Overview
為什么創建Spring WebFlux?
一部分答案是需要一個非阻塞式的Web堆棧來處理少量線程的並發並使用更少的硬件資源進行擴展。Servlet 3.1確實提供了用於非阻塞I / O的API。但是,使用它會導致Servlet API的其余部分偏離,在這些API中,合同是同步的(Filter,Servlet)或阻塞的(getParameter,getPart)。這是促使新的通用API成為所有非阻塞運行時的基礎的動機。這很重要,因為服務器(例如Netty)在異步,非阻塞空間中已建立良好。
另一部分答案是函數式編程,就像在Java 5中添加注釋會創造機會(例如帶注釋的REST控制器或單元測試)一樣,在Java 8中添加lambda表達式也會為Java中的功能API創造機會。這對於無阻塞的應用程序和延續樣式的API(如CompletableFuture和ReactiveX所流行)的好處是,它們允許以聲明方式構成異步邏輯。在編程模型級別,Java 8使Spring WebFlux能夠與帶注釋的控制器一起提供功能性的Web端點。
1.1.1. Define “Reactive”
我們談到了“非阻塞”和“功能性”,但是反應式意味着什么?
術語“反應性”是指圍繞對更改作出反應而構建的編程模型-網絡組件對I / O事件做出反應,UI控制器對鼠標事件做出反應等。從這個意義上說,非阻塞是反應性的,因為我們現在正處於操作完成或數據可用時對通知進行反應的方式,而不是被阻塞。
我們Spring團隊還有另一個重要機制與“反應性”相關聯,這是非阻塞背壓的機制。在同步命令式代碼中,阻塞調用是背壓的自然形式,它迫使調用者等待。在非阻塞代碼中,控制事件的速率很重要,這樣快速的生產者就不會擊垮消費者。
Reactive Streams是一個小的規范(在Java 9中也采用了),它定義了帶有反壓力的異步組件之間的交互。例如,數據庫(充當發布者)可以生成數據,然后HTTP服務器(充當訂閱者)可以將其寫入響應。Reactive Streams的主要目的是讓訂閱者控制發布者生成數據的速度或速度。
反應流的目的僅僅是建立機制和邊界。如果發布者無法放慢速度,則必須決定是緩沖,刪除還是失敗。
1.1.2. Reactive API
反應流對於互操作性起着重要作用。庫和基礎結構組件對此很感興趣,但由於它太底層了,因此它不適合用作應用程序API。應用程序需要更高級別且功能更豐富的API來構成異步邏輯,這與Java 8 Stream API相似,但不僅適用於集合。這就是反應式庫發揮的作用。
Reactor是Spring WebFlux的首選反應庫。它通過與ReactiveX運算符詞匯對齊的豐富運算符集,提供了Mono和Flux API類型,以處理0..1(Mono)和0..N(Flux)的數據序列。Reactor是Reactive Streams庫,因此,它的所有運算符都支持無阻塞背壓。Reactor非常注重服務器端Java。它是與Spring緊密合作開發的。
WebFlux要求Reactor作為核心依賴項,但它可以通過Reactive Streams與其他反應式庫互操作。通常,WebFlux API接受普通的Publisher作為輸入,在內部將其適應於Reactor類型,使用它,然后返回Flux或Mono作為輸出。因此,您可以將任何發布服務器作為輸入傳遞,並且可以對輸出應用操作,但是您需要調整輸出以與另一個反應庫一起使用。只要可行(例如,帶注釋的控制器),WebFlux就會透明地適應RxJava或其他反應式庫的使用。有關更多詳細信息,請參見反應式類庫。
1.1.3. Programming Models
spring-web模塊包含Spring WebFlux基礎的反應式基礎,包括HTTP抽象,用於支持的服務器的Reactive Streams適配器,編解碼器,以及與Servlet API類似但具有非阻塞合同的核心WebHandler API。
在此基礎上,Spring WebFlux提供了兩種編程模型的選擇:
- 帶注釋的控制器:與Spring MVC一致,並基於來自spring-web模塊的相同注釋。Spring MVC和WebFlux控制器都支持反應式(Reactor和RxJava)返回類型,因此,區分它們並不容易。一個顯着的區別是WebFlux還支持反應式@RequestBody參數。
- 功能端點:基於Lambda的輕量級功能編程模型。您可以將其視為一個小型庫或一組實用程序,應用程序可以使用它們來路由和處理請求。帶注釋的控制器的最大區別在於,應用程序負責從頭到尾的請求處理,而不是通過注釋聲明意圖並被回調。
1.1.4. Applicability
Spring MVC還是WebFlux?
這是一個很自然的問題,但卻建立了一個不合理的二分法。實際上,兩者共同努力擴大了可用選項的范圍。兩者的設計旨在實現彼此的連續性和一致性,它們可以並行使用,並且雙方的反饋對雙方都有利。下圖顯示了兩者之間的關系,它們的共同點以及各自的獨特支持:
我們建議您考慮以下幾點:
- 如果您有運行正常的Spring MVC應用程序,則無需更改。命令式編程是編寫,理解和調試代碼的最簡單方法。您有最大的庫選擇空間,因為從歷史上看,大多數庫都是阻塞的。
- 如果您已經在選擇非阻塞的Web技術棧,Spring WebFlux可以提供與該領域其他服務器相同的執行模型優勢,還可以選擇服務器(Netty,Tomcat,Jetty,Undertow和Servlet 3.1+容器),選擇編程模型(帶注釋的控制器和功能性Web端點),以及選擇反應式庫(Reactor,RxJava或其他)。
- 如果您對與Java 8 lambda或Kotlin一起使用的輕量級功能性Web框架感興趣,則可以使用Spring WebFlux功能性Web端點。對於要求較低復雜性的較小應用程序或微服務(可以受益於更高的透明度和控制)而言,這也是一個不錯的選擇。
- 在微服務架構中,您可以混合使用帶有Spring MVC或Spring WebFlux控制器或帶有Spring WebFlux功能端點的應用程序。在兩個框架中都支持相同的基於注釋的編程模型,這使得重用知識變得更加容易,同時還為正確的工作選擇了正確的工具。
- 評估應用程序的一種簡單方法是檢查其依賴關系。如果您要使用阻塞性持久性API(JPA,JDBC)或網絡API,則Spring MVC至少是常見體系結構的最佳選擇。使用Reactor和RxJava在單獨的線程上執行阻塞調用在技術上都是可行的,但是您不會充分利用非阻塞Web堆棧。
- 如果您的Spring MVC應用程序具有對遠程服務的調用,請嘗試響應式WebClient。您可以直接從Spring MVC控制器方法返回反應類型(Reactor,RxJava或其他)。每個調用的等待時間或調用之間的相互依賴性越大,好處就越明顯。Spring MVC控制器也可以調用其他反應式組件。
- 如果您有龐大的團隊,請牢記向非阻塞,功能性和聲明性編程的過渡過程中的學習曲線很陡。一種無需完全切換即可開始的實用方法是使用反應式WebClient。除此之外,從小處着手並衡量收益。我們希望對於廣泛的應用而言,這種轉變是不必要的。如果不確定要尋找什么好處,請先了解無阻塞I / O的工作原理(例如,單線程Node.js上的並發性)及其影響。
1.1.5. Servers
Tomcat,Jetty,Servlet 3.1+容器以及非Servlet運行時(例如Netty和Undertow)都支持Spring WebFlux。所有服務器都適應於低級通用API,因此可以跨服務器支持更高級別的編程模型。
Spring WebFlux不具有內置支持來啟動或停止服務器。但是,從Spring配置和WebFlux基礎結構組裝應用程序並用幾行代碼運行它很容易。
Spring Boot具有一個WebFlux啟動器,可以自動執行這些步驟。默認情況下,入門者使用Netty,但是通過更改Maven或Gradle依賴關系,可以輕松切換到Tomcat,Jetty或Undertow。Spring Boot默認為Netty,因為它在異步,非阻塞空間中得到更廣泛的使用,並允許客戶端和服務器共享資源。
Tomcat和Jetty可以與Spring MVC和WebFlux一起使用。但是請記住,它們的使用方式非常不同。Spring MVC依靠Servlet阻塞I / O,並允許應用程序在需要時直接使用Servlet API。 Spring WebFlux依賴Servlet 3.1非阻塞I / O,並在低級適配器后面使用Servlet API。請勿將其直接使用。
對於Undertow,Spring WebFlux直接使用Undertow API,而無需使用Servlet API。
1.1.6. Performance
性能最優說服力,反應和非阻塞不一定會使應用程序運行得更快。在某些情況下,它們可以(例如,如果使用WebClient並行運行遠程調用)。總體而言,以非阻塞方式進行處理需要更多的工作,這可能會稍微增加所需的處理時間。
反應性和非阻塞性的主要預期好處是能夠以較少的固定數量的線程和較少的內存進行擴展。這使應用程序在負載下更具彈性,因為它們以更可預測的方式擴展。但是,為了觀察這些好處,您需要有一些延遲(包括緩慢的和不可預測的網絡I / O的混合)。這就是反應堆開始顯示其優勢的地方,差異可能很大。
1.1.7. Concurrency Model
Spring MVC和Spring WebFlux都支持帶注釋的控制器,但是並發模型和默認的阻塞和線程假設存在關鍵差異。
在Spring MVC(通常是Servlet應用程序)中,假定應用程序可以阻塞當前線程(例如,用於遠程調用)。因此,Servlet容器使用大線程池來吸收請求處理期間的潛在阻塞。
在Spring WebFlux(通常是非阻塞服務器)中,假定應用程序未阻塞。因此,非阻塞服務器使用固定大小的小型線程池(事件循環工作器)來處理請求。
調用阻塞API
如果確實需要使用阻塞庫怎么辦? Reactor和RxJava都提供了publishOn運算符以繼續在其他線程上進行處理。這意味着容易逃生。但是請記住,阻塞式API不適用於此並發模型。
可變狀態
在Reactor和RxJava中,您可以通過運算符聲明邏輯。在運行時,會形成一個反應式管道,其中在不同的階段依次處理數據。這樣做的主要好處是,它使應用程序不必保護可變狀態,因為該管道中的應用程序代碼永遠不會被並發調用。
線程模型
您期望在運行Spring WebFlux的服務器上看到哪些線程?
- 在“原始” Spring WebFlux服務器上(例如,沒有數據訪問權限或其他可選依賴項),您可以期望該服務器有一個線程,而其他幾個線程則可以進行請求處理(通常與CPU核心數量一樣多)。但是,Servlet容器可能以更多線程開始(例如,Tomcat上為10),以支持Servlet(阻塞)I / O和Servlet 3.1(非阻塞)I / O使用。
- 反應式的“ WebClient”以事件循環的方式運行。因此,您會看到與之相關的固定數量的處理線程(例如,帶有Reactor Netty連接器的
reactor-http-nio-
)。但是,如果客戶端和服務器都使用Reactor Netty,則默認情況下,兩者共享事件循環資源。 - Reactor和RxJava提供了稱為調度程序的線程池抽象,以與publishOn運算符配合使用,該運算符用於將處理切換到其他線程池。調度程序具有建議特定並發策略的名稱-例如,“並行”(對於具有有限數量的線程的CPU綁定工作)或“彈性”(對於具有大量線程的I / O綁定)。如果看到這樣的線程,則意味着某些代碼正在使用特定的線程池“ Scheduler”策略。
- 數據訪問庫和其他第三方依賴性也可以創建和使用自己的線程。
配置
Spring框架不提供啟動和停止服務器的支持。要為服務器配置線程模型,您需要使用服務器特定的配置API,或者,如果使用Spring Boot,請檢查每個服務器的Spring Boot配置選項。您可以直接配置WebClient。對於所有其他庫,請參閱其各自的文檔。
1.2. Reactive Core
spring-web模塊包含以下對響應式Web應用程序的基礎支持:
- 對於服務器請求處理,有兩個級別的支持。
- HttpHandler:使用非阻塞I / O和Reactive Streams背壓進行HTTP請求處理的基本協定,以及Reactor Netty,Undertow,Tomcat,Jetty和任何Servlet 3.1+容器的適配器。
- WebHandler API:稍高級別的通用Web API,用於處理請求,在此之上構建了具體的編程模型,例如帶注釋的控制器和功能端點。
- 對於客戶端,有一個基本的ClientHttpConnector協定,以執行具有非阻塞I / O和響應流反壓力的HTTP請求,以及用於Reactor Netty,響應式Jetty HttpClient和Apache HttpComponents的適配器。應用程序中使用的更高級別的WebClient基於此基本協定。
- 對於客戶端和服務器,編解碼器用於HTTP請求和響應內容的序列化和反序列化。
1.2.1. HttpHandler
HttpHandler是具有一個用於處理請求和響應的單一方法的簡單協定。它是有意的最小化,其主要且唯一的目的是成為對不同HTTP服務器API的最小化抽象。
下表描述了受支持的服務器API:
Server name | Server API used | Reactive Streams support |
---|---|---|
Netty | Netty API | Reactor Netty |
Undertow | Undertow API | spring-web: Undertow to Reactive Streams bridge |
Tomcat | Servlet 3.1 non-blocking I/O; Tomcat API to read and write ByteBuffers vs byte[] | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
Jetty | Servlet 3.1 non-blocking I/O; Jetty API to write ByteBuffers vs byte[] | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
Servlet 3.1 container | Servlet 3.1 non-blocking I/O | spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge |
下表描述了服務器依賴性(另請參閱受支持的版本):
Server name | Group id | Artifact name |
---|---|---|
Reactor Netty | io.projectreactor.netty | reactor-netty |
Undertow | io.undertow | undertow-core |
Tomcat | org.apache.tomcat.embed | tomcat-embed-core |
Jetty | org.eclipse.jetty | jetty-server, jetty-servlet |
下面的代碼段顯示了對每個服務器API使用HttpHandler適配器的情況:
Reactor Netty
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
Tomcat
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
Jetty
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
Servlet 3.1+ Container
要將其作為WAR部署到任何Servlet 3.1+容器,您可以擴展WAR並將其包括在AbstractReactiveWebInitializer中。該類使用ServletHttpHandlerAdapter包裝HttpHandler並將其注冊為Servlet。
1.2.2. WebHandler
API
org.springframework.web.server包建立在HttpHandler契約的基礎上,以提供通用的Web API,以通過多個WebExceptionHandler,多個WebFilter和單個WebHandler組件的鏈來處理請求。通過簡單地指向自動檢測組件的Spring ApplicationContext和/或通過向構建器注冊組件,可以將該鏈與WebHttpHandlerBuilder放在一起。
盡管HttpHandler的目標很簡單,即抽象化不同HTTP服務器的使用,但WebHandler API的目的是提供Web應用程序中常用的更廣泛的功能集,例如:
- 具有屬性的用戶會話。
- 請求屬性。
- 請求的解析的語言環境或主體。
- 訪問已解析和緩存的表單數據。
- 多部分數據的抽象。
- 和更多
Special bean types
下表列出了WebHttpHandlerBuilder可以在Spring ApplicationContext中自動檢測的組件,或者可以直接向其注冊的組件:
Bean name | Bean type | Count | Description |
---|---|---|---|
|
WebExceptionHandler |
0..N | Provide handling for exceptions from the chain of WebFilter instances and the target WebHandler . For more details, see Exceptions. |
|
WebFilter |
0..N | Apply interception style logic to before and after the rest of the filter chain and the target WebHandler . For more details, see Filters. |
webHandler |
WebHandler |
1 | The handler for the request. |
webSessionManager |
WebSessionManager |
0..1 | The manager for WebSession instances exposed through a method on ServerWebExchange . DefaultWebSessionManager by default. |
serverCodecConfigurer |
ServerCodecConfigurer |
0..1 | For access to HttpMessageReader instances for parsing form data and multipart data that is then exposed through methods on ServerWebExchange . ServerCodecConfigurer.create() by default. |
localeContextResolver |
LocaleContextResolver |
0..1 | The resolver for LocaleContext exposed through a method on ServerWebExchange . AcceptHeaderLocaleContextResolver by default. |
forwardedHeaderTransformer |
ForwardedHeaderTransformer |
0..1 | For processing forwarded type headers, either by extracting and removing them or by removing them only. Not used by default. |
Form Data
ServerWebExchange公開了以下訪問表單數據的方法:
Mono<MultiValueMap<String, String>> getFormData();
DefaultServerWebExchange使用配置的HttpMessageReader將表單數據(application / x-www-form-urlencoded)解析為MultiValueMap。默認情況下,FormHttpMessageReader配置為由ServerCodecConfigurer Bean使用(請參閱Web Handler API)。
Multipart Data
ServerWebExchange公開了以下訪問 multipart 數據的方法:
Mono<MultiValueMap<String, Part>> getMultipartData();
DefaultServerWebExchange使用配置的HttpMessageReader >將multipart / form-data內容解析為MultiValueMap。默認情況下,這是DefaultPartHttpMessageReader,它沒有任何第三方依賴性。或者,可以使用基於Synchronoss NIO Multipart庫的SynchronossPartHttpMessageReader。兩者都是通過ServerCodecConfigurer bean進行配置的(請參閱Web Handler API)。
要以流方式解析多部分數據,可以使用從HttpMessageReader 返回的Flux 。例如,在帶注釋的控制器中,使用@RequestPart意味着按名稱對各個部分進行類似於Map的訪問,因此需要完整地解析多部分數據。相比之下,您可以使用@RequestBody將內容解碼為Flux 而不收集到MultiValueMap。
Forwarded Headers
當請求通過代理(例如負載平衡器)進行處理時,主機,端口和方案可能會更改。從客戶端的角度來看,創建指向正確主機,端口和方案的鏈接是一個挑戰。
RFC 7239定義了代理可以用來提供有關原始請求的信息的HTTP轉發頭。還有其他非標准標頭,包括X-Forwarded-Host,X-Forwarded-Port,X-Forwarded-Proto,X-Forwarded-Ssl和X-Forwarded-Prefix。
ForwardedHeaderTransformer是一個組件,可根據轉發的標頭修改請求的主機,端口和方案,然后刪除這些標頭。如果將其聲明為名稱為forwardedHeaderTransformer的bean,它將被檢測到並使用。
對於轉發的標頭,存在安全方面的考慮,因為應用程序無法知道標頭是由代理添加的,還是由惡意客戶端添加的。這就是為什么應配置信任邊界處的代理以刪除來自外部的不受信任的轉發流量的原因。您還可以使用removeOnly = true配置ForwardedHeaderTransformer,在這種情況下,它將刪除但不使用標頭。
1.2.3. Filters
在WebHandler API中,可以使用WebFilter在其余過濾器和目標WebHandler的其余處理鏈之前和之后應用攔截樣式的邏輯。使用WebFlux Config時,注冊WebFilter就像將其聲明為Spring bean一樣簡單,並且(可選)通過在bean聲明上使用@Order或實現Ordered來表達優先級。
CORS
Spring WebFlux通過控制器上的注釋為CORS配置提供了細粒度的支持。但是,當您將其與Spring Security結合使用時,我們建議您使用內置的CorsFilter,該產品必須在Spring Security的過濾器鏈之前訂購。
1.2.4. Exceptions
在WebHandler API中,可以使用WebExceptionHandler來處理WebFilter實例鏈和目標WebHandler鏈中的異常。使用WebFlux Config時,注冊WebExceptionHandler就像將其聲明為Spring bean一樣簡單,並且(可選)通過在bean聲明上使用@Order或實現Ordered來表達優先級。
下表描述了可用的WebExceptionHandler實現:
Exception Handler | Description |
---|---|
ResponseStatusExceptionHandler |
通過將響應設置為異常的HTTP狀態代碼,提供對ResponseStatusException類型的異常的處理。 |
WebFluxResponseStatusExceptionHandler |
ResponseStatusExceptionHandler的擴展,它也可以確定任何異常上@ResponseStatus批注的HTTP狀態代碼。該處理程序在WebFlux Config中聲明。 |
1.2.5. Codecs
spring-web和spring-core模塊提供支持,通過具有Reactive Streams背壓的非阻塞I / O,可以在高級對象之間來回串行化字節內容。以下介紹了此支持:
- 編碼器和解碼器是底層協議,用於獨立於HTTP編碼和解碼內容。
- HttpMessageReader和HttpMessageWriter是對HTTP消息內容進行編碼和解碼的協定。
- 可以使用EncoderHttpMessageWriter來包裝Encoder,以使其適合在Web應用程序中使用,而可以使用DecoderHttpMessageReader來包裝Decoder。
- DataBuffer抽象了不同的字節緩沖區表示形式(例如Netty ByteBuf,java.nio.ByteBuffer等),並且是所有編解碼器都在處理的內容。有關此主題的更多信息,請參見“ Spring核心”部分中的數據緩沖區和編解碼器。
spring-core模塊提供byte [],ByteBuffer,DataBuffer,Resource和String編碼器和解碼器實現。 spring-web模塊提供了Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers和其他編碼器和解碼器,以及僅Web的HTTP消息讀取器和寫入器實現,用於表單數據,多部分內容,服務器發送的事件等。
ClientCodecConfigurer和ServerCodecConfigurer通常用於配置和自定義要在應用程序中使用的編解碼器。請參閱有關配置HTTP消息編解碼器的部分。
Jackson JSON
當存在Jackson庫時,都支持JSON和二進制JSON(Smile)。
“ Jackson2Decoder”的工作方式如下:
- Jackson 的異步,非阻塞解析器用於將字節塊流聚合到TokenBuffer的每個塊中,每個代表JSON對象。
- 每個TokenBuffer都傳遞給Jackson的ObjectMapper以創建更高級別的對象。
- 解碼為單值發布者(例如Mono)時,有一個TokenBuffer。
- 當解碼為多值發布者(例如Flux)時,一旦為完整格式的對象接收到足夠的字節,每個TokenBuffer就會傳遞給ObjectMapper。輸入內容可以是JSON數組,也可以是任何以行分隔的JSON格式,例如NDJSON,JSON Lines或JSON Text Sequences。
Jackson2Encoder的工作方式如下:
- 對於單個值發布者(例如Mono),只需通過ObjectMapper對其進行序列化即可。
- 對於具有application / json的多值發布者,默認情況下使用Flux#collectToList()收集值,然后序列化結果集合。
- 對於具有流媒體類型(例如application / x-ndjson或application / stream + x-jackson-smile)的多值發布者,請使用行定界的JSON格式分別對每個值進行編碼,寫入和刷新。其他流媒體類型可以在編碼器中注冊。
- 對於SSE,將為每個事件調用Jackson2Encoder,並刷新輸出以確保交付沒有延遲。
Form Data
FormHttpMessageReader和FormHttpMessageWriter支持對應用程序/ x-www-form-urlencoded內容進行解碼和編碼。
在經常需要從多個位置訪問表單內容的服務器端,ServerWebExchange提供了專用的getFormData()方法,該方法通過FormHttpMessageReader解析內容,然后緩存結果以進行重復訪問。請參閱WebHandler API部分中的表單數據。
一旦使用getFormData(),就無法再從請求正文中讀取原始原始內容。因此,應用程序應始終通過ServerWebExchange來訪問緩存的表單數據,而不是從原始請求正文中進行讀取。
Multipart
MultipartHttpMessageReader和MultipartHttpMessageWriter支持對“ multipart / form-data”內容進行解碼和編碼。反過來,MultipartHttpMessageReader委托另一個HttpMessageReader進行實際解析為Flux ,然后將這些部分簡單地收集到MultiValueMap中。默認情況下,使用DefaultPartHttpMessageReader,但是可以通過ServerCodecConfigurer進行更改。有關DefaultPartHttpMessageReader的更多信息,請參考DefaultPartHttpMessageReader的javadoc。
在可能需要從多個位置訪問多部分表單內容的服務器端,ServerWebExchange提供了專用的getMultipartData()方法,該方法通過MultipartHttpMessageReader解析內容,然后緩存結果以進行重復訪問。請參閱WebHandler API部分中的多部分數據。
一旦使用getMultipartData(),就無法再從請求正文中讀取原始原始內容。因此,應用程序必須始終使用getMultipartData()來重復,類似地圖地訪問零件,否則必須依賴SynchronossPartHttpMessageReader來一次性訪問Flux 。
Limits
可以對緩沖部分或全部輸入流的Decoder和HttpMessageReader實現進行配置,並限制要在內存中緩沖的最大字節數。在某些情況下,由於輸入被聚合並表示為單個對象而發生緩沖,例如,具有@RequestBody byte [],x-www-form-urlencoded數據的控制器方法,等等。在分割輸入流(例如,定界文本,JSON對象流等)時,流處理也會發生緩沖。對於這些流情況,該限制適用於與流中一個對象關聯的字節數。
要配置緩沖區大小,可以檢查給定的Decoder或HttpMessageReader是否公開了maxInMemorySize屬性,如果這樣,則Javadoc將具有有關默認值的詳細信息。在服務器端,ServerCodecConfigurer提供了一個設置所有編解碼器的位置,請參閱HTTP消息編解碼器。在客戶端,可以在WebClient.Builder中更改所有編解碼器的限制。
對於Multipart解析,maxInMemorySize屬性限制了非文件部分的大小。對於文件部件,它確定將部件寫入磁盤的閾值。對於寫入磁盤的文件部件,還有一個額外的maxDiskUsagePerPart屬性可限制每個部件的磁盤空間量。還有一個maxParts屬性,用於限制多部分請求中的部分總數。要在WebFlux中配置所有這三個功能,您需要向ServerCodecConfigurer提供一個預先配置的MultipartHttpMessageReader實例。
Streaming
在流式傳輸到HTTP響應時(例如,text / event-stream,application / x-ndjson),定期發送數據很重要,這樣才能盡快(而不是稍后)可靠地檢測到斷開連接的客戶端。這樣的發送可以是僅注釋,空的SSE事件或任何其他可以有效充當心跳的“無操作”數據。
DataBuffer
DataBuffer是WebFlux中字節緩沖區的表示形式。本參考資料的Spring Core部分在“數據緩沖區和編解碼器”部分中有更多介紹。要理解的關鍵點是,在諸如Netty之類的某些服務器上,字節緩沖被池化並計數引用,並且在使用時必須將其釋放以避免內存泄漏。
WebFlux應用程序通常不需要關心此類問題,除非它們直接使用或產生數據緩沖區,而不是依賴於編解碼器與更高級別的對象之間進行轉換,或者除非它們選擇創建自定義編解碼器。對於這種情況,請查看數據緩沖區和編解碼器中的信息,尤其是有關使用數據緩沖區的部分。
1.2.6. Logging
Spring WebFlux中的DEBUG級別日志記錄旨在緊湊,最小化並且對用戶友好。它關注於一遍又一遍有用的高價值信息,而其他信息則僅在調試特定問題時才有用。
TRACE級別的日志記錄通常遵循與DEBUG相同的原理(例如,也不應成為firehose),但可用於調試任何問題。另外,某些日志消息在TRACE vs DEBUG上可能顯示不同級別的詳細信息。
良好的日志記錄來自使用日志的經驗。如果發現任何不符合既定目標的東西,請告訴我們。
Log Id
在WebFlux中,單個請求可以在多個線程上運行,並且線程ID對於關聯屬於特定請求的日志消息沒有用。這就是為什么WebFlux日志消息默認情況下帶有特定於請求的ID的原因。
在服務器端,日志ID存儲在ServerWebExchange屬性(LOG_ID_ATTRIBUTE)中,而可從ServerWebExchange#getLogPrefix()獲得基於該ID的全格式前綴。在WebClient端,日志ID存儲在ClientRequest屬性(LOG_ID_ATTRIBUTE)中,而完全格式的前綴可從ClientRequest#logPrefix()獲得。
Sensitive Data
DEBUG和TRACE日志記錄可以記錄敏感信息。這就是默認情況下屏蔽表單參數和標題的原因,並且必須顯式啟用它們的完整日志記錄。
下面的示例說明如何針對服務器端請求執行此操作:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
以下示例顯示了如何針對客戶端請求執行此操作
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
Appenders
日志庫(例如SLF4J和Log4J 2)提供了避免阻塞的異步記錄器。盡管它們有其自身的缺點,例如可能丟棄無法排隊進行日志記錄的消息,但它們是當前在反應性,非阻塞應用程序中使用的最佳可用選項。
Custom codecs
應用程序可以注冊自定義編解碼器以支持其他媒體類型,也可以注冊默認編解碼器不支持的特定行為。
開發人員表達的某些配置選項在默認編解碼器上強制執行。自定義編解碼器可能希望有機會與這些首選項保持一致,例如強制執行緩沖限制或記錄敏感數據。
下面的示例說明如何針對客戶端請求執行此操作:
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
1.3. DispatcherHandler
Spring WebFlux與Spring MVC類似,是圍繞前端控制器模式設計的,其中中央WebHandler DispatcherHandler提供了用於請求處理的共享算法,而實際工作是由可配置的委托組件執行的。該模型非常靈活,並支持多種工作流程。
DispatcherHandler從Spring配置中發現所需的委托組件。它還被設計為Spring Bean本身,並實現ApplicationContextAware來訪問其運行的上下文。如果以WebHandler的bean名稱聲明了DispatcherHandler,則WebHttpHandlerBuilder會發現它,而WebHttpHandlerBuilder會按照WebHandler API中的描述將請求處理鏈組合在一起。
WebFlux應用程序中的Spring配置通常包含:
Bean名稱為webHandler的DispatcherHandler
WebFilter和WebExceptionHandler bean
DispatcherHandler特殊豆
將配置提供給WebHttpHandlerBuilder以構建處理鏈,如以下示例所示:
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
生成的HttpHandler已准備好與服務器適配器一起使用。
1.3.1. Special Bean Types
DispatcherHandler委托給特殊的bean處理請求並呈現適當的響應。所謂“特殊bean”,是指實現WebFlux框架合同的Spring管理對象實例。這些通常帶有內置合同,但是您可以自定義它們的屬性,擴展它們或替換它們。
下表列出了DispatcherHandler檢測到的特殊bean。請注意,在較低級別還檢測到其他一些Bean(請參閱Web Handler API中的特殊Bean類型)。
Bean type | Explanation |
---|---|
HandlerMapping |
將請求映射到處理程序。映射基於某些條件,這些條件的詳細信息因HandlerMapping實現而有所不同-帶有注釋的控制器,簡單的URL模式映射以及其他。 主要的HandlerMapping實現是用於@RequestMapping注釋方法的RequestMappingHandlerMapping,用於功能端點路由的RouterFunctionMapping和用於URI路徑模式和WebHandler實例的顯式注冊的SimpleUrlHandlerMapping。 |
HandlerAdapter |
幫助DispatcherHandler調用映射到請求的處理程序,而不管實際如何調用該處理程序。例如,調用帶注釋的控制器需要解析注釋。 HandlerAdapter的主要目的是使DispatcherHandler免受此類細節的影響。 |
HandlerResultHandler |
處理來自處理程序調用的結果,並最終確定響應。請參閱結果處理。 |
1.3.2. WebFlux Config
應用程序可以聲明處理請求所需的基礎結構bean(在Web Handler API和DispatcherHandler下列出)。但是,在大多數情況下,WebFlux Config是最佳起點。它聲明了所需的bean,並提供了更高級別的配置回調API來對其進行自定義。
1.3.3. Processing
DispatcherHandler處理請求的方式如下:
- 要求每個HandlerMapping查找一個匹配的處理程序,並使用第一個匹配項。
- 如果找到處理程序,則通過適當的HandlerAdapter運行該處理程序,該處理程序將執行的返回值公開為HandlerResult。
- 通過直接寫入響應或使用視圖渲染,將HandlerResult提供給適當的HandlerResultHandler以完成處理。
1.3.4. Result Handling
通過HandlerAdapter調用處理程序的返回值連同其他一些上下文一起包裝為HandlerResult,並傳遞給第一個聲明支持它的HandlerResultHandler。下表顯示了可用的HandlerResultHandler實現,所有實現都在WebFlux Config中聲明:
Result Handler Type | Return Values | Default Order |
---|---|---|
ResponseEntityResultHandler |
ResponseEntity , typically from @Controller instances. |
0 |
ServerResponseResultHandler |
ServerResponse , typically from functional endpoints. |
0 |
ResponseBodyResultHandler |
Handle return values from @ResponseBody methods or @RestController classes. |
100 |
ViewResolutionResultHandler |
CharSequence , View , Model, Map , Rendering, or any other Object is treated as a model attribute.See also View Resolution. |
Integer.MAX_VALUE |
1.3.5. Exceptions
從HandlerAdapter返回的HandlerResult可以公開基於某些特定於處理程序的機制進行錯誤處理的函數。在以下情況下將調用此錯誤函數:
- 處理程序(例如,@ Controller)調用失敗。
- 通過HandlerResultHandler處理處理程序返回值失敗。
只要在從處理程序返回的反應類型產生任何數據項之前發生錯誤信號,錯誤函數就可以更改響應(例如,更改為錯誤狀態)。
這就是支持@Controller類中的@ExceptionHandler方法的方式。相比之下,Spring MVC中對相同功能的支持建立在HandlerExceptionResolver之上。這通常不重要。但是,請記住,在WebFlux中,不能使用@ControllerAdvice來處理在選擇處理程序之前發生的異常。
另請參見“帶注釋的控制器”部分中的“管理異常”或“ WebHandler API”部分中的“異常”。
1.3.6. View Resolution
視圖分辨率使您可以使用HTML模板和模型渲染到瀏覽器,而無需將您與特定的視圖技術聯系在一起。在Spring WebFlux中,通過專用的HandlerResultHandler支持視圖解析,該HandlerResultHandler使用ViewResolver實例將String(代表邏輯視圖名稱)映射到View實例。然后使用視圖來呈現響應。
Handling
傳遞給ViewResolutionResultHandler的HandlerResult包含處理程序的返回值和包含請求處理期間添加的屬性的模型。返回值將作為以下值之一進行處理:
- 字符串,CharSequence:通過配置的ViewResolver實現列表解析為View的邏輯視圖名稱。
- void:根據請求路徑選擇一個默認視圖名稱,減去前導斜杠和尾部斜杠,然后將其解析為視圖。當未提供視圖名稱(例如,返回模型屬性)或異步返回值(例如,Mono完成為空)時,也會發生同樣的情況。
- Rendering:用於視圖分辨率方案的API。通過代碼完成探索IDE中的選項。
- Model、Map:要添加到請求模型的額外模型屬性。
- 其他任何其他值:任何其他返回值(由BeanUtils#isSimpleProperty確定的簡單類型除外)都將作為要添加到模型的模型屬性。屬性名稱是通過使用約定從類名稱派生的,除非存在處理程序方法@ModelAttribute批注。
該模型可以包含異步,反應式類型(例如,來自Reactor或RxJava)。在渲染之前,AbstractView將這些模型屬性解析為具體值並更新模型。單值反應類型被解析為單個值或無值(如果為空),而多值反應類型(例如Flux )被收集並解析為List 。
配置視圖分辨率就像將一個SpringResolutionResultHandler bean添加到您的Spring配置中一樣簡單。 WebFlux Config提供了專用於視圖分辨率的配置API。
有關與Spring WebFlux集成的視圖技術的更多信息,請參見View Technologies。
Redirecting
視圖名稱中的特殊redirect:前綴使您可以執行重定向。 UrlBasedViewResolver(和子類)將其識別為需要重定向的指令。視圖名稱的其余部分是重定向URL。
最終效果與控制器返回RedirectView或Rendering.redirectTo(“ abc”)。build()相同,但是現在控制器本身可以根據邏輯視圖名稱進行操作。視圖名稱(例如redirect:/ some / resource)相對於當前應用程序,而視圖名稱(例如redirect:https://example.com/arbitrary/path)則重定向到絕對URL。
Content Negotiation
ViewResolutionResultHandler支持內容協商。它將請求媒體類型與每個選定視圖支持的媒體類型進行比較。使用支持請求的媒體類型的第一個視圖。
為了支持JSON和XML之類的媒體類型,Spring WebFlux提供了HttpMessageWriterView,它是通過HttpMessageWriter呈現的特殊視圖。通常,您可以通過WebFlux配置將其配置為默認視圖。如果默認視圖與請求的媒體類型匹配,則始終會選擇和使用它們。
1.4. Annotated Controllers
Spring WebFlux提供了一個基於注釋的編程模型,其中@Controller和@RestController組件使用注釋來表達請求映射,請求輸入,處理異常等。帶注釋的控制器具有靈活的方法簽名,無需擴展基類或實現特定的接口。
以下清單顯示了一個基本示例:
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
在前面的示例中,該方法返回要寫入響應主體的String。
1.4.1. @Controller
您可以使用標准的Spring bean定義來定義控制器bean。@Controller構造型允許自動檢測,並且與Spring常規支持保持一致,以支持在類路徑中檢測@Component類並為其自動注冊Bean定義。它還充當帶注釋類的構造型,表明其作為Web組件的作用。
要啟用對此類@Controller Bean的自動檢測,可以將組件掃描添加到Java配置中,如以下示例所示:
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
@RestController是一個組合式注釋,其本身使用@Controller和@ResponseBody進行了元注釋,表示一個控制器,其每個方法都繼承了類型級別的@ResponseBody注釋,因此,直接將其寫入響應主體(與視圖分辨率相對)並使用HTML模板。
1.4.2. Request Mapping
@RequestMapping批注用於將請求映射到控制器方法。它具有各種屬性,可以通過URL,HTTP方法,請求參數,標頭和媒體類型進行匹配。您可以在類級別使用它來表示共享的映射,也可以在方法級別使用它來縮小到特定的端點映射。
@RequestMapping還有HTTP方法特定的快捷方式:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
前面的注釋是提供的“自定義注釋”,因為可以說,大多數控制器方法應映射到特定的HTTP方法,而不是使用@RequestMapping,后者默認情況下與所有HTTP方法匹配。同時,在類級別仍需要@RequestMapping來表示共享映射。
以下示例使用類型和方法級別的映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
URI Patterns
您可以使用全局模式和通配符來映射請求:
Pattern | Description | Example |
---|---|---|
? |
匹配一個字符 | "/pages/t?st.html" matches "/pages/test.html" and "/pages/t3st.html" |
* |
匹配路徑段中的零個或多個字符 | "/resources/*.png" matches "/resources/file.png" "/projects/*/versions" matches "/projects/spring/versions" but does not match "/projects/spring/boot/versions" |
** |
匹配零個或多個路徑段,直到路徑結束 | "/resources/**" matches "/resources/file.png" and "/resources/images/file.png" "/resources/**/file.png" is invalid as ** is only allowed at the end of the path. |
{name} |
匹配路徑段並將其捕獲為名為“ name”的變量 | "/projects/{project}/versions" matches "/projects/spring/versions" and captures project=spring |
{name:[a-z]+} |
將正則表達式“ [[a-z] +””匹配為路徑變量“ name” | "/projects/{project:[a-z]+}/versions" matches "/projects/spring/versions" but not "/projects/spring1/versions" |
{*path} |
匹配零個或多個路徑段,直到路徑結尾,並將其捕獲為名為“ path”的變量 | "/resources/{*file}" matches "/resources/images/file.png" and captures file=images/file.png |
捕獲的URI變量可以通過@PathVariable
訪問,如以下示例所示:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
您可以在類和方法級別聲明URI變量,如以下示例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
URI變量會自動轉換為適當的類型,或者引發TypeMismatchException。默認情況下,支持簡單類型(int,long,Date等),您可以注冊對任何其他數據類型的支持。請參閱類型轉換和DataBinder。
可以顯式命名URI變量(例如,@PathVariable(“ customId”)),但是如果名稱相同,則可以省略該詳細信息,並使用調試信息或Java 8上的-parameters編譯器標志編譯代碼。 。
語法{* varName}聲明了一個與零個或多個剩余路徑段匹配的URI變量。例如,/ resources / {* path}匹配/ resources /下的所有文件,並且“ path”變量捕獲完整的相對路徑。
語法{varName:regex}聲明帶有正則表達式的URI變量,其語法為:{varName:regex}。例如,給定URL /spring-web-3.0.5 .jar,以下方法提取名稱,版本和文件擴展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
URI路徑模式還可以嵌入$ {…}占位符,這些占位符在啟動時通過PropertyPlaceHolderConfigurer針對本地,系統,環境和其他屬性源進行解析。您可以使用它來例如基於某些外部配置參數化基本URL。
Spring WebFlux不支持后綴模式匹配,這與Spring MVC不同,后者的映射(例如/ person)也匹配到/person.*。對於基於URL的內容協商,如果需要,我們建議使用查詢參數,該參數更簡單,更明確,並且不易受到基於URL路徑的攻擊。
Pattern Comparison
當多個模式與URL匹配時,必須將它們進行比較以找到最佳匹配。這是通過PathPattern.SPECIFICITY_COMPARATOR完成的,該工具查找更具體的模式。
對於每個模式,都會根據URI變量和通配符的數量計算得分,其中URI變量的得分低於通配符。總得分較低的模式將獲勝。如果兩個模式的分數相同,則選擇更長的時間。
包羅萬象的模式(例如*,{ varName})不計入評分,而是始終排在最后。如果兩種模式都通用,則選擇較長的模式。
Consumable Media Types
您可以根據請求的Content-Type縮小請求映射,如以下示例所示:
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
consumes 屬性還支持否定表達式-例如,!text/plain 表示除 text/plain 之外的任何內容類型。
您可以在類級別上聲明一個共享的cosumes屬性。但是,與大多數其他請求映射屬性不同,在類級別使用時,方法級別使用屬性覆蓋而不是擴展類級別聲明。
Producible Media Types
您可以根據接受請求標頭和控制器方法生成的內容類型列表來縮小請求映射,如以下示例所示:
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
媒體類型可以指定字符集。支持否定表達式-例如,!text / plain表示除text / plain之外的任何內容類型。
您可以在類級別聲明共享的Produces屬性。但是,與大多數其他請求映射屬性不同,在類級別使用方法級別時,方法級別會產生屬性覆蓋,而不是擴展類級別聲明。
Parameters and Headers
您可以根據查詢參數條件來縮小請求映射。您可以測試查詢參數(myParam)的存在,不存在(!myParam)或特定值(myParam = myValue)。以下示例測試具有值的參數:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
您還可以將其與請求標頭條件一起使用,如以下示例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
HTTP HEAD, OPTIONS
@GetMapping和@RequestMapping(method = HttpMethod.GET)透明地支持HTTP HEAD,用於請求映射。控制器方法無需更改。在HttpHandler服務器適配器中應用的響應包裝器確保將Content-Length標頭設置為寫入的字節數,而無需實際寫入響應。
默認情況下,通過將“允許響應”標頭設置為所有具有匹配URL模式的@RequestMapping方法中列出的HTTP方法列表,來處理HTTP OPTIONS。
對於沒有HTTP方法聲明的@RequestMapping,將Allow標頭設置為GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法應始終聲明受支持的HTTP方法(例如,通過使用HTTP方法特定的變體-@ GetMapping,@ PostMapping等)。
您可以將@RequestMapping方法顯式映射到HTTP HEAD和HTTP OPTIONS,但這在通常情況下不是必需的。
Custom Annotations
Spring WebFlux支持將組合注釋用於請求映射。這些注解本身使用@RequestMapping進行元注解,並且旨在以更狹窄,更具體的用途重新聲明@RequestMapping屬性的子集(或全部)。
@GetMapping,@ PostMapping,@ PutMapping,@ DeleteMapping和@PatchMapping是組合注釋的示例。之所以提供它們,是因為可以說,大多數控制器方法應該映射到特定的HTTP方法,而不是使用@RequestMapping,后者默認情況下與所有HTTP方法都匹配。如果需要組合注釋的示例,請查看如何聲明它們。
Spring WebFlux還支持具有自定義請求匹配邏輯的自定義請求映射屬性。這是一個更高級的選項,需要子類化RequestMappingHandlerMapping並覆蓋getCustomMethodCondition方法,您可以在其中檢查自定義屬性並返回自己的RequestCondition。
Explicit Registrations
您可以以編程方式注冊Handler方法,這些方法可用於動態注冊或高級用例,例如同一處理程序在不同URL下的不同實例。以下示例顯示了如何執行此操作:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build();
Method method = UserHandler.class.getMethod("getUser", Long.class);
mapping.registerMapping(info, handler, method);
}
}
1.4.3. Handler Methods
@RequestMapping處理程序方法具有靈活的簽名,可以從一系列受支持的控制器方法參數和返回值中進行選擇。
Method Arguments
下表顯示了受支持的控制器方法參數。
需要解析I / O(例如,讀取請求正文)的自變量支持反應性類型(Reactor,RxJava或其他)。這在“描述”列中進行了標記。不需要阻塞的參數不應使用反應性類型。
支持JDK 1.8的java.util.Optional作為方法參數,並與具有必需屬性(例如@ RequestParam,@ RequestHeader等)的注釋結合在一起,等效於required = false。
Controller method argument | Description |
---|---|
ServerWebExchange |
訪問用於HTTP請求和響應,請求和會話屬性,checkNotModified方法等的完整ServerWebExchange容器。 |
ServerHttpRequest , ServerHttpResponse |
訪問HTTP請求或響應。 |
WebSession |
訪問會話。除非添加了屬性,否則這不會強制開始新的會話。支持反應類型。 |
java.security.Principal |
當前經過身份驗證的用戶-可能是特定的Principal實現類(如果已知)。支持反應類型。 |
org.springframework.http.HttpMethod |
請求的HTTP方法。 |
java.util.Locale |
當前的請求區域設置,由最具體的可用LocaleResolver確定,實際上是配置的LocaleResolver / LocaleContextResolver。 |
java.util.TimeZone + java.time.ZoneId |
與當前請求關聯的時區,由LocaleContextResolver確定。 |
@PathVariable |
用於訪問URI模板變量。請參閱URI模式。 |
@MatrixVariable |
用於訪問URI路徑段中的名稱/值對。請參閱矩陣變量。 |
@RequestParam |
用於訪問Servlet請求參數。參數值將轉換為聲明的方法參數類型。請參閱@RequestParam。 請注意,@ RequestParam的使用是可選的,例如可以設置其屬性。請參閱此表后面的“其他任何參數”。 |
@RequestHeader |
用於訪問請求標頭。標頭值將轉換為聲明的方法參數類型。請參閱@RequestHeader。 |
@CookieValue |
用於訪問cookie。 Cookie值將轉換為聲明的方法參數類型。請參閱@CookieValue。 |
@RequestBody |
用於訪問HTTP請求正文。正文內容通過使用HttpMessageReader實例轉換為聲明的方法參數類型。支持反應類型。請參閱@RequestBody。 |
HttpEntity<B> |
用於訪問請求標頭和正文。主體使用HttpMessageReader實例進行轉換。支持反應類型。請參見HttpEntity。 |
@RequestPart |
用於訪問multipart / form-data請求中的零件。支持反應類型。請參見多部分內容和多部分數據。 |
java.util.Map , org.springframework.ui.Model , and org.springframework.ui.ModelMap . |
用於訪問HTML控制器中使用的模型,並作為視圖渲染的一部分公開給模板。 |
@ModelAttribute |
用於訪問已應用數據綁定和驗證的模型中現有的屬性(如果不存在,則進行實例化)。請參見@ModelAttribute以及Model和DataBinder。 請注意,@ ModelAttribute的使用是可選的,例如可以設置其屬性。請參閱此表后面的“其他任何參數”。 |
Errors , BindingResult |
為了訪問來自驗證和命令對象數據綁定的錯誤,即@ModelAttribute參數。必須在經過驗證的方法參數后立即聲明Errors或BindingResult參數。 |
SessionStatus + class-level @SessionAttributes |
為了標記表單處理完成,將觸發清除通過類級別@SessionAttributes注釋聲明的會話屬性。有關更多詳細信息,請參見@SessionAttributes。 |
UriComponentsBuilder |
用於准備相對於當前請求的主機,端口,方案和上下文路徑的URL。請參閱URI鏈接。 |
@SessionAttribute |
用於訪問任何會話屬性-與通過類級別@SessionAttributes聲明存儲在會話中的模型屬性相反。有關更多詳細信息,請參見@SessionAttribute。 |
@RequestAttribute |
用於訪問請求屬性。有關更多詳細信息,請參見@RequestAttribute。 |
Any other argument | 如果方法參數與以上任何參數都不匹配,則默認情況下,如果它是由BeanUtils#isSimpleProperty確定的簡單類型,則將其解析為@RequestParam,否則將其解析為@ModelAttribute。 |
Return Values
下表顯示了受支持的控制器方法返回值。請注意,所有返回值通常都支持Reactor,RxJava之類的庫中的反應類型。
Controller method return value | Description |
---|---|
@ResponseBody |
返回值通過HttpMessageWriter實例進行編碼,並寫入響應中。請參閱@ResponseBody。 |
HttpEntity<B> , ResponseEntity<B> |
返回值指定完整的響應,包括HTTP標頭,並且正文通過HttpMessageWriter實例進行編碼並寫入響應。請參閱ResponseEntity。 |
HttpHeaders |
用於返回不包含標題的響應。 |
String |
要用ViewResolver實例解析的視圖名稱,並與隱式模型一起使用-通過命令對象和@ModelAttribute方法確定。該處理程序方法還可以通過聲明Model參數(如前所述)以編程方式豐富模型。 |
View |
用於與隱式模型一起呈現的View實例,該隱式模型是通過命令對象和@ModelAttribute方法確定的。該處理程序方法還可以通過聲明Model參數(如前所述)以編程方式豐富模型。 |
java.util.Map , org.springframework.ui.Model |
要添加到隱式模型的屬性,其中視圖名稱根據請求路徑隱式確定。 |
@ModelAttribute |
要添加到模型的屬性,視圖名稱根據請求路徑隱式確定。 請注意,@ ModelAttribute是可選的。請參閱此表后面的“其他任何返回值”。 |
Rendering |
用於模型和視圖渲染方案的API。 |
void |
如果方法也具有ServerHttpResponse,ServerWebExchange參數或@ResponseStatus,則該方法具有無效的,可能是異步的(例如,Mono ),返回類型(或返回值為空)的方法被認為已完全處理了響應。注解。如果控制器進行了肯定的ETag或lastModified時間戳檢查,也是如此。 // TODO:有關詳細信息,請參見控制器。 如果以上所有條件都不成立,則對於REST控制器,void返回類型也可以指示“無響應正文”,對於HTML控制器,則表示默認視圖名稱選擇。 |
Flux<ServerSentEvent> , Observable<ServerSentEvent> , or other reactive type |
發出服務器發送的事件。僅需要寫入數據時,可以省略ServerSentEvent包裝器(但是,必須通過Produces屬性在映射中請求或聲明文本/事件流)。 |
Any other return value | 如果返回值不符合以上任何條件,則默認情況下將其視為視圖名稱,為String或void(適用默認視圖名稱選擇)或將其添加為模型的模型屬性,除非它是由BeanUtils#isSimpleProperty確定的簡單類型,否則在這種情況下它將無法解析。 |
Type Conversion
如果參數聲明為String以外的其他內容,則表示基於String的請求輸入的某些帶注釋的控制器方法參數(例如,@ RequestParam,@ RequestHeader,@ PathVariable,@ MatrixVariable和@CookieValue)可能需要類型轉換。
在這種情況下,將根據配置的轉換器自動應用類型轉換。默認情況下,支持簡單類型(例如int,long,Date和其他)。可以通過WebDataBinder(請參閱DataBinder)或通過向FormattingConversionService注冊格式化程序(請參見Spring字段格式)來自定義類型轉換。
類型轉換中的一個實際問題是處理空的String源值。如果此值由於類型轉換而變為null,則將其視為丟失。 Long,UUID和其他目標類型可能就是這種情況。如果要允許注入null,請在參數注釋上使用必需的標志,或將參數聲明為@Nullable。
Matrix Variables
RFC 3986討論路徑段中的名稱/值對。在Spring WebFlux中,基於Tim Berners-Lee的“舊帖子”,我們將其稱為“矩陣變量”,但它們也可以稱為URI路徑參數。
矩陣變量可以出現在任何路徑段中,每個變量用分號分隔,多個值用逗號分隔,例如“ / cars; color = red,green; year = 2012”。也可以通過重復的變量名來指定多個值,例如“ color = red; color = green; color = blue”。
與Spring MVC不同,在WebFlux中,URL中是否存在矩陣變量不會影響請求映射。換句話說,您不需要使用URI變量來屏蔽變量內容。就是說,如果要從控制器方法訪問矩陣變量,則需要將URI變量添加到期望矩陣變量的路徑段中。以下示例顯示了如何執行此操作:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
鑒於所有路徑段都可以包含矩陣變量,因此有時可能需要消除矩陣變量應位於哪個路徑變量的歧義,如以下示例所示:
// GET /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
}
您可以定義一個矩陣變量,可以將其定義為可選變量並指定一個默認值,如以下示例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
要獲取所有矩陣變量,請使用MultiValueMap,如以下示例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam
您可以使用@RequestParam批注將查詢參數綁定到控制器中的方法參數。以下代碼段顯示了用法:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
默認情況下需要使用@RequestParam批注的方法參數,但是您可以通過將@RequestParam的required標志設置為false或通過使用java.util.Optional包裝器聲明參數來指定方法參數是可選的。
如果目標方法參數類型不是字符串,則將自動應用類型轉換。請參閱類型轉換。
在Map 或MultiValueMap 參數上聲明@RequestParam批注時,將使用所有查詢參數填充該映射。
請注意,@ RequestParam的使用是可選的,例如可以設置其屬性。默認情況下,任何簡單值類型的參數(由BeanUtils#isSimpleProperty確定)並且沒有被其他任何參數解析器解析,就如同使用@RequestParam進行了注釋一樣。
@RequestHeader
您可以使用@RequestHeader批注將請求標頭綁定到控制器中的方法參數。
以下示例顯示了帶有標頭的請求:
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
以下示例獲取Accept-Encoding和Keep-Alive標頭的值:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
在Map ,MultiValueMap 或HttpHeaders參數上使用@RequestHeader批注時,將使用所有標頭值填充該映射。
@CookieValue
您可以使用@CookieValue批注將HTTP cookie的值綁定到控制器中的方法參數。
以下示例顯示了一個帶有cookie的請求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下代碼示例演示如何獲取cookie值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) {
//...
}
如果目標方法參數類型不是字符串,則將自動應用類型轉換。請參閱類型轉換。
@ModelAttribute
您可以在方法參數上使用@ModelAttribute批注,以從模型訪問屬性,或將其實例化(如果不存在)。 model屬性還覆蓋了名稱與字段名稱匹配的查詢參數和表單字段的值。這稱為數據綁定,它使您不必處理解析和轉換單個查詢參數和表單字段的工作。下面的示例綁定Pet的實例:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }
前面示例中的Pet實例解析如下:
- 從模型(如果已通過Model添加)。
- 從HTTP會話通過@SessionAttributes。
- 從默認構造函數的調用開始。
- 從帶有匹配查詢參數或表單字段的參數的“主要構造函數”的調用開始。參數名稱是通過JavaBeans @ConstructorProperties或字節碼中運行時保留的參數名稱確定的。
獲取模型屬性實例后,將應用數據綁定。 WebExchangeDataBinder類將查詢參數和表單字段的名稱與目標Object上的字段名稱匹配。必要時在應用類型轉換后填充匹配字段。有關數據綁定(和驗證)的更多信息,請參見驗證。有關自定義數據綁定的更多信息,請參見DataBinder。
數據綁定可能導致錯誤。默認情況下,引發WebExchangeBindException,但是,要檢查控制器方法中的此類錯誤,可以在@ModelAttribute旁邊立即添加BindingResult參數,如以下示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}
您可以在數據綁定之后通過添加javax.validation.Valid注釋或Spring的@Validated注釋自動應用驗證(另請參見Bean驗證和Spring驗證)。以下示例使用@Valid批注:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
if (result.hasErrors()) {
return "petForm";
}
// ...
}
與Spring MVC不同,Spring WebFlux在模型中支持反應性類型,例如Mono 或io.reactivex.Single 。您可以聲明一個@ModelAttribute參數,帶或不帶反應性類型包裝器,並將根據需要將其解析為實際值。但是,請注意,要使用BindingResult參數,必須在@ModelAttribute參數之前聲明@ModelAttribute參數,而不必使用反應式類型包裝器,如先前所示。另外,您可以通過反應式處理任何錯誤,如以下示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}
請注意,@ ModelAttribute的使用是可選的,例如可以設置其屬性。默認情況下,任何不是簡單值類型(由BeanUtils#isSimpleProperty確定)且未被其他任何參數解析器解析的參數都將被視為使用@ModelAttribute注釋。
@SessionAttributes
@SessionAttributes用於在請求之間的WebSession中存儲模型屬性。它是類型級別的注釋,用於聲明特定控制器使用的會話屬性。這通常列出應透明地存儲在會話中以供后續訪問請求的模型屬性名稱或模型屬性類型。
考慮以下示例:
@Controller
@SessionAttributes("pet")
public class EditPetForm {
// ...
}
在第一個請求上,將名稱為pet的模型屬性添加到模型后,該屬性會自動升級到WebSession並保存在WebSession中。它會一直保留在那里,直到另一個控制器方法使用SessionStatus方法參數來清除存儲,如以下示例所示:
@Controller
@SessionAttributes("pet")
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors()) {
// ...
}
status.setComplete();
// ...
}
}
}
@SessionAttribute
如果您需要訪問全局存在(例如,在控制器外部(例如,通過過濾器)管理)並且可能存在或可能不存在的預先存在的會話屬性,則可以在方法參數上使用@SessionAttribute注釋,以下示例顯示:
@GetMapping("/")
public String handle(@SessionAttribute User user) {
// ...
}
對於需要添加或刪除會話屬性的用例,請考慮將WebSession注入控制器方法中。
若要將模型屬性作為控制器工作流的一部分臨時存儲在會話中,請考慮使用SessionAttributes,如@SessionAttributes中所述。
@RequestAttribute
與@SessionAttribute相似,您可以使用@RequestAttribute批注來訪問先前創建的預先存在的請求屬性(例如,通過WebFilter),如以下示例所示:
@GetMapping("/")
public String handle(@RequestAttribute Client client) {
// ...
}
Multipart Content
如多部分數據中所述,ServerWebExchange提供對多部分內容的訪問。在控制器中處理文件上傳表單(例如,從瀏覽器)的最佳方法是通過將數據綁定到命令對象,如以下示例所示:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
您還可以在RESTful服務方案中從非瀏覽器客戶端提交多部分請求。以下示例將文件與JSON一起使用:
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
您可以使用@RequestPart訪問各個部分,如以下示例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata,
@RequestPart("file-data") FilePart file) {
// ...
}
Using @RequestPart to get the metadata. |
|
---|---|
Using @RequestPart to get the file. |
要反序列化原始零件的內容(例如,轉換為JSON(類似於@RequestBody)),可以聲明一個具體的目標Object而不是Part,如以下示例所示:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) {
// ...
}
您可以將@RequestPart與javax.validation.Valid或Spring的@Validated注釋結合使用,這將導致應用標准Bean驗證。驗證錯誤導致WebExchangeBindException,該異常導致響應400(BAD_REQUEST)。異常包含具有錯誤詳細信息的BindingResult,也可以在控制器方法中通過使用異步包裝器聲明參數,然后使用與錯誤相關的運算符來處理該異常:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
要將所有多部分數據作為MultiValueMap進行訪問,可以使用@RequestBody,如以下示例所示:
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) {
// ...
}
要以流方式順序訪問多部分數據,可以將@RequestBody與Flux (或Kotlin中的Flow )一起使用,如以下示例所示:
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) {
// ...
}
@RequestBody
您可以使用@RequestBody批注使請求正文通過HttpMessageReader讀取並反序列化為Object。以下示例使用@RequestBody參數:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
與Spring MVC不同,在WebFlux中,@RequestBody方法參數支持反應類型以及完全無阻塞的讀取和(客戶端到服務器)流傳輸。
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
您可以使用WebFlux Config的HTTP消息編解碼器選項來配置或自定義消息閱讀器。
您可以將@RequestBody與javax.validation.Valid或Spring的@Validated注釋結合使用,這將導致應用標准Bean驗證。驗證錯誤會導致WebExchangeBindException,從而導致響應400(BAD_REQUEST)。異常包含具有錯誤詳細信息的BindingResult,可以在控制器方法中通過使用異步包裝器聲明參數,然后使用與錯誤相關的運算符來處理該異常:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
// use one of the onError* operators...
}
HttpEntity
HttpEntity或多或少與使用@RequestBody相同,但它基於公開請求標頭和正文的容器對象。以下示例使用HttpEntity:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@ResponseBody
您可以在方法上使用@ResponseBody批注,以將返回值通過HttpMessageWriter序列化為響應主體。以下示例顯示了如何執行此操作:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
在類級別還支持@ResponseBody,在這種情況下,所有控制器方法都將繼承它。這就是@RestController的效果,它只不過是帶有@Controller和@ResponseBody標記的元注釋。
@ResponseBody支持反應類型,這意味着您可以返回Reactor或RxJava類型,並將它們產生的異步值呈現給響應。有關更多詳細信息,請參見流和JSON呈現。
您可以將@ResponseBody方法與JSON序列化視圖結合使用。有關詳細信息,請參見Jackson JSON。
您可以使用WebFlux Config的HTTP消息編解碼器選項來配置或自定義消息編寫。
ResponseEntity
ResponseEntity類似於@ResponseBody,但具有狀態和標頭。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
WebFlux支持使用單值反應類型異步生成ResponseEntity,和/或為主體使用單值和多值反應類型。
Jackson JSON
Spring提供了對Jackson JSON庫的支持。
JSON Views
Spring WebFlux為Jackson的序列化視圖提供了內置支持,該視圖僅呈現對象中所有字段的一部分。要將其與@ResponseBody或ResponseEntity控制器方法一起使用,可以使用Jackson的@JsonView批注來激活序列化視圖類,如以下示例所示:
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@JsonView允許一組視圖類,但是每個控制器方法只能指定一個。如果需要激活多個視圖,請使用復合界面。
1.4.4. Model
您可以使用@ModelAttribute批注:
- 在@RequestMapping方法中的方法參數上,可從模型創建或訪問對象,並將其通過WebDataBinder綁定到請求。
- 作為@Controller或@ControllerAdvice類中的方法級注釋,有助於在任何@RequestMapping方法調用之前初始化模型。
- 在@RequestMapping方法上將其返回值標記為模型屬性。
本節討論@ModelAttribute方法,或前面列表中的第二項。控制器可以具有任意數量的@ModelAttribute方法。所有此類方法均在同一控制器中的@RequestMapping方法之前調用。也可以通過@ControllerAdvice在控制器之間共享@ModelAttribute方法。有關更多詳細信息,請參見“控制器建議”部分。
@ModelAttribute方法具有靈活的方法簽名。它們支持許多與@RequestMapping方法相同的參數(@ModelAttribute本身以及與請求正文相關的任何東西除外)。
以下示例使用@ModelAttribute方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
以下示例僅添加一個屬性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
與Spring MVC不同,Spring WebFlux在模型中顯式支持響應類型(例如Mono 或io.reactivex.Single )。可以在@RequestMapping調用時將此類異步模型屬性透明地解析(並更新模型)為其實際值,只要聲明了@ModelAttribute參數而沒有包裝,如以下示例所示:
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
// ...
}
此外,任何具有反應性類型包裝器的模型屬性都將在視圖渲染之前解析為其實際值(並更新了模型)。
您也可以將@ModelAttribute用作@RequestMapping方法上的方法級注釋,在這種情況下,@ RequestMapping方法的返回值將解釋為模型屬性。通常不需要這樣做,因為它是HTML控制器的默認行為,除非返回值是一個String,否則它將被解釋為視圖名稱。 @ModelAttribute還可以幫助自定義模型屬性名稱,如以下示例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
1.4.5. DataBinder
@Controller或@ControllerAdvice類可以具有@InitBinder方法,以初始化WebDataBinder的實例。這些依次用於:
- 將請求參數(即表單數據或查詢)綁定到模型對象。
- 將基於字符串的請求值(例如請求參數,路徑變量,標頭,Cookie等)轉換為控制器方法參數的目標類型。
- 呈現HTML表單時,將模型對象的值格式化為String值。
@InitBinder方法可以注冊特定於控制器的java.beans.PropertyEditor或Spring Converter和Formatter組件。此外,您可以使用WebFlux Java配置在全局共享的FormattingConversionService中注冊Converter和Formatter類型。
@InitBinder方法支持與@RequestMapping方法相同的許多參數,除了@ModelAttribute(命令對象)參數。通常,它們使用WebDataBinder參數聲明(用於注冊)和空返回值。以下示例使用@InitBinder批注:
@Controller
public class FormController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
另外,當通過共享的FormattingConversionService使用基於Formatter的設置時,可以重新使用相同的方法並注冊特定於控制器的Formatter實例,如以下示例所示:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
1.4.6. Managing Exceptions
@Controller和@ControllerAdvice類可以具有@ExceptionHandler方法來處理來自控制器方法的異常。下面的示例包括這樣的處理程序方法:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
該異常可以與正在傳播的頂級異常(即,引發直接IOException)匹配,也可以與頂級包裝器異常(例如,包裝在IllegalStateException內部的IOException)內的直接原因匹配。
對於匹配的異常類型,最好將目標異常聲明為方法參數,如前面的示例所示。或者,注釋聲明可以縮小異常類型以使其匹配。我們通常建議在參數簽名中盡可能具體,並在以相應順序優先的@ControllerAdvice上聲明您的主根異常映射。有關詳細信息,請參見MVC部分。
HandlerAdapter為@RequestMapping方法提供了對Spring WebFlux中@ExceptionHandler方法的支持。有關更多詳細信息,請參見DispatcherHandler。
REST API exceptions
REST服務的常見要求是在響應正文中包含錯誤詳細信息。 Spring框架不會自動這樣做,因為響應主體中錯誤詳細信息的表示是特定於應用程序的。但是,@ RestController可以將@ExceptionHandler方法與ResponseEntity返回值一起使用,以設置響應的狀態和主體。也可以在@ControllerAdvice類中聲明此類方法,以將其全局應用。
1.4.7. Controller Advice
通常,@ ExceptionHandler,@ InitBinder和@ModelAttribute方法在聲明它們的@Controller類(或類層次結構)中適用。如果要使此類方法更全局地應用(跨控制器),則可以在帶有@ControllerAdvice或@RestControllerAdvice注釋的類中聲明它們。
@ControllerAdvice帶有@Component注釋,這意味着可以通過組件掃描將此類注冊為Spring Bean。 @RestControllerAdvice是由@ControllerAdvice和@ResponseBody注釋的組合注釋,這實際上意味着@ExceptionHandler方法通過消息轉換(而不是視圖分辨率或模板渲染)呈現到響應主體。
啟動時,@ RequestMapping和@ExceptionHandler方法的基礎結構類將檢測使用@ControllerAdvice注釋的Spring bean,然后在運行時應用其方法。全局@ExceptionHandler方法(來自@ControllerAdvice)在本地方法(來自@Controller)之后應用。相比之下,全局@ModelAttribute和@InitBinder方法在本地方法之前應用。
默認情況下,@ ControllerAdvice方法適用於每個請求(即所有控制器),但是您可以通過使用批注上的屬性將其范圍縮小到控制器的子集,如以下示例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
前面示例中的選擇器在運行時進行評估,如果廣泛使用,可能會對性能產生負面影響。有關更多詳細信息,請參見@ControllerAdvice javadoc。
1.5. Functional Endpoints
Spring WebFlux包含WebFlux.fn,這是一個輕量級的函數編程模型,其中的函數用於路由和處理請求,而契約則是為不變性而設計的。它是基於注釋的編程模型的替代方案,但可以在相同的Reactive Core基礎上運行。
1.5.1. Overview
在WebFlux.fn中,HTTP請求由HandlerFunction處理:該函數接受ServerRequest並返回延遲的ServerResponse(即Mono )。請求和響應對象都具有不可變的協定,這些協定為JDK 8提供了對HTTP請求和響應的友好訪問。 HandlerFunction等效於基於注釋的編程模型中@RequestMapping方法的主體。
傳入的請求通過RouterFunction路由到處理程序函數:該函數接受ServerRequest並返回延遲的HandlerFunction(即Mono )。當路由器功能匹配時,返回處理程序功能。否則為空Mono。 RouterFunction等效於@RequestMapping批注,但主要區別在於路由器功能不僅提供數據,還提供行為。
RouterFunctions.route()提供了一個路由器構建器,可簡化路由器的創建過程,如以下示例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
運行RouterFunction的一種方法是將其轉換為HttpHandler並通過內置服務器適配器之一進行安裝:
RouterFunctions.toHttpHandler(RouterFunction)
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
大多數應用程序都可以通過WebFlux Java配置運行,請參閱運行服務器。
1.5.2. HandlerFunction
ServerRequest和ServerResponse是不可變的接口,它們提供JDK 8友好的HTTP請求和響應訪問。請求和響應都為反應流提供了對體流的反壓力。請求主體用Reactor Flux或Mono表示。響應主體由任何Reactive Streams Publisher代表,包括Flux和Mono。有關更多信息,請參見反應式庫。
ServerRequest
ServerRequest提供對HTTP方法,URI,標頭和查詢參數的訪問,而通過body方法提供對主體的訪問。
以下示例將請求正文提取到Mono :
Mono<String> string = request.bodyToMono(String.class);
以下示例將主體提取到Flux (或Kotlin中的Flow ),其中Person對象從某種序列化形式(例如JSON或XML)解碼:
Flux<Person> people = request.bodyToFlux(Person.class);
前面的示例是使用更通用的ServerRequest.body(BodyExtractor)的快捷方式,該請求接受BodyExtractor功能策略接口。實用程序類BodyExtractors提供對許多實例的訪問。例如,前面的示例也可以編寫如下:
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
下面的示例演示如何訪問表單數據:
Mono<MultiValueMap<String, String> map = request.formData();
以下示例顯示了如何以map的形式訪問multipart數據:
Mono<MultiValueMap<String, Part> map = request.multipartData();
下面的示例演示如何以流方式一次訪問多個部分:
Flux<Part> parts = request.body(BodyExtractors.toParts());
ServerResponse
ServerResponse提供對HTTP響應的訪問,並且由於它是不可變的,因此您可以使用構建方法來創建它。您可以使用構建器來設置響應狀態,添加響應標題或提供正文。以下示例使用JSON內容創建200(確定)響應:
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
下面的示例演示如何構建一個具有Location標頭且沒有正文的201(已創建)響應:
URI location = ...
ServerResponse.created(location).build();
根據所使用的編解碼器,可以傳遞提示參數以自定義主體的序列化或反序列化方式。例如,要指定Jackson JSON視圖:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Handler Classes
我們可以將處理程序函數編寫為lambda,如以下示例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
這很方便,但是在應用程序中我們需要多個功能,並且多個內聯lambda可能會變得凌亂。因此,將相關的處理程序功能分組到一個處理程序類中很有用,該類的作用與基於注釋的應用程序中的@Controller相似。例如,以下類公開了反應型Person存儲庫:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
Validation
功能端點可以使用Spring的驗證工具將驗證應用於請求正文。例如,給定Person的自定義Spring Validator實現:
public class PersonHandler {
private final Validator validator = new PersonValidator();
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
處理程序還可以通過基於LocalValidatorFactoryBean創建和注入全局Validator實例來使用標准的bean驗證API(JSR-303)。請參閱春季驗證。
1.5.3. RouterFunction
路由器功能用於將請求路由到相應的HandlerFunction。通常,您不是自己編寫路由器功能,而是使用RouterFunctions實用工具類上的方法創建一個。 RouterFunctions.route()(無參數)為您提供了一個流暢的生成器來創建路由器功能,而RouterFunctions.route(RequestPredicate,HandlerFunction)提供了直接創建路由器的方法。
通常,建議使用route()生成器,因為它為典型的映射方案提供了便捷的快捷方式,而無需發現靜態導入。例如,路由器功能構建器提供了GET(String,HandlerFunction)方法來創建GET請求的映射。和POST(String,HandlerFunction)進行POST。
除了基於HTTP方法的映射外,路由構建器還提供了一種在映射到請求時引入其他謂詞的方法。對於每個HTTP方法,都有一個以RequestPredicate作為參數的重載變體,盡管可以表達其他約束。
Predicates
您可以編寫自己的RequestPredicate,但是RequestPredicates實用程序類根據請求路徑,HTTP方法,內容類型等提供常用的實現。以下示例使用請求謂詞基於Accept頭創建約束:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
您可以使用以下命令組合多個請求謂詞:
RequestPredicate.and(RequestPredicate)
— both must match.RequestPredicate.or(RequestPredicate)
— either can match.
RequestPredicates中的許多謂詞都是組成的。例如,RequestPredicates.GET(String)由RequestPredicates.method(HttpMethod)和RequestPredicates.path(String)組成。上面顯示的示例還使用了兩個請求謂詞,因為構建器在內部使用RequestPredicates.GET並將其與接受謂詞組合在一起。
Routes
路由器功能按順序評估:如果第一個路由不匹配,則評估第二個路由,依此類推。因此,在通用路由之前聲明更具體的路由是有意義的。請注意,此行為不同於基於注釋的編程模型,在該模型中,將自動選擇“最特定”的控制器方法。
使用路由器功能生成器時,所有定義的路由都組成一個RouterFunction,從build()返回。還有其他方法可以將多個路由器功能組合在一起:
add(RouterFunction)
on theRouterFunctions.route()
builderRouterFunction.and(RouterFunction)
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— shortcut forRouterFunction.and()
with nestedRouterFunctions.route()
.
以下示例顯示了四種路線的組成:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
Nested Routes
一組路由器功能通常具有共享謂詞,例如共享路徑。在上面的示例中,共享謂詞將是與/ person匹配的路徑謂詞,由三個路由使用。使用注釋時,您可以通過使用映射到/ person的類型級別@RequestMapping注釋來刪除此重復項。在WebFlux.fn中,可以通過路由器功能構建器上的path方法共享路徑謂詞。例如,可以通過以下方式使用嵌套路由來改進上面示例的最后幾行:
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();
盡管基於路徑的嵌套是最常見的,但是您可以通過使用構建器上的nest方法來嵌套在任何種類的謂詞上。上面的內容仍然包含一些以共享的Accept-header謂詞形式出現的重復。我們可以通過將nest方法與accept一起使用來進一步改進:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.build();
1.5.4. Running a Server
如何在HTTP服務器中運行路由器功能?一個簡單的選項是使用以下方法之一將路由器功能轉換為HttpHandler:
RouterFunctions.toHttpHandler(RouterFunction)
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
然后,可以通過遵循HttpHandler來獲取特定於服務器的指令,將返回的HttpHandler與許多服務器適配器一起使用。
Spring Boot還使用了一個更典型的選項,即通過WebFlux Config與基於DispatcherHandler的設置一起運行,該配置使用Spring配置來聲明處理請求所需的組件。 WebFlux Java配置聲明以下基礎結構組件以支持功能端點:
- RouterFunctionMapping:在Spring配置中檢測一個或多個RouterFunction <?> bean,通過RouterFunction.andOther組合它們,並將請求路由到生成的組成RouterFunction。
- HandlerFunctionAdapter:簡單的適配器,它使DispatcherHandler調用映射到請求的HandlerFunction。
- ServerResponseResultHandler:通過調用ServerResponse的writeTo方法來處理HandlerFunction調用的結果。
前面的組件使功能端點適合於DispatcherHandler請求處理生命周期,並且(可能)與帶注釋的控制器(如果已聲明)並排運行。這也是Spring Boot WebFlux啟動器啟用功能端點的方式。
以下示例顯示了WebFlux Java配置(有關如何運行它,請參見DispatcherHandler):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
1.5.5. Filtering Handler Functions
您可以通過使用路由功能構建器上的before,after或filter方法來過濾處理程序函數。使用注釋,可以通過使用@ ControllerAdvice,ServletFilter或同時使用兩者來實現類似的功能。該過濾器將應用於構建器構建的所有路由。這意味着在嵌套路由中定義的過濾器不適用於“頂級”路由。例如,考慮以下示例:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response))
.build();
路由器構建器上的filter方法采用HandlerFilterFunction:該函數采用ServerRequest和HandlerFunction並返回ServerResponse。 handler函數參數代表鏈中的下一個元素。這通常是路由到的處理程序,但是如果應用了多個,它也可以是另一個過濾器。
現在,我們可以在路由中添加一個簡單的安全過濾器,假設我們擁有一個可以確定是否允許特定路徑的SecurityManager。以下示例顯示了如何執行此操作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
前面的示例演示了調用next.handle(ServerRequest)是可選的。我們只允許在允許訪問時運行處理程序函數。
除了在路由器功能構建器上使用filter方法之外,還可以通過RouterFunction.filter(HandlerFilterFunction)將過濾器應用於現有路由器功能。
1.6. URI Links
本節描述了Spring框架中用於准備URI的各種選項。
1.6.1. UriComponents
Spring MVC and Spring WebFlux
UriComponentsBuilder有助於從具有變量的URI模板中構建URI,如以下示例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.build();
URI uri = uriComponents.expand("Westin", "123").toUri();
可以將前面的示例合並為一個鏈,並通過buildAndExpand進行縮短,如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
您可以通過直接轉到URI(這意味着編碼)來進一步縮短它,如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
您可以使用完整的URI模板進一步縮短它,如以下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
1.6.2. UriBuilder
Spring MVC and Spring WebFlux
riComponentsBuilder實現UriBuilder。您可以依次使用UriBuilderFactory創建UriBuilder。 UriBuilderFactory和UriBuilder一起提供了一種可插入的機制,可以基於共享配置(例如基本URL,編碼首選項和其他詳細信息)從URI模板構建URI。
您可以使用UriBuilderFactory配置RestTemplate和WebClient以自定義URI的准備。 DefaultUriBuilderFactory是UriBuilderFactory的默認實現,該實現在內部使用UriComponentsBuilder並公開共享的配置選項。
以下示例顯示如何配置RestTemplate:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
下面的示例配置一個WebClient:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
此外,您也可以直接使用DefaultUriBuilderFactory。它類似於使用UriComponentsBuilder,但不是靜態工廠方法,而是一個包含配置和首選項的實際實例,如以下示例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
1.6.3. URI Encoding
Spring MVC and Spring WebFlux
UriComponentsBuilder在兩個級別公開了編碼選項:
- UriComponentsBuilder#encode(): 首先對URI模板進行預編碼,然后在擴展時嚴格對URI變量進行編碼。
- UriComponents#encode(): 擴展URI變量后,對URI組件進行編碼。
這兩個選項都使用轉義的八位字節替換非ASCII和非法字符。但是,第一個選項還會替換出現在URI變量中的具有保留含義的字符。
在大多數情況下,第一個選項可能會產生預期的結果,因為它將URI變量視為要完全編碼的不透明數據,而選項2僅在URI變量有意包含保留字符的情況下才有用。
以下示例使用第一個選項:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通過直接轉到URI(這意味着編碼)來縮短前面的示例,如以下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的URI模板進一步縮短它,如以下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
WebClient和RestTemplate通過UriBuilderFactory策略在內部擴展和編碼URI模板。兩者都可以使用自定義策略進行配置。如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
DefaultUriBuilderFactory實現在內部使用UriComponentsBuilder來擴展和編碼URI模板。作為工廠,它提供了一個位置,可以根據以下一種編碼模式來配置編碼方法:
- TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode()(對應於較早列表中的第一個選項)對URI模板進行預編碼,並在擴展時嚴格編碼URI變量。
- VALUES_ONLY:不對URI模板進行編碼,而是在將其擴展到模板之前通過UriUtils#encodeUriUriVariables對URI變量進行嚴格編碼。
- URI_COMPONENT:在擴展URI變量后,使用UriComponents#encode()(對應於先前列表中的第二個選項)對URI組件值進行編碼。
- NONE:未應用編碼。
由於歷史原因和向后兼容性,將RestTemplate設置為EncodingMode.URI_COMPONENT。 WebClient依賴於DefaultUriBuilderFactory中的默認值,該默認值已從5.0.x中的EncodingMode.URI_COMPONENT更改為5.1中的EncodingMode.TEMPLATE_AND_VALUES。
1.7. CORS
Spring WebFlux使您可以處理CORS(跨源資源共享)。本節介紹如何執行此操作。
1.7.1. Introduction
出於安全原因,瀏覽器禁止AJAX調用當前來源以外的資源。例如,您可以在一個標簽頁中擁有您的銀行帳戶,而在另一個標簽頁中擁有evil.com。來自evil.com的腳本不應使用您的憑據向您的銀行API發出AJAX請求。例如,從您的帳戶中提取資金!
跨域資源共享(CORS)是由大多數瀏覽器實現的W3C規范,可讓您指定授權哪種類型的跨域請求,而不是使用基於IFRAME或JSONP的安全性較低且功能較弱的變通辦法。
1.7.2. Processing
CORS規范區准備階段,簡單和實際要求。要了解CORS的工作原理,您可以閱讀本文以及其他內容,或者參閱規范以獲取更多詳細信息。
Spring WebFlux HandlerMapping實現為CORS提供內置支持。成功將請求映射到處理程序后,HandlerMapping將檢查給定請求和處理程序的CORS配置,並采取進一步的措施。飛行前請求直接處理,而簡單和實際的CORS請求被攔截,驗證並設置了所需的CORS響應標頭。
為了啟用跨域請求(即存在Origin標頭,並且與請求的主機不同),您需要具有一些顯式聲明的CORS配置。如果找不到匹配的CORS配置,則飛行前請求將被拒絕。沒有將CORS標頭添加到簡單和實際CORS請求的響應中,因此,瀏覽器拒絕了它們。
可以使用基於URL模式的CorsConfiguration映射分別配置每個HandlerMapping。在大多數情況下,應用程序使用WebFlux Java配置聲明此類映射,從而導致將單個全局映射傳遞給所有HandlerMapping實現。
您可以將HandlerMapping級別的全局CORS配置與更細粒度的處理程序級別的CORS配置結合使用。例如,帶注釋的控制器可以使用類或方法級別的@CrossOrigin注釋(其他處理程序可以實現CorsConfigurationSource)。
全局和本地配置組合的規則通常是相加的,例如,所有全局和所有本地起源。對於只能接受單個值的那些屬性(例如allowCredentials和maxAge),局部屬性將覆蓋全局值。有關更多詳細信息,請參見CorsConfiguration#combine(CorsConfiguration)。
1.7.3. @CrossOrigin
@CrossOrigin批注啟用帶注釋的控制器方法上的跨域請求,如以下示例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
By default, @CrossOrigin
allows:
- All origins.
- All headers.
- All HTTP methods to which the controller method is mapped.
默認情況下,allowCredentials未啟用,因為它建立了一個信任級別,可以公開敏感的用戶特定信息(例如cookie和CSRF令牌),並且僅在適當的地方使用。啟用后,必須將allowOrigins設置為一個或多個特定域(而不是特殊值“ *”),或者可將allowOriginPatterns屬性用於匹配動態的一組原點。
maxAge設置為30分鍾。
在類級別也支持@CrossOrigin,並且所有方法都繼承了@CrossOrigin。以下示例指定了一個特定域,並將maxAge設置為一個小時:
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
您可以在類和方法級別使用@CrossOrigin,如以下示例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Mono<Account> retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public Mono<Void> remove(@PathVariable Long id) {
// ...
}
}
1.7.4. Global Configuration
除了細粒度的控制器方法級配置外,您可能還想定義一些全局CORS配置。您可以在任何HandlerMapping上分別設置基於URL的CorsConfiguration映射。但是,大多數應用程序都使用WebFlux Java配置來執行此操作。
默認情況下,全局配置啟用以下功能:
- All origins.
- All headers.
GET
,HEAD
, andPOST
methods.
默認情況下,allowedCredentials未啟用,因為它建立了一個信任級別,可以公開敏感的用戶特定信息(例如cookie和CSRF令牌),並且僅在適當的地方使用。啟用后,必須將allowOrigins設置為一個或多個特定域(而不是特殊值“ *”),或者可將allowOriginPatterns屬性用於匹配動態的一組原點。
maxAge設置為30分鍾。
要在WebFlux Java配置中啟用CORS,可以使用CorsRegistry回調,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
1.7.5. CORS WebFilter
您可以通過內置的CorsWebFilter應用CORS支持,該功能非常適合功能性端點。
If you try to use the CorsFilter with Spring Security, keep in mind that Spring Security has built-in support for CORS. |
|
---|---|
要配置過濾器,可以聲明一個CorsWebFilter bean並將CorsConfigurationSource傳遞給其構造函數,如以下示例所示:
@Bean
CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
1.8. Web Security
Spring Security項目為保護Web應用程序免受惡意利用提供了支持。請參閱Spring Security參考文檔,包括:
1.9. View Technologies
Spring WebFlux中視圖技術的使用是可插入的。是否決定使用Thymeleaf,FreeMarker或其他某種視圖技術主要取決於配置更改。本章介紹了與Spring WebFlux集成的視圖技術。我們假設您已經熟悉View Resolution。
1.9.1. Thymeleaf
Thymeleaf是一種現代的服務器端Java模板引擎,它強調可以通過雙擊在瀏覽器中預覽的自然HTML模板,這對於獨立處理UI模板(例如,由設計人員)非常有用,而無需使用正在運行的服務器。 Thymeleaf提供了廣泛的功能集,並且正在積極地開發和維護。有關更完整的介紹,請參見Thymeleaf項目主頁。
Thymeleaf與Spring WebFlux的集成由Thymeleaf項目管理。該配置涉及一些bean聲明,例如SpringResourceTemplateResolver,SpringWebFluxTemplateEngine和ThymeleafReactiveViewResolver。有關更多詳細信息,請參見Thymeleaf + Spring和WebFlux集成公告。
1.9.2. FreeMarker
Apache FreeMarker是一個模板引擎,用於生成從HTML到電子郵件等的任何類型的文本輸出。 Spring框架具有內置的集成,可以將Spring WebFlux與FreeMarker模板一起使用。
View Configuration
以下示例顯示了如何將FreeMarker配置為一種視圖技術:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
return configurer;
}
}
您的模板需要存儲在FreeMarkerConfigurer指定的目錄中,如上例所示。給定上述配置,如果您的控制器返回視圖名稱welcome,則解析程序將查找類路徑:/templates/freemarker/welcome.ftl模板。
FreeMarker Configuration
您可以通過在FreeMarkerConfigurer bean上設置適當的bean屬性,將FreeMarker的“設置”和“ SharedVariables”直接傳遞給FreeMarker配置對象(由Spring管理)。 freemarkerSettings屬性需要一個java.util.Properties對象,而freemarkerVariables屬性需要一個java.util.Map。以下示例顯示了如何使用FreeMarkerConfigurer:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// ...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
Map<String, Object> variables = new HashMap<>();
variables.put("xml_escape", new XmlEscape());
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
configurer.setFreemarkerVariables(variables);
return configurer;
}
}
有關設置和變量應用於Configuration對象的詳細信息,請參見FreeMarker文檔。
Form Handling
Spring提供了一個供JSP使用的標簽庫,其中包含一個元素。該元素主要允許表單顯示來自表單支持對象的值,並顯示來自Web或業務層中Validator的驗證失敗的結果。 Spring還支持FreeMarker中的相同功能,並帶有用於生成表單輸入元素本身的附加便利宏。
The Bind Macros
FreeMarker的spring-webflux.jar文件中維護了一組標准宏,因此它們始終可用於經過適當配置的應用程序。
Spring模板庫中定義的某些宏被視為內部(私有)宏,但是在宏定義中不存在這種范圍,使所有宏對調用代碼和用戶模板可見。以下各節僅關注您需要從模板內直接調用的宏。如果您希望直接查看宏代碼,則該文件名為spring.ftl,位於org.springframework.web.reactive.result.view.freemarker包中。
有關綁定支持的更多詳細信息,請參見Spring MVC的簡單綁定。
Form Macros
有關Spring對FreeMarker模板的表單宏支持的詳細信息,請參閱Spring MVC文檔的以下部分。
1.9.3. Script Views
Spring框架具有內置的集成,可以將Spring WebFlux與可以在JSR-223 Java腳本引擎之上運行的任何模板庫一起使用。下表顯示了我們在不同腳本引擎上測試過的模板庫:
Scripting Library | Scripting Engine |
---|---|
Handlebars | Nashorn |
Mustache | Nashorn |
React | Nashorn |
EJS | Nashorn |
ERB | JRuby |
String templates | Jython |
Kotlin Script templating | Kotlin |
Requirements
您需要在類路徑上具有腳本引擎,其細節因腳本引擎而異:
- Java 8+隨附了Nashorn JavaScript引擎。強烈建議使用可用的最新更新版本。
- JRuby 應該將JRuby添加為對Ruby支持的依賴。
- Jython 應該添加為對Python支持的依賴。
org.jetbrains.kotlin:kotlin-script-util
dependency and aMETA-INF/services/javax.script.ScriptEngineFactory
file containing aorg.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
line should be added for Kotlin script support. See this example for more detail.
您需要具有腳本模板庫。針對Javascript的一種方法是通過WebJars。
Script Templates
您可以聲明一個ScriptTemplateConfigurer bean來指定要使用的腳本引擎,要加載的腳本文件,調用呈現模板的函數等等。以下示例使用Mustache模板和Nashorn JavaScript引擎:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
使用以下參數調用render函數:
String template
: The template contentMap model
: The view modelRenderingContext renderingContext
: TheRenderingContext
that gives access to the application context, the locale, the template loader, and the URL (since 5.0)
Mustache.render()與該簽名本地兼容,因此您可以直接調用它。
如果您的模板技術需要一些自定義,則可以提供一個實現自定義渲染功能的腳本。例如,Handlerbars需要在使用模板之前先對其進行編譯,並且需要使用polyfill來模擬服務器端腳本引擎中不可用的某些瀏覽器功能。以下示例顯示如何設置自定義渲染功能:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
Setting the sharedEngine property to false is required when using non-thread-safe script engines with templating libraries not designed for concurrency, such as Handlebars or React running on Nashorn. In that case, Java SE 8 update 60 is required, due to this bug, but it is generally recommended to use a recent Java SE patch release in any case. |
|
---|---|
polyfill.js僅定義Handlebars正常運行所需的window對象,如以下代碼片段所示:
var window = {};
這個基本的render.js實現在使用模板之前先對其進行編譯。生產就緒的實現還應該存儲和重用緩存的模板或預編譯的模板。這可以在腳本端以及您需要的任何自定義(例如,管理模板引擎配置)上完成。以下示例顯示了如何編譯模板:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
查看Spring Framework單元測試,Java和資源,以獲取更多配置示例。
1.9.4. JSON and XML
出於內容協商的目的,根據客戶端請求的內容類型,能夠在使用HTML模板呈現模型或以其他格式(例如JSON或XML)呈現模型之間進行切換非常有用。為了支持此操作,Spring WebFlux提供了HttpMessageWriterView,您可以使用它插入spring-web中的任何可用編解碼器,例如Jackson2JsonEncoder,Jackson2SmileEncoder或Jaxb2XmlEncoder。
與其他視圖技術不同,HttpMessageWriterView不需要ViewResolver,而是配置為默認視圖。您可以配置一個或多個此類默認視圖,並包裝不同的HttpMessageWriter實例或Encoder實例。在運行時使用與請求的內容類型匹配的內容。
在大多數情況下,模型包含多個屬性。要確定要序列化的對象,可以使用模型屬性的名稱配置HttpMessageWriterView進行渲染。如果模型僅包含一個屬性,則使用該屬性。
1.10. HTTP Caching
HTTP緩存可以顯着提高Web應用程序的性能。 HTTP緩存圍繞Cache-Control響應標頭和后續的條件請求標頭(例如Last-Modified和ETag)。 Cache-Control建議私有(例如瀏覽器)和公共(例如代理)緩存如何緩存和重新使用響應。 ETag標頭用於發出條件請求,如果內容未更改,則可能導致沒有主體的304(NOT_MODIFIED)。 ETag可以看作是Last-Modified頭的更復雜的后繼者。
本節描述了Spring WebFlux中與HTTP緩存相關的選項。
1.10.1. CacheControl
CacheControl支持配置與Cache-Control標頭相關的設置,並在許多地方作為參數被接受:
RFC 7234描述了Cache-Control響應標頭的所有可能的指令,而CacheControl類型采用了針對用例的方法,着重於常見方案,如以下示例所示:
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
1.10.2. Controllers
控制器可以添加對HTTP緩存的顯式支持。我們建議您這樣做,因為需要先計算資源的lastModified或ETag值,然后才能將其與條件請求標頭進行比較。控制器可以將ETag和Cache-Control設置添加到ResponseEntity,如以下示例所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
如果與條件請求標頭的比較表明內容未更改,則前面的示例發送帶有空主體的304(NOT_MODIFIED)響應。否則,ETag和Cache-Control標頭將添加到響應中。
您還可以在控制器中針對條件請求標頭進行檢查,如以下示例所示:
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {
long eTag = ...
if (exchange.checkNotModified(eTag)) {
return null;
}
model.addAttribute(...);
return "myViewName";
}
可以使用三種變體來檢查針對eTag值,lastModified值或兩者的條件請求。對於有條件的GET和HEAD請求,可以將響應設置為304(NOT_MODIFIED)。對於條件POST,PUT和DELETE,您可以將響應設置為412(PRECONDITION_FAILED),以防止並發修改。
1.10.3. Static Resources
您應該為靜態資源提供Cache-Control和條件響應標頭,以實現最佳性能。請參閱有關配置靜態資源的部分。
1.11. WebFlux Config
WebFlux Java配置聲明使用帶注釋的控制器或功能端點來聲明處理請求所必需的組件,並且它提供了用於自定義配置的API。這意味着您不需要了解Java配置創建的底層bean。但是,如果您想了解它們,則可以在WebFluxConfigurationSupport中查看它們,或閱讀有關特殊Bean類型中的內容的更多信息。
對於配置API中沒有的更高級的自定義設置,您可以通過“高級配置模式”獲得對配置的完全控制。
1.11.1. Enabling WebFlux Config
您可以在Java配置中使用@EnableWebFlux批注,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig {
}
前面的示例注冊了許多Spring WebFlux基礎結構Bean,並適應了classpath上可用的依賴關系(對於JSON,XML等)。
1.11.2. WebFlux config API
在Java配置中,可以實現WebFluxConfigurer接口,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}
1.11.3. Conversion, formatting
默認情況下,將安裝各種數字和日期類型的格式化程序,並支持通過字段上的@NumberFormat和@DateTimeFormat自定義。
要在Java配置中注冊自定義格式器和轉換器,請使用以下命令:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
默認情況下,Spring WebFlux在解析和格式化日期值時會考慮請求區域設置。這適用於使用“輸入”表單字段將日期表示為字符串的表單。但是,對於“日期”和“時間”表單字段,瀏覽器使用HTML規范中定義的固定格式。在這種情況下,日期和時間格式可以按以下方式自定義:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
1.11.4. Validation
默認情況下,如果Bean驗證存在於類路徑中(例如,Hibernate Validator),則LocalValidatorFactoryBean將注冊為全局驗證器,以與@Valid和@Controller方法參數中的@Validated一起使用。
在Java配置中,您可以自定義全局Validator實例,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public Validator getValidator(); {
// ...
}
}
請注意,您還可以在本地注冊Validator實現,如以下示例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
1.11.5. Content Type Resolvers
您可以配置Spring WebFlux如何根據請求為@Controller實例確定所請求的媒體類型。默認情況下,僅選中Accept標頭,但您也可以啟用基於查詢參數的策略。
以下示例顯示如何自定義請求的內容類型解析:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}
1.11.6. HTTP message codecs
以下示例顯示如何自定義讀取和寫入請求和響應正文的方式:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(512 * 1024);
}
}
ServerCodecConfigurer提供了一組默認的讀取器和寫入器。您可以使用它來添加更多讀取器和寫入器,自定義默認讀取器或完全替換默認讀取器。
對於Jackson JSON和XML,請考慮使用Jackson2ObjectMapperBuilder,該工具使用以下屬性自定義Jackson的默認屬性:
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
is disabled.MapperFeature.DEFAULT_VIEW_INCLUSION
is disabled.
如果在類路徑中檢測到以下知名模塊,它將自動注冊以下知名模塊:
jackson-datatype-joda
: Support for Joda-Time types.jackson-datatype-jsr310
: Support for Java 8 Date and Time API types.jackson-datatype-jdk8
: Support for other Java 8 types, such asOptional
.jackson-module-kotlin
: Support for Kotlin classes and data classes.
1.11.7. View Resolvers
下面的示例顯示如何配置視圖分辨率:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}
ViewResolverRegistry具有與Spring Framework集成的視圖技術的快捷方式。以下示例使用FreeMarker(這也需要配置基礎FreeMarker視圖技術):
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure Freemarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("classpath:/templates");
return configurer;
}
}
您還可以插入任何ViewResolver實現,如以下示例所示:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ViewResolver resolver = ... ;
registry.viewResolver(resolver);
}
}
為了支持“ Content Negotiation ”並通過視圖分辨率(除HTML之外)呈現其他格式,您可以基於HttpMessageWriterView實現配置一個或多個默認視圖,該實現接受spring-web中的任何可用編解碼器。以下示例顯示了如何執行此操作:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}
有關與Spring WebFlux集成的視圖技術的更多信息,請參見View Technologies。
1.11.8. Static Resources
此選項提供了一種方便的方法來從基於資源的位置列表中提供靜態資源。
在下一個示例中,給定一個以/ resources開頭的請求,相對路徑用於在類路徑上查找和提供相對於/ static的靜態資源。資源的有效期為一年,以確保最大程度地利用瀏覽器緩存並減少瀏覽器發出的HTTP請求。還評估Last-Modified頭,如果存在,則返回304狀態碼。以下列表顯示了示例:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
資源處理程序還支持一系列ResourceResolver實現和ResourceTransformer實現,可用於創建用於優化資源的工具鏈。
您可以根據從內容,固定應用程序版本或其他信息計算出的MD5哈希,將VersionResourceResolver用於版本化的資源URL。 ContentVersionStrategy(MD5哈希)是一個不錯的選擇,但有一些明顯的例外(例如與模塊加載器一起使用的JavaScript資源)。
以下示例顯示如何在Java配置中使用VersionResourceResolver:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
您可以使用ResourceUrlProvider重寫URL並應用完整的解析器和轉換器鏈(例如,插入版本)。 WebFlux配置提供了ResourceUrlProvider,以便可以將其注入其他資源。
與Spring MVC不同,目前,在WebFlux中,由於沒有視圖技術可以利用解析器和轉換器的無阻塞鏈,因此無法透明地重寫靜態資源URL。當僅提供本地資源時,解決方法是直接使用ResourceUrlProvider(例如,通過自定義元素)並進行阻止。
請注意,在同時使用EncodedResourceResolver(例如,Gzip,Brotli編碼)和VersionedResourceResolver時,必須按該順序注冊它們,以確保始終基於未編碼文件可靠地計算基於內容的版本。
WebJars也通過WebJarsResourceResolver支持,當org.webjars:webjars-locator-core庫存在於類路徑中時,WebJars將自動注冊。解析程序可以重寫URL以包括jar的版本,還可以與沒有版本的傳入URL進行匹配,例如,從/jquery/jquery.min.js到/jquery/1.2.0/jquery.min.js。
1.11.9. Path Matching
您可以自定義與路徑匹配有關的選項。有關各個選項的詳細信息,請參見PathMatchConfigurer javadoc。以下示例顯示如何使用PathMatchConfigurer:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
}
1.11.10. WebSocketService
WebFlux Java配置聲明了一個WebSocketHandlerAdapter bean,該bean為WebSocket處理程序的調用提供支持。這意味着要處理WebSocket握手請求,剩下要做的就是通過SimpleUrlHandlerMapping將WebSocketHandler映射到URL。
在某些情況下,可能需要使用提供的WebSocketService服務創建WebSocketHandlerAdapter bean,該服務允許配置WebSocket服務器屬性。例如:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public WebSocketService getWebSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
1.11.11. Advanced Configuration Mode
@EnableWebFlux導入DelegatingWebFluxConfiguration:
為WebFlux應用程序提供默認的Spring配置
檢測並委托給WebFluxConfigurer實現以自定義該配置。
對於高級模式,您可以刪除@EnableWebFlux並直接從DelegatingWebFluxConfiguration擴展而不是實現WebFluxConfigurer,如以下示例所示:
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}
您可以將現有方法保留在Web Config中,但是現在您還可以覆蓋基類中的bean聲明,並且在類路徑上仍然具有任意數量的其他WebMvcConfigurer實現。
1.12. HTTP/2
Reactor Netty,Tomcat,Jetty和Undertow支持HTTP / 2。但是,有一些與服務器配置有關的注意事項。有關更多詳細信息,請參見HTTP / 2 Wiki頁面。
2. WebClient
Spring WebFlux包括一個用於執行HTTP請求的客戶端。 WebClient具有一個基於Reactor的功能性,流暢的API,請參閱Reactive Libraries,它可以以聲明方式構成異步邏輯,而無需處理線程或並發。它是完全非阻塞的,它支持流傳輸,並且依賴於相同的編解碼器,該編解碼器還用於在服務器端對請求和響應內容進行編碼和解碼。
WebClient需要一個HTTP客戶端庫來執行請求。內置支持以下內容:
- Reactor Netty
- Jetty Reactive HttpClient
- Apache HttpComponents
- Others can be plugged via
ClientHttpConnector
.
2.1. Configuration
創建WebClient的最簡單方法是通過靜態工廠方法之一:
WebClient.create()
WebClient.create(String baseUrl)
您還可以將WebClient.builder()
與其他選項一起使用:s
uriBuilderFactory
:自定義的UriBuilderFactory用作基本URL。defaultUriVariables
: 擴展URI模板時使用的默認值。defaultHeader
:默認請求頭defaultCookie
: 默認 cookiedefaultRequest
: 默認請求filter
: 默認客戶端攔截器exchangeStrategies
: 定制HTTP消息讀取、寫入。clientConnector
: HTTP客戶端庫設置.
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
建立之后,WebClient
是不可變的。但是,您可以克隆它並按如下所示構建修改后的副本:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
2.1.1. MaxInMemorySize
編解碼器具有在內存中緩沖數據的限制,以避免出現應用程序內存問題。默認情況下,這些設置為256KB。如果這還不夠,則會出現以下錯誤:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
要更改默認編解碼器的限制,請使用以下命令:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
2.1.2. Reactor Netty
要自定義Reactor Netty設置,請提供一個預配置的HttpClient:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Resources
默認情況下,HttpClient會參與Reactor.netty.http.HttpResources中包含的全局Reactor Netty資源,包括事件循環線程和連接池。這是推薦的模式,因為固定的共享資源是事件循環並發的首選。在這種模式下,全局資源將保持活動狀態,直到進程退出。
如果服務器與進程同步,通常不需要顯式關閉。但是,如果服務器可以啟動或停止進程內(例如,部署為WAR的Spring MVC應用程序),則可以聲明類型為ReactorResourceFactory的Spring托管Bean,其具有globalResources = true(默認值)以確保Reactor關閉Spring ApplicationContext時,將關閉Netty全局資源,如以下示例所示:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
您也可以選擇不參與全局Reactor Netty資源。但是,在這種模式下,確保所有Reactor Netty客戶端和服務器實例使用共享資源是您的重擔,如以下示例所示:
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper);
return WebClient.builder().clientConnector(connector).build();
}
Timeouts
要配置連接超時:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
要配置讀取或寫入超時:
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...
要為所有請求配置響應超時:
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
要為特定請求配置響應超時:
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
2.1.3. Jetty
以下示例顯示如何自定義Jetty HttpClient設置:
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
默認情況下,HttpClient創建自己的資源(執行程序,ByteBufferPool,調度程序),這些資源將保持活動狀態,直到進程退出或調用stop()為止。
您可以在Jetty客戶端(和服務器)的多個實例之間共享資源,並通過聲明類型為JettyResourceFactory的Spring托管Bean來確保在關閉Spring ApplicationContext時關閉資源,如以下示例所示:
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory());
return WebClient.builder().clientConnector(connector).build();
}
2.1.4. HttpComponents
以下示例顯示了如何自定義Apache HttpComponents HttpClient設置:
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
2.2. retrieve()
Retrieve()方法可用於聲明如何提取響應。例如:
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
或者獲取 JSON body
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
或者獲取解碼對象流
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
默認情況下,4xx或5xx響應會導致WebClientResponseException,包括特定HTTP狀態代碼的子類。要自定義錯誤響應的處理,請使用onStatus處理程序,如下所示:
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
2.3. Exchange
exchangeToMono()和exchangeToFlux()方法(或Kotlin中的awaitExchange {}和exchangeToFlow {})對於需要更多控制的更高級情況很有用,例如根據響應狀態不同地解碼響應:
Mono<Object> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
// Suppress error status code
return response.bodyToMono(ErrorContainer.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
使用上述方法時,在返回的Mono或Flux完成后,將檢查響應主體,如果未消耗響應主體,則將其釋放以防止內存和連接泄漏。因此,無法在下游進一步解碼響應。如果需要,由提供的函數聲明如何解碼響應。
2.4. Request Body
可以使用ReactiveAdapterRegistry處理的任何異步類型對請求主體進行編碼,例如Mono或Deferred的Kotlin Coroutines,如以下示例所示:
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
您還可以對對象流進行編碼,如以下示例所示:
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);
或者,如果您具有實際值,則可以使用bodyValue快捷方式,如以下示例所示:
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
2.4.1. Form Data
要發送表單數據,可以提供MultiValueMap 作為正文。請注意,內容由FormHttpMessageWriter自動設置為application / x-www-form-urlencoded。下面的示例演示如何使用MultiValueMap :
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
您還可以使用BodyInserters在線提供表單數據,如以下示例所示:
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
2.4.2. Multipart Data
要發送多部分數據,您需要提供一個MultiValueMap ,其值可以是代表零件內容的Object實例或代表零件內容和標題的HttpEntity實例。MultipartBodyBuilder提供了一個方便的API來准備多部分請求。下面的示例演示如何創建MultiValueMap :
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
在大多數情況下,您不必為每個部分指定Content-Type。內容類型是根據選擇用於對其進行序列化的HttpMessageWriter自動確定的,對於資源來說,取決於文件擴展名。如有必要,您可以通過重載的構建器part方法之一顯式提供MediaType以供每個零件使用。
准備好MultiValueMap之后,將其傳遞給WebClient的最簡單方法是通過body方法,如以下示例所示:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
如果MultiValueMap包含至少一個非String值,它也可以表示常規表單數據(即application / x-www-form-urlencoded),則無需將Content-Type設置為multipart / form-data。使用MultipartBodyBuilder時,總是這樣,以確保HttpEntity包裝器。
作為MultipartBodyBuilder的替代方案,您還可以通過內置的BodyInserters以內聯樣式提供多部分內容,如以下示例所示:
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
2.5. Filters
您可以通過WebClient.Builder注冊客戶端過濾器(ExchangeFilterFunction),以攔截和修改請求,如以下示例所示:
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
這可以用於跨領域的關注,例如身份驗證。以下示例使用過濾器通過靜態工廠方法進行基本身份驗證:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
您可以通過使用另一個實例作為起點來創建新的WebClient實例。這允許在不影響原始WebClient的情況下插入或刪除過濾器。以下是在索引0處插入基本身份驗證過濾器的示例:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
2.6. Attributes
您可以向請求添加屬性。如果要通過篩選器鏈傳遞信息並影響給定請求的篩選器行為,這將很方便。例如:
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
請注意,可以在WebClient.Builder級別上全局配置defaultRequest回調,該回調使您可以將屬性插入所有請求,例如,可以在Spring MVC應用程序中使用該屬性來基於ThreadLocal數據填充請求屬性。
2.7. Context
屬性提供了一種將信息傳遞到篩選器鏈的便捷方法,但是它們僅影響當前請求。如果您想傳遞傳播到嵌套的其他請求的信息,例如通過flatMap或在之后執行通過concatMap,則需要使用Reactor Context。
為了應用於所有操作,需要在反應鏈的末尾填充Reactor上下文。例如:
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));
2.8. Synchronous Use
通過在結果末尾進行阻塞,可以以同步方式使用WebClient:
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
但是,如果需要進行多次通話,則可以避免單獨阻塞每個響應,而不必等待合並結果,這樣會更有效:
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();
以上僅是一個示例。還有許多其他模式和運算符可用於構建響應式管道,該響應式管道可進行許多遠程調用(可能是嵌套的,相互依賴的),而不會阻塞到最后。
使用Flux或Mono,您永遠不必阻塞Spring MVC或Spring WebFlux控制器。只需從controller方法返回結果類型即可。相同的原則適用於Kotlin Coroutines和Spring WebFlux,只需在控制器方法中使用暫停功能或返回Flow即可。
2.9. Testing
若要測試使用WebClient的代碼,可以使用模擬Web服務器,例如OkHttp MockWebServer。要查看其用法示例,請查看Spring Framework測試套件中的WebClientIntegrationTests或OkHttp存儲庫中的靜態服務器示例。