【持續翻譯中】Spring官方文檔:響應式Web技術棧

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

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

Version 5.2.0.RELEASE

本文檔是介紹響應式技術棧,它基於響應式流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框架WebFlux是在5.0版本添加的。它是完全非阻塞的,且支持響應式流背壓,可以運行在Netty、Undertow和Servlet 3.1+等容器上。

這兩個框架的模塊名模塊名spring-webmvcspring-webflux是它們各自的寫照,在Spring框架中共存。應用程序即可擇其一用之,在某些情況下亦可同時用之。例如使用基於響應式WebClient的Spring MVC控制器。

1.1 概述

爲什麼要開發Spring Webflux呢?

一部分原因是需要通過非阻塞web技術棧使用更少量的線程及硬件資源處理併發。Servlet 3.1的確提供了非租塞I/O相關API,然而,這些API和原有的Servlet API,如同步的(FilterServlet)或阻塞的(getParametergetPart)相去甚遠。這就是設計新的通用非阻塞運行時基礎API的動機。因爲像Netty這樣的服務器在異步、非阻塞方面做得非常好,所以通用性、基礎性非常重要。

另一部分原因是函數式編程。就像Java 5註解的引入帶來的的增強(基於註解的REST控制器或單元測試),Java 8引入的lambda表達式使Java擁有了使用函數式API的機會。它有助於構建非阻塞應用和聲明式組合異步邏輯的持續式風格API(流行於CompletableFutureReactiveX)的使用。從編程模型來看,Java 8使Spring WebFlux結合註解控制器提供函數式web終端成爲可能。

1.1.1 “響應式”的定義

我們一直在談論“非阻塞”和“函數式”,但響應式到底是什麼意思呢?

術語“響應式”是指圍繞對變化的響應而構建的編程模型,例如,網絡組件響應I/O事件、UI控件響應鼠標事件等。這種場景是非阻塞的,因爲在操作完成或數據可用時才做出反應,在這之前程序不會因等待而阻塞。

Spring團隊在“響應式”上提供的另一重要機制是非阻塞背壓。在同步的、命令式的代碼中,強制調用方等待的阻塞構成了背壓的自然形式。而在非阻塞代碼中,爲了不讓速度快的生產者沖垮目標,控制事件的流速變得重要起來。

響應式流是一個小型標準(亦被Java 9採納),定義了背壓場景下異步組件間的交互。例如,數據倉庫(作爲發佈者)產生數據,HTTP服務器(作爲訂閱者)讀取並寫入響應。響應式流的主要目的就是使訂閱者可以控制發佈者生產數據的快慢。

通常疑問:如果發佈者不能慢下來怎麼辦?

響應式流只建立機制和邊界。如果發佈者不能慢下來,則考慮使用緩衝區、拋棄或讓操作失敗。

1.1.2 響應式API

響應式流在互操作性上扮演了重要角色。它關注的是庫的基礎設施組件,因爲太底層了,所以在應用API上用處不大。應用程序需要豐富的高級函數式API組合異步邏輯,和Java 8 的Stream API相似,但不僅僅是集合操作。這就輪到響應式庫登場了。

Spring WebFlux選擇的是Reactor響應式庫。它的API提供了Mono類型和Flux類型,可以使用一系列基於ReactiveX的操作符分別操作0…1(Mono)和0…N(Flux)的數據序列。Reactor是一個響應式流庫,故所有操作都支持非阻塞背壓。Reactor重點關注服務器端Java。在開發時就注重和Spring的協作。

Reactor是WebFlux的核心依賴,但WebFlux也可以和其他響應式流庫合作。WebFlux API的通用模式是:接收普通的Publisher作爲輸入,在內部將其轉爲Reactor類型並使用,返回FluxMono作爲輸出。所以可以傳入任意類型的Publisher作爲輸入,並在輸出上執行操作。但需要將輸出和所選響應式庫進行適配。只要可行(如使用註解的控制器),WebFlux就會透明地適配RxJava或其他響應式庫。參考響應式庫獲取更多詳細信息。

作爲響應式API的擴展,WebFlux同時支持在Kotlin中使用協程API,它可以讓編程風格更加命令式。後文將提供使用協程API的Kotlin代碼示例。

1.1.3 編程模型

spring-web模塊包含了Spring WebFlux的響應式基礎部分,包括HTTP抽象、響應式流對所支持服務器的適配器編解碼器以及類似Servlet API但非阻塞的核心Web處理器API

