【持續(xù)翻譯中】Spring響應式Web技術(shù)棧

@TOC

原文地址:https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#spring-webflux

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-webmvcspring-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)或阻塞的(getParametergetPart)相去甚遠洋只。這就是設(shè)計新的通用非阻塞運行時基礎(chǔ)API的動機辆沦。因為像Netty這樣的服務(wù)器在異步昼捍、非阻塞方面做得非常好,所以通用性肢扯、基礎(chǔ)性非常重要妒茬。

另一部分原因是函數(shù)式編程。就像Java 5注解的引入帶來的的增強(基于注解的REST控制器或單元測試)蔚晨,Java 8引入的lambda表達式使Java擁有了使用函數(shù)式API的機會乍钻。它有助于構(gòu)建非阻塞應用和聲明式組合異步邏輯的持續(xù)式風格API(流行于CompletableFutureReactiveX)的使用。從編程模型來看铭腕,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類型并使用蝎毡,返回FluxMono作為輸出。所以可以傳入任意類型的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應用中廣泛使用的功能虚倒。例如:

  • 用戶會話及屬性美侦。
  • 請求屬性。
  • 請求中解析得到的LocalePrincipal裹刮。
  • 數(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中幌陕。默認情況下,ServerCodecConfigurerbean配置使用FormHttpMessageReader(參考Web Handler API)汽煮。

多段數(shù)據(jù)

Spring MVC對應部分

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ā)頭

Spring MVC對應部分

對于穿過代理(如負載均衡)的請求,其主機名矩肩、端口和協(xié)議可能會改變现恼,從客戶端的角度,這帶來了創(chuàng)建指向正確主機名黍檩、端口和協(xié)議的挑戰(zhàn)叉袍。

RFC 7239定義了ForwardedHTTP頭,代理可以使用它提供原始請求的信息刽酱。此外也有一些非標準定義的頭喳逛,包括X-Forwarded-HostX-Forwareded-Port棵里、X-Forwarded-SslX-Forwarded-Prefix等润文。

ForwardedHeaderTransformer是一個根據(jù)轉(zhuǎn)發(fā)頭修改請求主機名姐呐、端口和協(xié)議并在之后移除這些頭信息的組件。您可以使用forwardedHeaderTransformer作為名稱聲明這個組件典蝌,它會被檢測到并使用曙砂。

需要考慮轉(zhuǎn)發(fā)頭的安全問題,因為應用不知道這些頭信息是由代理按預想添加的骏掀,還是由客戶端餓一天假的鸠澈。這就是可信任的邊界代理需要配置移除來自外界的不可信轉(zhuǎn)發(fā)的原因。您也可以配置ForwardedHeaderTransformerremoveOnly=true截驮,直接移除而不使用這些頭信息笑陈。

在5.1中,ForwardedHeaderFilter已過時并由ForwardedHeaderTransformer代替侧纯,轉(zhuǎn)發(fā)頭可以在交換器創(chuàng)建之前更早被處理新锈。如果依然配置了該過濾器,會從過濾器列表中自動移除它眶熬,并使用ForwardedHeaderTransformer代替。

1.2.3 過濾器

Spring MVC對應部分

WebHandler API中块请,可以通過WebFilter在過濾器鏈其它實例和目標WebHandler之前和之后添加攔截器風格的邏輯娜氏。在使用WebFlux Config時,WebFilter的注冊和聲明Spring bean一樣簡單墩新,此外贸弥,(可選的)優(yōu)先級的指定可以通過在Bean聲明上使用@Order注解或使之實現(xiàn)Ordered接口。

CORS

Spring MVC對應部分

Spring WebFlux提供了通過控制器注解對CORS的細粒度配置支持海渊。然而绵疲,在使用Spring Security時,建議轉(zhuǎn)用內(nèi)建的CorsFilter臣疑,且必須將其順序放在Spring Security的過濾器鏈之前盔憨。

參考CORS小節(jié)及CORSWebFilter獲取更多信息。

1.2.4 異常

Spring MVC對應部分

WebHandlerAPI中讯沈,可以使用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 MVC對應部分

