@TOC
Version 5.2.0.RELEASE
本文檔是介紹響應式技術(shù)棧彼乌,它基于響應式流API構(gòu)建,運行于Netty、Undertow捧灰、Servlet 3.1+等非阻塞服務(wù)容器之上。之后的章節(jié)分別介紹了Spring WebFlux框架鼎姐、響應式WebClient奈惑、測試支持及響應式庫秽浇。有關(guān)Servlet技術(shù)棧的web應用文檔雀鹃,請轉(zhuǎn)到Servlet Web技術(shù)棧掸读。
1 Spring WebFlux
Spring中原本的的web框架,Spring Web MVC倍权,是為Servlet API和Servlet容器構(gòu)建的。而響應式web框架WebFlux是在5.0版本添加的衙四。它是完全非阻塞的泉哈,且支持響應式流背壓,可以運行在Netty到旦、Undertow和Servlet 3.1+等容器上旨巷。
這兩個框架的模塊名模塊名spring-webmvc和spring-webflux是它們各自的寫照巨缘,在Spring框架中共存添忘。應用程序即可擇其一用之,在某些情況下亦可同時用之若锁。例如使用基于響應式WebClient
的Spring MVC控制器搁骑。
1.1 概述
為什么要開發(fā)Spring Webflux呢?
一部分原因是需要通過非阻塞web技術(shù)棧使用更少量的線程及硬件資源處理并發(fā)又固。Servlet 3.1的確提供了非租塞I/O相關(guān)API仲器,然而,這些API和原有的Servlet API仰冠,如同步的(Filter
乏冀、Servlet
)或阻塞的(getParameter
、getPart
)相去甚遠洋只。這就是設(shè)計新的通用非阻塞運行時基礎(chǔ)API的動機辆沦。因為像Netty這樣的服務(wù)器在異步昼捍、非阻塞方面做得非常好,所以通用性肢扯、基礎(chǔ)性非常重要妒茬。
另一部分原因是函數(shù)式編程。就像Java 5注解的引入帶來的的增強(基于注解的REST控制器或單元測試)蔚晨,Java 8引入的lambda表達式使Java擁有了使用函數(shù)式API的機會乍钻。它有助于構(gòu)建非阻塞應用和聲明式組合異步邏輯的持續(xù)式風格API(流行于CompletableFuture
和ReactiveX)的使用。從編程模型來看铭腕,Java 8使Spring WebFlux結(jié)合注解控制器提供函數(shù)式web終端成為可能银择。
1.1.1 “響應式”的定義
我們一直在談?wù)摗胺亲枞焙汀昂瘮?shù)式”,但響應式到底是什么意思呢累舷?
術(shù)語“響應式”是指圍繞對變化的響應而構(gòu)建的編程模型欢摄,例如,網(wǎng)絡(luò)組件響應I/O事件笋粟、UI控件響應鼠標事件等怀挠。這種場景是非阻塞的,因為在操作完成或數(shù)據(jù)可用時才做出反應害捕,在這之前程序不會因等待而阻塞绿淋。
Spring團隊在“響應式”上提供的另一重要機制是非阻塞背壓。在同步的尝盼、命令式的代碼中吞滞,強制調(diào)用方等待的阻塞構(gòu)成了背壓的自然形式。而在非阻塞代碼中盾沫,為了不讓速度快的生產(chǎn)者沖垮目標裁赠,控制事件的流速變得重要起來。
響應式流是一個小型標準(亦被Java 9采納)赴精,定義了背壓場景下異步組件間的交互佩捞。例如,數(shù)據(jù)倉庫(作為發(fā)布者)產(chǎn)生數(shù)據(jù)蕾哟,HTTP服務(wù)器(作為訂閱者)讀取并寫入響應一忱。響應式流的主要目的就是使訂閱者可以控制發(fā)布者生產(chǎn)數(shù)據(jù)的快慢。
通常疑問:如果發(fā)布者不能慢下來怎么辦谭确?
響應式流只建立機制和邊界帘营。如果發(fā)布者不能慢下來,則考慮使用緩沖區(qū)逐哈、拋棄或讓操作失敗芬迄。
1.1.2 響應式API
響應式流在互操作性上扮演了重要角色。它關(guān)注的是庫的基礎(chǔ)設(shè)施組件昂秃,因為太底層了禀梳,所以在應用API上用處不大择诈。應用程序需要豐富的高級函數(shù)式API組合異步邏輯,和Java 8 的Stream
API相似出皇,但不僅僅是集合操作羞芍。這就輪到響應式庫登場了。
Spring WebFlux選擇的是Reactor響應式庫郊艘。它的API提供了Mono類型和Flux類型荷科,可以使用一系列基于ReactiveX的操作符分別操作0..1(Mono
)和0..N(Flux
)的數(shù)據(jù)序列。Reactor是一個響應式流庫纱注,故所有操作都支持非阻塞背壓畏浆。Reactor重點關(guān)注服務(wù)器端Java。在開發(fā)時就注重和Spring的協(xié)作狞贱。
Reactor是WebFlux的核心依賴刻获,但WebFlux也可以和其他響應式流庫合作。WebFlux API的通用模式是:接收普通的Publisher
作為輸入瞎嬉,在內(nèi)部將其轉(zhuǎn)為Reactor類型并使用蝎毡,返回Flux
或Mono
作為輸出。所以可以傳入任意類型的Publisher
作為輸入氧枣,并在輸出上執(zhí)行操作沐兵。但需要將輸出和所選響應式庫進行適配。只要可行(如使用注解的控制器)便监,WebFlux就會透明地適配RxJava或其他響應式庫扎谎。參考響應式庫獲取更多詳細信息。
作為響應式API的擴展烧董,WebFlux同時支持在Kotlin中使用協(xié)程API毁靶,它可以讓編程風格更加命令式。后文將提供使用協(xié)程API的Kotlin代碼示例逊移。
1.1.3 編程模型
spring-web
模塊包含了Spring WebFlux的響應式基礎(chǔ)部分预吆,包括HTTP抽象、響應式流對所支持服務(wù)器的適配器螟左、編解碼器以及類似Servlet API但非阻塞的核心Web處理器API啡浊。
在這之上,Spring WebFlux提供了兩種編程模型:
-
使用注解的控制器 - 順承Spring MVC胶背,同樣基于
spring-web
模塊的注解。Spring MVC和WebFlux都支持響應式(Reactor喘先、RxJava)返回類型钳吟,彼此分辨并不容易。值得注意的一點不同是WebFlux還支持響應式@RequestBody
參數(shù)窘拯。 - 函數(shù)式終端 - 基于lambda红且、輕量級坝茎、函數(shù)式編程模型∠痉可以將其視為一個小型代碼庫或一系列工具方法嗤放,應用程序用它處理路由和請求。函數(shù)式終端和使用注解的控制器間最大的區(qū)別在于壁酬,前者負責從頭到尾的請求處理次酌,后者通過注解聲明意圖并回調(diào)。
1.1.4 適用性
Spring MVC 還是 WebFlux舆乔?
這是一個自然的提問岳服,但也包含了不必要的對立。實際上希俩,兩者可以同時工作吊宋,拓寬了可選項的范圍。兩者在設(shè)計上和對方是持續(xù)統(tǒng)一的颜武,它們可以并肩作戰(zhàn)璃搜,雙方都可以享受到對方帶來的好處。下圖展示了兩者的關(guān)系鳞上,哪些是它們共有的腺劣,哪些是各自獨有的:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-PPZVeUoe-1571103878397)(https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/images/spring-mvc-and-webflux-venn.png)]
我們建議您考慮如下要素:
- 如果應用使用Spring MVC工作的很好,那就沒必要更換因块。命令式程序代碼在編寫橘原、理解和調(diào)試上都是最簡單的。而且涡上,因為歷史原因趾断,應用中選用的絕大多數(shù)類庫都是阻塞的。
- 如果您已經(jīng)考察過非阻塞web技術(shù)棧吩愧,Spring WebFlux提供相同的執(zhí)行模型優(yōu)勢芋酌,而且還提供了多種服務(wù)器選項(Netty、Tomcat雁佳、Jetty脐帝、Undertow、Servlet 3.1+容器)糖权、多種編程模型(使用注解的控制器和函數(shù)式web終端)及多種響應式庫選項(Reactor堵腹、RxJava等)。
- 如果對使用Kotlin或Java 8 lambda表達式的輕量級函數(shù)式web框架感興趣星澳,可以使用Spring WebFlux的函數(shù)式web終端疚顷。對小型應用或沒有復雜依賴的微服務(wù)也是個不錯的選擇,可以從更好的透明度和控制中受益。
- 在微服務(wù)架構(gòu)中腿堤,Spring MVC和Spring WebFlux控制器或Spring WebFlux函數(shù)式終端可以混合使用阀坏。在框架間使用相同的注解編程模型不僅使知識的復用更加簡單,也是用正確的工具做正確的事笆檀。
- 簡單地評估應用的一個方法是檢查其依賴忌堂。如果有阻塞持久化API(JPA、JDBC)或阻塞網(wǎng)絡(luò)API的使用酗洒,Spring MVC至少是通用架構(gòu)的最佳選擇士修。使用Reactor和RxJava在不同的線程上處理阻塞式調(diào)用在技術(shù)上是可行的,但不能充分發(fā)揮非阻塞式web技術(shù)棧的作用寝蹈。
- 如果Spring MVC應用中存在遠程服務(wù)調(diào)用李命,可以試一試響應式
WebClient
。Spring MVC控制器方法可以直接返回響應式類型(Reactor箫老、RxJava或其他)封字。調(diào)用的延遲越長,或調(diào)用間的依賴性越強耍鬓,優(yōu)勢就越大阔籽。Spring MVC也可以調(diào)用其他響應式組件。 - 如果團隊很大牲蜀,就要考慮切換至非阻塞笆制、函數(shù)式、聲明式編程陡峭的學習曲線涣达。部分切換的可行起步方案是使用響應式
WebClient
在辆。然后,一點點開始并權(quán)衡其優(yōu)勢度苔。我們期望對多數(shù)程序來說匆篓,切換是不必要的。如果不確定要尋找什么好處寇窑,可以通過學習非阻塞I/O的工作方式(例如Node.js的單線程并發(fā))及其影響開始鸦概。
1.1.5 服務(wù)器
Spring WebFlux支持Tomcat、Jetty甩骏、Servlet 3.1+及Netty和Undertow等非Servlet運行時窗市。它們都會適配底層通用API,從而在服務(wù)器間支持高級編程模型饮笛。
Spring WebFlux沒有內(nèi)建服務(wù)器啟動和關(guān)閉的支持咨察。但是通過Spring配置和WebFlux基礎(chǔ)設(shè)施可以通過數(shù)行代碼很簡單地在應用中嵌入并啟動服務(wù)器。
Spring Boot有一個WebFlux起步器缎浇,會自動執(zhí)行這些步驟扎拣。默認情況下,起步器使用Netty素跺,但可以通過修改Maven或Gradle依賴輕松切換至Tomcat二蓝、Jetty或Undertow。Spring Boot默認選擇Netty指厌,是因為它廣泛用于異步刊愚、非阻塞領(lǐng)域,而且可以使客戶端和服務(wù)器共享資源踩验。
Tomcat和Jetty既可以配合Spring MVC鸥诽,也可以配合Spring 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 性能 vs 規(guī)模
性能具有多種特征和意義移斩。響應式和非阻塞通常不能讓應用跑的更快上真,但在在某些場景下(如使用WebClient
并發(fā)執(zhí)行遠程調(diào)用)可以咬腋。總的來說睡互,以非阻塞方式執(zhí)行操作需要的工作更多根竿,且會輕微增加處理時間。
響應式和非阻塞最可預見的好處是其可以使用較少固定數(shù)量的線程和少量內(nèi)存規(guī)模的能力就珠。因為其硬件規(guī)模更加可預測寇壳,所以應用程序在欠載時更有彈性。不過嗓违,要見識到這些好處需要一定的潛伏期(包括一系列緩慢且不可預測的網(wǎng)絡(luò)I/O)九巡。這才是響應式技術(shù)棧秀肌肉的場合,其改變會極具戲劇性蹂季。
1.1.7 并發(fā)模型
Spring MVC和Spring WebFlux都支持使用注解的控制器冕广,但兩者的關(guān)鍵不同在于并發(fā)模型以及默認對阻塞和線程的假定。
Spring MVC(以及所有Servlet應用)假定應用會阻塞當前線程(如遠程調(diào)用)偿洁,由此撒汉,Servlet容器需要使用一個巨大的線程池以應對潛在的請求處理阻塞。
Spring WebFlux(以及所有非阻塞服務(wù)器)假定應用不會阻塞涕滋,因此睬辐,非阻塞服務(wù)器使用小規(guī)模固定大小的線程池(事件循環(huán)作業(yè)器)處理請求。
“擴展性”和“少量線程”聽起來是矛盾的,但永不阻塞當前線程(其依賴回調(diào))溯饵,就不用額外線程承載阻塞調(diào)用侵俗,所以也就不需要額外的線程。
調(diào)用阻塞式API
如果確實需要使用阻塞庫怎么辦丰刊?Reactor和RxJava都提供了publishOn
操作符用于在額外線程上持續(xù)處理隘谣。也就是說兼容很簡單。然而還是要記住啄巧,阻塞式API和并發(fā)模型并不能良好契合寻歧。
可變狀態(tài)
在Reactor和RxJava中,邏輯是通過操作符聲明的秩仆,之后在運行時的不同階段码泛,會組織一條響應式管線順序處理數(shù)據(jù)。這樣做的關(guān)鍵優(yōu)點是將應用從保護可變狀態(tài)中解放了出來澄耍,因為管線中的應用代碼永不會并發(fā)執(zhí)行噪珊。
線程模型
在使用了Spring WebFlux的服務(wù)器上,線程是什么樣的呢逾苫?
- 在“普通的”Spring WebFlux服務(wù)器上(例如卿城,沒有數(shù)據(jù)訪問也沒有其他可選依賴),服務(wù)器只使用一條線程加上用于處理請求的其他若干個線程(通常等于CPU的核心數(shù)量)铅搓。不過Servlet容器可能會啟動更多個線程(例如瑟押,Tomcat會啟動10個)用于同時支持Servlet(阻塞)I/O及Servlet 3.1(非阻塞)I/O的使用。
- 響應式
WebClient
的操作是事件循環(huán)風格的星掰。一簇小型固定數(shù)量的處理線程與之關(guān)聯(lián)(例如Reactor Netty連接器的reactor-http-nio-
)多望。不過,如果在客戶端和服務(wù)端同時使用Reactor Netty氢烘,默認情況下兩者會共享事件循環(huán)資源怀偷。 - Reactor和RxJava提供了名為“調(diào)度器(Scheduler)”的線程池抽象,配合
publishOn
操作符使用播玖,將處理切換到另一個線程池上椎工。調(diào)度器的名字聲明了其并發(fā)策略,例如蜀踏,“parallel(CPU在限定個數(shù)的線程上密集工作)”或“elastic(使用大量線程的I/O密集工作)”维蒙。如果看到這些線程,那就意味著某些代碼正在使用Scheduler
策略的專用線程池果覆。 - 數(shù)據(jù)訪問庫和其他第三方依賴同樣可以創(chuàng)建和使用它們自己的線程颅痊。
配置
Spring框架并不提供服務(wù)器的啟動和停止支持。要配置服務(wù)器的線程模型局待,要么使用服務(wù)器專屬的配置API斑响,要么使用Spring Boot菱属,設(shè)置Spring Boot關(guān)于不同服務(wù)器的不同配置選項。WebClient
可以直接配置舰罚。其他的庫纽门,則需要分別參考對應文檔。
1.2 響應式核心
spring-web
模塊包含對響應式web應用的基礎(chǔ)支持如下:
- 服務(wù)器請求處理有兩個級別的支持沸停。
- HttpHandler - 基于非阻塞I/O的HTTP請求處理及響應式流背壓的基礎(chǔ)約定膜毁,以及Reactor Netty昭卓、Undertow愤钾、Tomcat、Jetty和所有Servlet 3.1+容器的適配器候醒。
-
WebHandler
API - 稍高級能颁,請求處理的多用途web API,在此之上構(gòu)建了使用注解的控制器和函數(shù)式終端等編程模型倒淫。
- 對客戶端來說伙菊,基礎(chǔ)的
ClientHttpConnector
約定了通過Reactor Netty適配器和響應式Jetty HttpClient適配器發(fā)出基于非阻塞I/O的HTTP請求及響應式流背壓。應用中使用的高級WebClient基于這些基礎(chǔ)約定構(gòu)建敌土。 - 不論在客戶端還是服務(wù)端镜硕,都用編解碼器來序列化和反序列化HTTP請求和響應的內(nèi)容。
1.2.1 HttpHandler
HttpHandler是通過單個方法處理請求和響應的基礎(chǔ)約定返干。它被有意地極簡化兴枯,其主要也是唯一的目的是成為不同HTTP服務(wù)器API的最小抽象。
下文表格描述了它所支持的服務(wù)器API:
服務(wù)器名 | 所用服務(wù)器API | 支持的響應式流 |
---|---|---|
Netty | Netty API | Reactor Netty |
Undertow | Undertow API | spring-web:Undertow-響應式流橋接 |
Tomcat | Servlet 3.1非阻塞I/O矩欠;Tomcat讀取和寫入ByteBuffer和byte[]的API | spring-web:Servlet 3.1非阻塞I/O-響應式流橋接 |
Jetty | Servlet 3.1非阻塞I/O财剖;Jetty讀取和寫入ByteBuffer和byte[]的API | spring-web:Servlet 3.1非阻塞I/O-響應式流橋接 |
Servlet 3.1容器 | Servlet 3.1非阻塞I/O | spring-web:Servlet 3.1非阻塞I/O-響應式流橋接 |
下文表格描述了服務(wù)的依賴(同時參見支持的版本):
服務(wù)器名 | 組織ID | 包名 |
---|---|---|
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 |
下面的代碼展示了HttpHandler
和每種服務(wù)器API的配合使用:
Reactor Netty
Java:
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
Kotlin:
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
Undertow
Java:
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
Kotlin:
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
Tomcat
Java:
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
Kotlin:
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
Jetty
Java:
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
Kotlin:
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
Servlet 3.1+ 容器
要想以WAR的形式在任意Servlet 3.1+ 容器上部署,需要在WAR中繼承并引入AbstractReactiveWebInitializer
癌淮。該類使用ServletHttpHandlerAdapter
封裝了HttpHandler
并將其注冊為一個Servlet
躺坟。
1.2.2 WebHandler
API
基于HttpHandler
約定構(gòu)建的org.springframework.web.server
包提供了通過一條具有單個WebHandler
、多個WebFilter
及多個WebExceptionHandler
組件組成的鏈處理請求的多用途web API乳蓄。該鏈條可通過簡單指定Spring ApplicationContext
的自動檢測位置或在構(gòu)建器中注冊組件的方式和WebHttpHandlerBuilder
放在一起咪橙。
盡管HttpHandler
對使用不同HTTP服務(wù)器進行抽象的目的簡單,WebHandler
API的目標卻是提供大量web應用中廣泛使用的功能虚倒。例如:
- 用戶會話及屬性美侦。
- 請求屬性。
- 請求中解析得到的
Locale
和Principal
裹刮。 - 數(shù)據(jù)解析和緩存的訪問音榜。
- 多段數(shù)據(jù)的抽象。
- 其他…
特別的Bean類型
下表列舉的是在Spring應用上下文中WebHandlerBuilder
可自動檢測的或可直接注冊在其上的組件:
Bean名稱 | Bean類型 | 數(shù)量 | 描述 |
---|---|---|---|
任意 | WebExceptionHandler |
0..N | 提供對來自WebFilter 鏈實例及目標WebHandler 的異常的處理捧弃。更多詳情參考異常赠叼。 |
任意 | WebFilter |
0..N | 在過濾器鏈其它實例和目標WebHandler 之前和之后提供攔截式風格邏輯擦囊。更多詳情參考過濾器。 |
webHandler |
WebHandler |
1 | 請求處理器嘴办。 |
webSessionManager |
WebSessionManager |
0..1 | 通過ServerWebExchange 中一個方法暴露WebSession 實例的管理器瞬场。默認使用DefaultWebSessionManager 。 |
serverCodecConfigurer |
ServerCodecConfigurer |
0..1 | 訪問HttpMessageReader 實例解析數(shù)據(jù)和多段數(shù)據(jù)并在之后通過ServerWebExchange 中的方法暴露涧郊。默認使用ServerCodecConfigurer.create() 贯被。 |
localContextResolver |
LocalContextResolver |
0..1 | 通過ServerWebExchange 暴露的LocaleContext 的解析器。默認使用AcceptHeaderLocaleContextResolver 妆艘。 |
forwardedHeaderTransformer |
ForwardedHeaderTransformer |
0..1 | 處理轉(zhuǎn)發(fā)類頭彤灶,對其擴展或刪除。默認不使用批旺。 |
表單數(shù)據(jù)
ServerWebExchange
暴露了如下訪問表單數(shù)據(jù)的方法:
Java:
Mono<MultiValueMap<String, String>> getFormData();
Kotlin:
suspend fun getFormData(): MultiValueMap<String, String>
DefaultServerWebExchange
使用配置的HttpMessageReader
解析表單數(shù)據(jù)(application/x-www-form-urlencoded
)至一個MultiValueMap
中幌陕。默認情況下,ServerCodecConfigurer
bean配置使用FormHttpMessageReader
(參考Web Handler API)汽煮。
多段數(shù)據(jù)
ServerWebExchange
暴露了如下訪問多段數(shù)據(jù)的方法:
Java:
Mono<MultiValueMap<String, Part>> getMultipartData();
Kotlin:
suspend fun getMultipartData(): MultiValueMap<String, Part>
DefaultServerWebExchange
使用配置的HttpMessageReader<MultiValueMap<String, Part>>
解析multipart/form-data
內(nèi)容至一個MultiValueMap
中搏熄。目前,唯一一個被支持的第三方庫是Synchronoss NIO Multipart暇赤,我們也只知道這一個多段數(shù)據(jù)請求非阻塞解析庫心例。它通過ServerCodecConfigurer
配置(參考Web Handler API)。
要以流的形式解析多段數(shù)據(jù)鞋囊,可以使用HttpMessageReader<Part>
返回的Flux<Part>
代替止后。例如,在使用注解的控制器中失暴,使用@RequestPart
意味著使用Map
式根據(jù)名稱訪問各個部分坯门,意味著需要將多段數(shù)據(jù)完全解析出來。與之相反逗扒,可以使用@RequestBody
將內(nèi)容解碼為Flux<Part>
而不將其聚合為MultiValueMap
古戴。
轉(zhuǎn)發(fā)頭
對于穿過代理(如負載均衡)的請求,其主機名矩肩、端口和協(xié)議可能會改變现恼,從客戶端的角度,這帶來了創(chuàng)建指向正確主機名黍檩、端口和協(xié)議的挑戰(zhàn)叉袍。
RFC 7239定義了Forwarded
HTTP頭,代理可以使用它提供原始請求的信息刽酱。此外也有一些非標準定義的頭喳逛,包括X-Forwarded-Host
、X-Forwareded-Port
棵里、X-Forwarded-Ssl
和X-Forwarded-Prefix
等润文。
ForwardedHeaderTransformer
是一個根據(jù)轉(zhuǎn)發(fā)頭修改請求主機名姐呐、端口和協(xié)議并在之后移除這些頭信息的組件。您可以使用forwardedHeaderTransformer
作為名稱聲明這個組件典蝌,它會被檢測到并使用曙砂。
需要考慮轉(zhuǎn)發(fā)頭的安全問題,因為應用不知道這些頭信息是由代理按預想添加的骏掀,還是由客戶端餓一天假的鸠澈。這就是可信任的邊界代理需要配置移除來自外界的不可信轉(zhuǎn)發(fā)的原因。您也可以配置ForwardedHeaderTransformer
的removeOnly=true
截驮,直接移除而不使用這些頭信息笑陈。
在5.1中,
ForwardedHeaderFilter
已過時并由ForwardedHeaderTransformer
代替侧纯,轉(zhuǎn)發(fā)頭可以在交換器創(chuàng)建之前更早被處理新锈。如果依然配置了該過濾器,會從過濾器列表中自動移除它眶熬,并使用ForwardedHeaderTransformer
代替。
1.2.3 過濾器
在WebHandler
API中块请,可以通過WebFilter
在過濾器鏈其它實例和目標WebHandler
之前和之后添加攔截器風格的邏輯娜氏。在使用WebFlux Config時,WebFilter
的注冊和聲明Spring bean一樣簡單墩新,此外贸弥,(可選的)優(yōu)先級的指定可以通過在Bean聲明上使用@Order
注解或使之實現(xiàn)Ordered
接口。
CORS
Spring WebFlux提供了通過控制器注解對CORS的細粒度配置支持海渊。然而绵疲,在使用Spring Security時,建議轉(zhuǎn)用內(nèi)建的CorsFilter
臣疑,且必須將其順序放在Spring Security的過濾器鏈之前盔憨。
參考CORS小節(jié)及CORSWebFilter
獲取更多信息。
1.2.4 異常
在WebHandler
API中讯沈,可以使用WebExceptionHander
處理來自WebFilter
鏈實例及目標WebHandler
的異常郁岩。在使用WebFlux Config時,WebExceptionHander
的注冊和聲明Spring bean一樣簡單缺狠,此外问慎,(可選的)優(yōu)先級的指定可以通過在Bean聲明上使用@Order
注解或使之實現(xiàn)Ordered
接口。
下表描述了可用的WebExceptionHandler
實現(xiàn):
異常處理器 | 描述 |
---|---|
ResponseStatusExceptionHandler |
提供對ResponseStatusException 類型的異常的處理挤茄,設(shè)置HTTP響應該異常對應的狀態(tài)碼 |
WebFluxResponseStatusExceptionHandler |
擴展ResponseStatusExceptionHandler 如叼,同時支持任何異常類型上的@ResponseStatus 注解。該處理器在WebFlux Config中聲明穷劈。 |
1.2.5 編解碼
spring-web
和spring-core
模塊通過基于響應式流背壓的非阻塞I/O提供了對高級對象到字節(jié)內(nèi)容的序列化和反序列化支持笼恰。下面是對該支持的描述:
- Encoder和Decoder是獨立于HTTP的底層編碼和解碼約定片酝。
- HttpMessageReader和HttpMessageWriter是對HTTP消息內(nèi)容編碼和解碼的約定。
-
Encoder
可通過EncoderHttpMessageWriter
封裝適配web應用使用挖腰,同理Decoder
可通過DecoderHttpMessageReader
封裝適配web應用使用雕沿。 -
DataBuffer
是對不同種字節(jié)緩沖區(qū)表示的抽象(如Netty的ByteBuf
、java.nio.ByteBuffer
等)猴仑,所有編解碼器都在其之上工作审轮。參考《Spring核心》中的數(shù)據(jù)緩沖區(qū)及編解碼器了解更多內(nèi)容。
spring-core
模塊提供了對byte[]
辽俗、ByteBuffer
疾渣、DataBuffer
、Resource
及String
的編解碼器實現(xiàn)崖飘。spring-web
模塊提供了Jackson JSON榴捡、Jackson Smile、JAXB2朱浴、Protocol Buffer等對專為HTTP消息(表單數(shù)據(jù)吊圾、多段內(nèi)容、服務(wù)器事件等)的編解碼器實現(xiàn)翰蠢。
ClientCodedConfigurer
和ServerCodecCofigurer
通常用于在應用中配置或自定義編解碼器项乒。請參考HTTP 消息編解碼配置小節(jié)。
Jackson JSON
使用Jackson庫梁沧,可以提供JSON和二進制JSON(Smile)的支持檀何。
Jackson2Decoder
的方式工作如下:
- 使用Jackson的異步非阻塞解析器聚合一段流為
TokenBuffer
字節(jié)塊,每一塊代表一個JSON對象廷支。 - 每個
TokenBuffer
都會傳給Jackson的ObjectMapper
用于創(chuàng)建高級對象频鉴。 - 在解碼單值發(fā)布者(如
Mono
)時,只有一個TokenBuffer
恋拍。 - 在解碼多值發(fā)布者(如
Flux
)時垛孔,每個TokenBuffer
在接收到足以表示完整對象的字節(jié)后生成并傳給ObjectMapper
。輸入的內(nèi)容可以是JSON數(shù)組芝囤,如果content-type是application/stream+json
的話亦可以是行界定JSON似炎。
Jaclson2Encoder
的工作方式如下:
- 單值發(fā)布者(如
Mono
),直接通過ObjectMapper
序列化悯姊。 - 對于
application/json
形式的多值發(fā)布者羡藐,默認使用Flux#collectToList()
收集值,并在之后序列化結(jié)果集悯许。 - 對于流媒體類型的多值發(fā)布者仆嗦,如
application/stream+json
或application/stream-x-jackson-smile
,通過行界定JSON格式分別編碼先壕、寫入或回刷每個值瘩扼。 - 對SSE谆甜,每個事件都會調(diào)用
Jackson2Encoder
,回刷輸出以保證傳輸無延遲集绰。
默認情況下
Jacoson2Encoder
和Jackson2Decoder
都不支持String
類型规辱。作為替代,默認假設(shè)字符串或一系列字符串代表已序列化的JSON內(nèi)容栽燕,會通過CharSequenceEncoder
渲染罕袋。如果需要從Flux<String>
中渲染出JSON數(shù)組,則需使用Flux#collectToList()
并編碼為Mono<List<String>>
碍岔。
表單數(shù)據(jù)
FormHttpMessageReader
和FormHttpMessageWriter
支持application/x-www-form-urlencoded
類型內(nèi)容的編解碼浴讯。
在服務(wù)器端,表單內(nèi)容可能需要在多個地方訪問蔼啦。ServerWebExchange
專門提供了一個getFormData()
方法榆纽,通過FormHttpMessageReader
解析內(nèi)容并緩存結(jié)果以待后續(xù)重復訪問。參考WebHandler
API的表單數(shù)據(jù)小節(jié)捏肢。
一旦使用了getFormData()
方法奈籽,就不能再從請求體中讀取原始內(nèi)容了。因此猛计,應用需要統(tǒng)一通過ServerWebExchange
獲取數(shù)據(jù)的方式唠摹,要么訪問已緩存的表單數(shù)據(jù),要么讀取原始請求體奉瘤。
多段數(shù)據(jù)
MultipartHttpMessageReader
和MultipartHttpMessageWriter
支持multipart/form-data
類型內(nèi)容的編解碼。MultipartHttpMessageReader
代理其他HttpMessageReader
真正Flux<Part>
解析的執(zhí)行煮甥,并在之后簡單的將各部分收集為MultiValueMap
盗温。當前使用Synchronoss NIO Multipart作為解析庫。
在服務(wù)器端成肘,多段表單內(nèi)容可能需要在多個地方訪問卖局。ServerWebExchange
專門提供了一個getMultipartData()
方法,通過MultipartHttpMessageReader
解析內(nèi)容并緩存結(jié)果以待后續(xù)重復訪問双霍。參考WebHandler
API的多段數(shù)據(jù)小節(jié)砚偶。
一旦使用了getMultipartData()
方法,就不能再從請求體中讀取原始內(nèi)容了洒闸。因此染坯,應用要么反復調(diào)用getMultipartData()
方法并使用Map式訪問內(nèi)容,要么依賴SynchronossPartHttpMessageReader
一次性訪問Flux<Part>
丘逸。
流
當在HTTP響應中使用流(如text/event-stream
和application/stream+json
)時单鹿,為了可靠地監(jiān)測到遲早會斷開連接的客戶端,周期性的數(shù)據(jù)發(fā)送非常重要深纲。這種數(shù)據(jù)發(fā)送可以是僅注釋的空SSE事件仲锄,或其他任何可以有效充當心跳的“無操作”數(shù)據(jù)劲妙。
DataBuffer
DataBuffer
是WebFlux中對字節(jié)緩沖區(qū)的表示。Spring核心參考文檔的數(shù)據(jù)緩沖區(qū)和編解碼器小節(jié)有更多相關(guān)內(nèi)容儒喊。要理解的關(guān)鍵點是在像Netty等服務(wù)器中镣奋,字節(jié)緩沖區(qū)會通過池和引用計數(shù)管理并在消耗后釋放,以避免內(nèi)存泄漏怀愧。
WebFlux應用通常無需關(guān)注這些問題侨颈,除非直接建立或消耗字節(jié)緩沖區(qū),而不是依賴編解碼器對高級對象進行轉(zhuǎn)換掸驱「匕幔或者,也有可能是他們決定創(chuàng)建自定義的編解碼器毕贼。對這些場景温赔,請復習《數(shù)據(jù)緩沖區(qū)和編解碼器》小節(jié),特別是《DataBuffer的使用》鬼癣。
日志
Spring WebFlux中DEBUG級別日志的設(shè)計理念是小巧陶贼、極簡且對用戶友好。它重點關(guān)注一遍又一遍出現(xiàn)的高價值信息待秃,而且他信息則僅在調(diào)試特定問題時才有用拜秧。
TRACE級別的日志遵循和DEBUG級別相同的理論(例如,同樣不能成坨輸出)但其可以用于問題調(diào)試章郁。此外一些日志信息在TRACE級別和DEBUG級別以不同的詳細程度展示枉氮。
好的日志記錄源自使用日志框架的經(jīng)驗。如果您發(fā)現(xiàn)任何不符合既定目標的地方暖庄,請告訴我們聊替。
日志ID
在WebFlux中,一次請求會在多個線程中執(zhí)行培廓,線程ID在分辨屬于特定請求的日志信息上沒什么用惹悄。這就是WebFlux默認在日志消息上加入請求專屬ID的原因。
在服務(wù)器端肩钠,日志ID存放在ServerWebExchange
屬性(LOG_ID_ATTRIBUTE
)中泣港,同時可通過ServletWebExchange#getLogPrefix()
方法獲取基于該ID的完整格式化的前綴。在WebClient
端价匠,日志ID存在ClientRequest
屬性(LOG_ID_ATTRIBUTE
)中同時可通過ClientRequest#logPrefix()
方法獲取基于該ID的完整格式化的前綴当纱。
敏感數(shù)據(jù)
DEBUG
和TRACE
日志可記錄敏感信息。這就是表單參數(shù)及頭信息默認被隱去的原因霞怀,如有需要惫东,必須明確啟用在此之上的完整日志記錄。
下文示例展示了如何開啟服務(wù)端請求的全量日志:
Java:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
Kotlin:
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}
下文示例展示了如何開啟客戶端請求的全量日志:
Java:
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build())
.build();
Kotlin:
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies(ExchangeStrategies.builder().codecs(consumer).build())
.build()
(未完待續(xù))