在這之上,Spring WebFlux提供了兩種編程模型:

  • 使用註解的控制器 - 順承Spring MVC,同樣基於spring-web模塊的註解。Spring MVC和WebFlux都支持響應式(Reactor、RxJava)返回類型,彼此分辨並不容易。值得注意的一點不同是WebFlux還支持響應式@RequestBody參數。
  • 函數式終端 - 基於lambda、輕量級、函數式編程模型。可以將其視爲一個小型代碼庫或一系列工具方法,應用程序用它處理路由和請求。函數式終端和使用註解的控制器間最大的區別在於,前者負責從頭到尾的請求處理,後者通過註解聲明意圖並回調。

1.1.4 適用性

Spring MVC 還是 WebFlux?

這是一個自然的提問,但也包含了不必要的對立。實際上,兩者可以同時工作,拓寬了可選項的範圍。兩者在設計上和對方是持續統一的,它們可以並肩作戰,雙方都可以享受到對方帶來的好處。下圖展示了兩者的關係,哪些是它們共有的,哪些是各自獨有的:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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工作的很好,那就沒必要更換。命令式程序代碼在編寫、理解和調試上都是最簡單的。而且,因爲歷史原因,應用中選用的絕大多數類庫都是阻塞的。
  • 如果您已經考察過非阻塞web技術棧,Spring WebFlux提供相同的執行模型優勢,而且還提供了多種服務器選項(Netty、Tomcat、Jetty、Undertow、Servlet 3.1+容器)、多種編程模型(使用註解的控制器和函數式web終端)及多種響應式庫選項(Reactor、RxJava等)。
  • 如果對使用Kotlin或Java 8 lambda表達式的輕量級函數式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 服務器

Spring WebFlux支持Tomcat、Jetty、Servlet 3.1+及Netty和Undertow等非Servlet運行時。它們都會適配底層通用API,從而在服務器間支持高級編程模型

Spring WebFlux沒有內建服務器啓動和關閉的支持。但是通過Spring配置和WebFlux基礎設施可以通過數行代碼很簡單地在應用中嵌入啓動服務器。

Spring Boot有一個WebFlux起步器,會自動執行這些步驟。默認情況下,起步器使用Netty,但可以通過修改Maven或Gradle依賴輕鬆切換至Tomcat、Jetty或Undertow。Spring Boot默認選擇Netty,是因爲它廣泛用於異步、非阻塞領域,而且可以使客戶端和服務器共享資源。

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 規模

性能具有多種特徵和意義。響應式和非阻塞通常不能讓應用跑的更快,但在在某些場景下(如使用WebClient併發執行遠程調用)可以。總的來說,以非阻塞方式執行操作需要的工作更多,且會輕微增加處理時間。

響應式和非阻塞最可預見的好處是其可以使用較少固定數量的線程和少量內存規模的能力。因爲其硬件規模更加可預測,所以應用程序在欠載時更有彈性。不過,要見識到這些好處需要一定的潛伏期(包括一系列緩慢且不可預測的網絡I/O)。這纔是響應式技術棧秀肌肉的場合,其改變會極具戲劇性。

1.1.7 併發模型

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提供了名爲“調度器(Scheduler)”的線程池抽象,配合publishOn操作符使用,將處理切換到另一個線程池上。調度器的名字聲明瞭其併發策略,例如,“parallel(CPU在限定個數的線程上密集工作)”或“elastic(使用大量線程的I/O密集工作)”。如果看到這些線程,那就意味着某些代碼正在使用Scheduler策略的專用線程池。
  • 數據訪問庫和其他第三方依賴同樣可以創建和使用它們自己的線程。

配置

Spring框架並不提供服務器的啓動和停止支持。要配置服務器的線程模型,要麼使用服務器專屬的配置API,要麼使用Spring Boot,設置Spring Boot關於不同服務器的不同配置選項。WebClient可以直接配置。其他的庫,則需要分別參考對應文檔。

1.2 響應式核心

spring-web模塊包含對響應式web應用的基礎支持如下:

  • 服務器請求處理有兩個級別的支持。
    • HttpHandler - 基於非阻塞I/O的HTTP請求處理及響應式流背壓的基礎約定,以及Reactor Netty、Undertow、Tomcat、Jetty和所有Servlet 3.1+容器的適配器。
    • WebHandler API - 稍高級,請求處理的多用途web API,在此之上構建了使用註解的控制器和函數式終端等編程模型。
  • 對客戶端來說,基礎的ClientHttpConnector約定了通過Reactor Netty適配器和響應式Jetty HttpClient適配器發出基於非阻塞I/O的HTTP請求及響應式流背壓。應用中使用的高級WebClient基於這些基礎約定構建。
  • 不論在客戶端還是服務端,都用編解碼器來序列化和反序列化HTTP請求和響應的內容。