spring-webspring-core模塊通過基于響應式流背壓的非阻塞I/O提供了對高級對象到字節(jié)內(nèi)容的序列化和反序列化支持笼恰。下面是對該支持的描述:

  • EncoderDecoder是獨立于HTTP的底層編碼和解碼約定片酝。
  • HttpMessageReaderHttpMessageWriter是對HTTP消息內(nèi)容編碼和解碼的約定。
  • Encoder可通過EncoderHttpMessageWriter封裝適配web應用使用挖腰,同理Decoder可通過DecoderHttpMessageReader封裝適配web應用使用雕沿。
  • DataBuffer是對不同種字節(jié)緩沖區(qū)表示的抽象(如Netty的ByteBufjava.nio.ByteBuffer等)猴仑,所有編解碼器都在其之上工作审轮。參考《Spring核心》中的數(shù)據(jù)緩沖區(qū)及編解碼器了解更多內(nèi)容。

spring-core模塊提供了對byte[]辽俗、ByteBuffer疾渣、DataBufferResourceString的編解碼器實現(xiàn)崖飘。spring-web模塊提供了Jackson JSON榴捡、Jackson Smile、JAXB2朱浴、Protocol Buffer等對專為HTTP消息(表單數(shù)據(jù)吊圾、多段內(nèi)容、服務(wù)器事件等)的編解碼器實現(xiàn)翰蠢。

ClientCodedConfigurerServerCodecCofigurer通常用于在應用中配置或自定義編解碼器项乒。請參考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+jsonapplication/stream-x-jackson-smile,通過行界定JSON格式分別編碼先壕、寫入或回刷每個值瘩扼。
  • 對SSE谆甜,每個事件都會調(diào)用Jackson2Encoder,回刷輸出以保證傳輸無延遲集绰。

默認情況下Jacoson2EncoderJackson2Decoder都不支持String類型规辱。作為替代,默認假設(shè)字符串或一系列字符串代表已序列化的JSON內(nèi)容栽燕,會通過CharSequenceEncoder渲染罕袋。如果需要從Flux<String>中渲染出JSON數(shù)組,則需使用Flux#collectToList()并編碼為Mono<List<String>>碍岔。

表單數(shù)據(jù)

FormHttpMessageReaderFormHttpMessageWriter支持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ù)

MultipartHttpMessageReaderMultipartHttpMessageWriter支持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>丘逸。

Spring MVC對應部分

當在HTTP響應中使用流(如text/event-streamapplication/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 MVC對應部分

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ù)

Spring MVC對應部分

DEBUGTRACE日志可記錄敏感信息。這就是表單參數(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ù))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市廉沮,隨后出現(xiàn)的幾起案子颓遏,更是在濱河造成了極大的恐慌,老刑警劉巖滞时,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叁幢,死亡現(xiàn)場離奇詭異,居然都是意外死亡坪稽,警方通過查閱死者的電腦和手機曼玩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窒百,“玉大人黍判,你說我怎么就攤上這事「萆遥” “怎么了顷帖?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長渤滞。 經(jīng)常有香客問我贬墩,道長,這世上最難降的妖魔是什么妄呕? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任陶舞,我火速辦了婚禮,結(jié)果婚禮上绪励,老公的妹妹穿的比我還像新娘肿孵。我一直安慰自己,他們只是感情好疏魏,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布颁井。 她就那樣靜靜地躺著,像睡著了一般蠢护。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上养涮,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天葵硕,我揣著相機與錄音,去河邊找鬼贯吓。 笑死懈凹,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的悄谐。 我是一名探鬼主播介评,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了们陆?” 一聲冷哼從身側(cè)響起寒瓦,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坪仇,沒想到半個月后杂腰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡椅文,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年喂很,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皆刺。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡少辣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出羡蛾,到底是詐尸還是另有隱情漓帅,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布林说,位于F島的核電站煎殷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏腿箩。R本人自食惡果不足惜豪直,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望珠移。 院中可真熱鬧弓乙,春花似錦、人聲如沸钧惧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浓瞪。三九已至懈玻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乾颁,已是汗流浹背涂乌。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留英岭,地道東北人湾盒。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像诅妹,于是被迫代替她去往敵國和親罚勾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容