1.2.1 HttpHandler

HttpHandler是通過單個方法處理請求和響應的基礎約定。它被有意地極簡化,其主要也是唯一的目的是成爲不同HTTP服務器API的最小抽象。

下文表格描述了它所支持的服務器API:

服務器名 所用服務器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-響應式流橋接

下文表格描述了服務的依賴(同時參見支持的版本):

服務器名 組織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和每種服務器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約定構建的org.springframework.web.server包提供了通過一條具有單個WebHandler、多個WebFilter及多個WebExceptionHandler組件組成的鏈處理請求的多用途web API。該鏈條可通過簡單指定Spring ApplicationContext自動檢測位置或在構建器中註冊組件的方式和WebHttpHandlerBuilder放在一起。

儘管HttpHandler對使用不同HTTP服務器進行抽象的目的簡單,WebHandler API的目標卻是提供大量web應用中廣泛使用的功能。例如:

  • 用戶會話及屬性。
  • 請求屬性。
  • 請求中解析得到的LocalePrincipal
  • 數據解析和緩存的訪問。
  • 多段數據的抽象。
  • 其他…

特別的Bean類型

下表列舉的是在Spring應用上下文中WebHandlerBuilder可自動檢測的或可直接註冊在其上的組件:

Bean名稱 Bean類型 數量 描述
任意 WebExceptionHandler 0…N 提供對來自WebFilter鏈實例及目標WebHandler的異常的處理。更多詳情參考異常
任意 WebFilter 0…N 在過濾器鏈其它實例和目標WebHandler之前和之後提供攔截式風格邏輯。更多詳情參考過濾器
webHandler WebHandler 1 請求處理器。
webSessionManager WebSessionManager 0…1 通過ServerWebExchange中一個方法暴露WebSession實例的管理器。默認使用DefaultWebSessionManager
serverCodecConfigurer ServerCodecConfigurer 0…1 訪問HttpMessageReader實例解析數據和多段數據並在之後通過ServerWebExchange中的方法暴露。默認使用ServerCodecConfigurer.create()
localContextResolver LocalContextResolver 0…1 通過ServerWebExchange暴露的LocaleContext的解析器。默認使用AcceptHeaderLocaleContextResolver
forwardedHeaderTransformer ForwardedHeaderTransformer 0…1 處理轉發類頭,對其擴展或刪除。默認不使用。

表單數據

ServerWebExchange暴露了如下訪問表單數據的方法:

Java:

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

Kotlin:

suspend fun getFormData(): MultiValueMap<String, String>

DefaultServerWebExchange使用配置的HttpMessageReader解析表單數據(application/x-www-form-urlencoded)至一個MultiValueMap中。默認情況下,ServerCodecConfigurerbean配置使用FormHttpMessageReader(參考Web Handler API)。

多段數據

Spring MVC對應部分

ServerWebExchange暴露了如下訪問多段數據的方法:

Java:

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

Kotlin:

suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange使用配置的HttpMessageReader<MultiValueMap<String, Part>>解析multipart/form-data內容至一個MultiValueMap中。目前,唯一一個被支持的第三方庫是Synchronoss NIO Multipart,我們也只知道這一個多段數據請求非阻塞解析庫。它通過ServerCodecConfigurer配置(參考Web Handler API)。

要以流的形式解析多段數據,可以使用HttpMessageReader<Part>返回的Flux<Part>代替。例如,在使用註解的控制器中,使用@RequestPart意味着使用Map式根據名稱訪問各個部分,意味着需要將多段數據完全解析出來。與之相反,可以使用@RequestBody將內容解碼爲Flux<Part>而不將其聚合爲MultiValueMap

轉發頭

Spring MVC對應部分

對於穿過代理(如負載均衡)的請求,其主機名、端口和協議可能會改變,從客戶端的角度,這帶來了創建指向正確主機名、端口和協議的挑戰。

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

ForwardedHeaderTransformer是一個根據轉發頭修改請求主機名、端口和協議並在之後移除這些頭信息的組件。您可以使用forwardedHeaderTransformer作爲名稱聲明這個組件,它會被檢測到並使用。

需要考慮轉發頭的安全問題,因爲應用不知道這些頭信息是由代理按預想添加的,還是由客戶端餓一天假的。這就是可信任的邊界代理需要配置移除來自外界的不可信轉發的原因。您也可以配置ForwardedHeaderTransformerremoveOnly=true,直接移除而不使用這些頭信息。

在5.1中,ForwardedHeaderFilter已過時並由ForwardedHeaderTransformer代替,轉發頭可以在交換器創建之前更早被處理。如果依然配置了該過濾器,會從過濾器列表中自動移除它,並使用ForwardedHeaderTransformer代替。

1.2.3 過濾器

Spring MVC對應部分

WebHandler API中,可以通過WebFilter在過濾器鏈其它實例和目標WebHandler之前和之後添加攔截器風格的邏輯。在使用WebFlux Config時,WebFilter的註冊和聲明Spring bean一樣簡單,此外,(可選的)優先級的指定可以通過在Bean聲明上使用@Order註解或使之實現Ordered接口。

CORS

Spring MVC對應部分

Spring WebFlux提供了通過控制器註解對CORS的細粒度配置支持。然而,在使用Spring Security時,建議轉用內建的CorsFilter,且必須將其順序放在Spring Security的過濾器鏈之前。

參考CORS小節及CORSWebFilter獲取更多信息。

1.2.4 異常

Spring MVC對應部分

WebHandlerAPI中,可以使用WebExceptionHander處理來自WebFilter鏈實例及目標WebHandler的異常。在使用WebFlux Config時,WebExceptionHander的註冊和聲明Spring bean一樣簡單,此外,(可選的)優先級的指定可以通過在Bean聲明上使用@Order註解或使之實現Ordered接口。

下表描述了可用的WebExceptionHandler實現:

異常處理器 描述
ResponseStatusExceptionHandler 提供對ResponseStatusException類型的異常的處理,設置HTTP響應該異常對應的狀態碼
WebFluxResponseStatusExceptionHandler 擴展ResponseStatusExceptionHandler,同時支持任何異常類型上的@ResponseStatus註解。
該處理器在WebFlux Config中聲明。

1.2.5 編解碼

Spring MVC對應部分

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

  • EncoderDecoder是獨立於HTTP的底層編碼和解碼約定。
  • HttpMessageReaderHttpMessageWriter是對HTTP消息內容編碼和解碼的約定。
  • Encoder可通過EncoderHttpMessageWriter封裝適配web應用使用,同理Decoder可通過DecoderHttpMessageReader封裝適配web應用使用。
  • DataBuffer是對不同種字節緩衝區表示的抽象(如Netty的ByteBufjava.nio.ByteBuffer等),所有編解碼器都在其之上工作。參考《Spring核心》中的數據緩衝區及編解碼器瞭解更多內容。

spring-core模塊提供了對byte[]ByteBufferDataBufferResourceString的編解碼器實現。spring-web模塊提供了Jackson JSON、Jackson Smile、JAXB2、Protocol Buffer等對專爲HTTP消息(表單數據、多段內容、服務器事件等)的編解碼器實現。

ClientCodedConfigurerServerCodecCofigurer通常用於在應用中配置或自定義編解碼器。請參考HTTP 消息編解碼配置小節。

Jackson JSON

使用Jackson庫,可以提供JSON和二進制JSON(Smile)的支持。

Jackson2Decoder的方式工作如下:

  • 使用Jackson的異步非阻塞解析器聚合一段流爲TokenBuffer字節塊,每一塊代表一個JSON對象。
  • 每個TokenBuffer都會傳給Jackson的ObjectMapper用於創建高級對象。
  • 在解碼單值發佈者(如Mono)時,只有一個TokenBuffer
  • 在解碼多值發佈者(如Flux)時,每個TokenBuffer在接收到足以表示完整對象的字節後生成並傳給ObjectMapper。輸入的內容可以是JSON數組,如果content-type是application/stream+json的話亦可以是行界定JSON

Jaclson2Encoder的工作方式如下:

  • 單值發佈者(如Mono),直接通過ObjectMapper序列化。
  • 對於application/json形式的多值發佈者,默認使用Flux#collectToList()收集值,並在之後序列化結果集。
  • 對於流媒體類型的多值發佈者,如application/stream+jsonapplication/stream-x-jackson-smile,通過行界定JSON格式分別編碼、寫入或回刷每個值。
  • 對SSE,每個事件都會調用Jackson2Encoder,回刷輸出以保證傳輸無延遲。

默認情況下Jacoson2EncoderJackson2Decoder都不支持String類型。作爲替代,默認假設字符串或一系列字符串代表已序列化的JSON內容,會通過CharSequenceEncoder渲染。如果需要從Flux<String>中渲染出JSON數組,則需使用Flux#collectToList()並編碼爲Mono<List<String>>

表單數據

FormHttpMessageReaderFormHttpMessageWriter支持application/x-www-form-urlencoded類型內容的編解碼。

在服務器端,表單內容可能需要在多個地方訪問。ServerWebExchange專門提供了一個getFormData()方法,通過FormHttpMessageReader解析內容並緩存結果以待後續重複訪問。參考WebHandler API表單數據小節。

一旦使用了getFormData()方法,就不能再從請求體中讀取原始內容了。因此,應用需要統一通過ServerWebExchange獲取數據的方式,要麼訪問已緩存的表單數據,要麼讀取原始請求體。

多段數據

MultipartHttpMessageReaderMultipartHttpMessageWriter支持multipart/form-data類型內容的編解碼。MultipartHttpMessageReader代理其他HttpMessageReader真正Flux<Part>解析的執行,並在之後簡單的將各部分收集爲MultiValueMap。當前使用Synchronoss NIO Multipart作爲解析庫。

在服務器端,多段表單內容可能需要在多個地方訪問。ServerWebExchange專門提供了一個getMultipartData()方法,通過MultipartHttpMessageReader解析內容並緩存結果以待後續重複訪問。參考WebHandler API多段數據小節。

一旦使用了getMultipartData()方法,就不能再從請求體中讀取原始內容了。因此,應用要麼反覆調用getMultipartData()方法並使用Map式訪問內容,要麼依賴SynchronossPartHttpMessageReader一次性訪問Flux<Part>

Spring MVC對應部分

當在HTTP響應中使用流(如text/event-streamapplication/stream+json)時,爲了可靠地監測到遲早會斷開連接的客戶端,週期性的數據發送非常重要。這種數據發送可以是僅註釋的空SSE事件,或其他任何可以有效充當心跳的“無操作”數據。

DataBuffer

DataBuffer是WebFlux中對字節緩衝區的表示。Spring核心參考文檔的數據緩衝區和編解碼器小節有更多相關內容。要理解的關鍵點是在像Netty等服務器中,字節緩衝區會通過池和引用計數管理並在消耗後釋放,以避免內存泄漏。

WebFlux應用通常無需關注這些問題,除非直接建立或消耗字節緩衝區,而不是依賴編解碼器對高級對象進行轉換。或者,也有可能是他們決定創建自定義的編解碼器。對這些場景,請複習《數據緩衝區和編解碼器》小節,特別是《DataBuffer的使用》

日誌

Spring MVC對應部分

Spring WebFlux中DEBUG級別日誌的設計理念是小巧、極簡且對用戶友好。它重點關注一遍又一遍出現的高價值信息,而且他信息則僅在調試特定問題時纔有用。

TRACE級別的日誌遵循和DEBUG級別相同的理論(例如,同樣不能成坨輸出)但其可以用於問題調試。此外一些日誌信息在TRACE級別和DEBUG級別以不同的詳細程度展示。

好的日誌記錄源自使用日誌框架的經驗。如果您發現任何不符合既定目標的地方,請告訴我們。

日誌ID

在WebFlux中,一次請求會在多個線程中執行,線程ID在分辨屬於特定請求的日誌信息上沒什麼用。這就是WebFlux默認在日誌消息上加入請求專屬ID的原因。

在服務器端,日誌ID存放在ServerWebExchange屬性(LOG_ID_ATTRIBUTE)中,同時可通過ServletWebExchange#getLogPrefix()方法獲取基於該ID的完整格式化的前綴。在WebClient端,日誌ID存在ClientRequest屬性(LOG_ID_ATTRIBUTE)中同時可通過ClientRequest#logPrefix()方法獲取基於該ID的完整格式化的前綴。

敏感數據

Spring MVC對應部分

DEBUGTRACE日誌可記錄敏感信息。這就是表單參數及頭信息默認被隱去的原因,如有需要,必須明確啓用在此之上的完整日誌記錄。

下文示例展示瞭如何開啓服務端請求的全量日誌:

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

(未完待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章