Flux 和 Mono 、reactor實戰 (史上最全)

文章很長,建議收藏起來慢慢讀!瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版 爲您奉上珍貴的學習資源 :


推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文

入大廠 、做架構、大力提升Java 內功 必備的精彩博文 2021 秋招漲薪1W + 必備的精彩博文
1:Redis 分佈式鎖 (圖解-秒懂-史上最全) 2:Zookeeper 分佈式鎖 (圖解-秒懂-史上最全)
3: Redis與MySQL雙寫一致性如何保證? (面試必備) 4: 面試必備:秒殺超賣 解決方案 (史上最全)
5:面試必備之:Reactor模式 6: 10分鐘看懂, Java NIO 底層原理
7:TCP/IP(圖解+秒懂+史上最全) 8:Feign原理 (圖解)
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) 10:CDN圖解(秒懂 + 史上最全 + 高薪必備)
11: 分佈式事務( 圖解 + 史上最全 + 吐血推薦 ) 12:限流:計數器、漏桶、令牌桶
三大算法的原理與實戰(圖解+史上最全)
13:架構必看:12306搶票系統億級流量架構
(圖解+秒懂+史上最全)
14:seata AT模式實戰(圖解+秒懂+史上最全)
15:seata 源碼解讀(圖解+秒懂+史上最全) 16:seata TCC模式實戰(圖解+秒懂+史上最全)

Java 面試題 30個專題 , 史上最全 , 面試必刷 阿里、京東、美團... 隨意挑、橫着走!!!
1: JVM面試題(史上最強、持續更新、吐血推薦) 2:Java基礎面試題(史上最全、持續更新、吐血推薦
3:架構設計面試題 (史上最全、持續更新、吐血推薦) 4:設計模式面試題 (史上最全、持續更新、吐血推薦)
17、分佈式事務面試題 (史上最全、持續更新、吐血推薦) 一致性協議 (史上最全)
29、多線程面試題(史上最全) 30、HR面經,過五關斬六將後,小心陰溝翻船!
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 更多專題, 請參見【 瘋狂創客圈 高併發 總目錄

SpringCloud 微服務 精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
SpringCloud gateway (史上最全) 分庫分表sharding-jdbc底層原理與實操(史上最全,5W字長文,吐血推薦)

前言

響應式編程用的是越來越多,尤其是在移動端 安卓的應用上邊。

在Java後臺服務開發中, 響應式編程用的不是太廣泛,主要原因是, 響應式編程需要一個完整的生態, 包括數據庫、緩存、中間件,都需要配套的響應式組件。 但是這點,其實很多並沒有。

但是,隨着 SpringCloud Gateway 的火爆, 響應式編程又變成了 不可迴避, 不得不去學習的技術。

如果要做 SpringCloud Gateway 的開發, 就必須掌握一些響應式編程的知識。 由於最近在做 spring cloud gateway相關的開發工作, 所以:

把響應式編程Flux 和 Mono 的知識梳理一下,形成了此文

並且,此文會不斷完善。

姊妹篇:關於 SpringCloud Gateway 簡介

SpringCloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在爲微服務架構提供一種簡單有效的統一的 API 路由管理方式。

SpringCloud Gateway 作爲 Spring Cloud 生態系統中的網關,目標是替代 Zuul,在Spring Cloud 2.0以上版本中,沒有對新版本的Zuul 2.0以上最新高性能版本進行集成,仍然還是使用的Zuul 2.0之前的非Reactor模式的老版本。而爲了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway 的目標,不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全,監控/指標,和限流。

有關 Spring Cloud Gateway 響應式編程的實戰, 具體請參考本文姊妹篇:

《SpringCloud gateway (史上最全))》

特別說明

Spring Cloud Gateway 底層使用了高性能的通信框架Netty。

Netty 是高性能中間件的通訊底座, rocketmq 、seata、nacos 、sentinel 、redission 、dubbo 等太多、太多的的大名鼎鼎的中間件,無一例外都是基於netty。

可以毫不誇張的說: netty 是進入大廠、走向高端 的必備技能

要想深入瞭解springcloud gateway ,最好是掌握netty 編程

有關 netty學習 具體請參見機工社出版 、尼恩的暢銷書: Java高併發核心編程卷 1

響應式編程概述

背景知識

爲了應對高併發服務器端開發場景,在2009 年,微軟提出了一個更優雅地實現異步編程的方式——Reactive Programming,我們稱之爲響應式編程。隨後,Netflix 和LightBend 公司提供了RxJava 和Akka Stream 等技術,使得Java 平臺也有了能夠實現響應式編程的框架。

在2017 年9 月28 日,Spring 5 正式發佈。Spring 5 發佈最大的意義在於,它將響應式編程技術的普及向前推進了一大步。而同時,作爲在背後支持Spring 5 響應式編程的框架Spring Reactor,也進入了里程碑式的3.1.0 版本。

什麼是響應式編程

響應式編程是一種面向數據流和變化傳播的編程範式。這意味着可以在編程語言中很方便地表達靜態或動態的數據流,而相關的計算模型會自動將變化的值通過數據流進行傳播。

響應式編程基於reactor(Reactor 是一個運行在 Java8 之上的響應式框架)的思想,當你做一個帶有一定延遲的才能夠返回的io操作時,不會阻塞,而是立刻返回一個流,並且訂閱這個流,當這個流上產生了返回數據,可以立刻得到通知並調用回調函數處理數據。

電子表格程序就是響應式編程的一個例子。單元格可以包含字面值或類似"=B1+C1"的公式,而包含公式的單元格的值會依據其他單元格的值的變化而變化。

響應式傳播核心特點之一:變化傳播:一個單元格變化之後,會像多米諾骨牌一樣,導致直接和間接引用它的其他單元格均發生相應變化。

基於Java8實現觀察者模式

Observable類:此類表示可觀察對象,或模型視圖範例中的“數據”。

它可以被子類實現以表示應用程序想要觀察的對象。

//想要觀察的對象 ObserverDemo
public class ObserverDemo extends Observable {
    public static void main(String[] args) {
        ObserverDemo observerDemo = new ObserverDemo();
        //添加觀察者
        observerDemo.addObserver((o,arg)->{
            System.out.println("數據發生變化A");
        });
        observerDemo.addObserver((o,arg)->{
            System.out.println("數據發生變化B");
        });
        observerDemo.setChanged();//將此Observable對象標記爲已更改
        observerDemo.notifyObservers();//如果該對象發生了變化,則通知其所有觀察者
    }
}

啓動程序測試:

在這裏插入圖片描述

創建一個Observable

rxjava中,可以使用Observable.create() 該方法接收一個Obsubscribe對象

Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() {
            @Override
            public void call(Subscriber<? super Integer> subscriber) {

            }
        });

來個例子:

        Observable<Integer> observable=Observable.create(new Observable.OnSubscribe<Integer>() {
            @Override
            public void call(Subscriber<? super Integer> subscriber) {
                for(int i=0;i<5;i++){
                    subscriber.onNext(i);
                }
                subscriber.onCompleted();
            }
        });
        //Observable.subscribe(Observer),Observer訂閱了Observable
        Subscription subscribe = observable.subscribe(new Observer<Integer>() {
            @Override
            public void onCompleted() {
                Log.e(TAG, "完成");
            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "異常");
            }

            @Override
            public void onNext(Integer integer) {
                Log.e(TAG, "接收Obsverable中發射的值:" + integer);
            }
        });

輸出:

 接收Obsverable中發射的值:0
接收Obsverable中發射的值:1
接收Obsverable中發射的值:2
接收Obsverable中發射的值:3
接收Obsverable中發射的值:4

從上面的例子可以看出,在Observer訂閱了Observable後,

Observer作爲OnSubscribe中call方法的參數傳入,從而調用了Observer的相關方法

基於 Reactor 實現

Reactor 是一個運行在 Java8 之上滿足 Reactice 規範的響應式框架,它提供了一組響應式風格的 API。

Reactor 有兩個核心類: Flux<T>Mono<T>,這兩個類都實現 Publisher 接口。

  • Flux 類似 RxJava 的 Observable,它可以觸發零到多個事件,並根據實際情況結束處理或觸發錯誤。
  • Mono 最多隻觸發一個事件,所以可以把 Mono 用於在異步任務完成時發出通知。

preview

Flux 和 Mono 都是數據流的發佈者,使用 Flux 和 Mono 都可以發出三種數據信號:元素值,錯誤信號,完成信號;錯誤信號和完成信號都代表終止信號,終止信號用於告訴訂閱者數據流結束了,錯誤信號終止數據流同時把錯誤信息傳遞給訂閱者。

三種信號的特點:

  • 錯誤信號和完成信號都是終止信號,不能共存
  • 如果沒有發送任何元素值,而是直接發送錯誤或者完成信號,表示是空數據流
  • 如果沒有錯誤信號,也沒有完成信號,表示是無限數據流

引入依賴

        <dependency>
            <groupId>org.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>

just 和 subscribe方法

just():創建Flux序列,並聲明指定數據流

subscribe():訂閱Flux序列,只有進行訂閱後纔回觸發數據流,不訂閱就什麼都不會發生

public class TestReactor {
    public static void main(String[] args) {
        //just():創建Flux序列,並聲明數據流,
        Flux<Integer> integerFlux = Flux.just(1, 2, 3, 4);//整形
        //subscribe():訂閱Flux序列,只有進行訂閱後纔回觸發數據流,不訂閱就什麼都不會發生
        integerFlux.subscribe(System.out::println);
        
        Flux<String> stringFlux = Flux.just("hello", "world");//字符串
        stringFlux.subscribe(System.out::println);
        
        //fromArray(),fromIterable()和fromStream():可以從一個數組、Iterable 對象或Stream 對象中創建Flux序列
        Integer[] array = {1,2,3,4};
        Flux.fromArray(array).subscribe(System.out::println);
        
        List<Integer> integers = Arrays.asList(array);
        Flux.fromIterable(integers).subscribe(System.out::println);
        
        Stream<Integer> stream = integers.stream();
        Flux.fromStream(stream).subscribe(System.out::println);
    }
}

啓動測試:

在這裏插入圖片描述

響應流的特點

要搞清楚這兩個概念,必須說一下響應流規範。它是響應式編程的基石。他具有以下特點:

  • 響應流必須是無阻塞的。

  • 響應流必須是一個數據流。

  • 它必須可以異步執行。

  • 並且它也應該能夠處理背壓。

  • 即時響應性: 只要有可能, 系統就會及時地做出響應。 即時響應是可用性和實用性的基石, 而更加重要的是,即時響應意味着可以快速地檢測到問題並且有效地對其進行處理。 即時響應的系統專注於提供快速而一致的響應時間, 確立可靠的反饋上限, 以提供一致的服務質量。 這種一致的行爲轉而將簡化錯誤處理、 建立最終用戶的信任並促使用戶與系統作進一步的互動。

  • 回彈性:系統在出現失敗時依然保持即時響應性。 這不僅適用於高可用的、 任務關鍵型系統——任何不具備回彈性的系統都將會在發生失敗之後丟失即時響應性。 回彈性是通過複製、 遏制、 隔離以及委託來實現的。 失敗的擴散被遏制在了每個組件內部, 與其他組件相互隔離, 從而確保系統某部分的失敗不會危及整個系統,並能獨立恢復。 每個組件的恢復都被委託給了另一個(外部的)組件, 此外,在必要時可以通過複製來保證高可用性。 (因此)組件的客戶端不再承擔組件失敗的處理。

  • 彈性: 系統在不斷變化的工作負載之下依然保持即時響應性。 反應式系統可以對輸入(負載)的速率變化做出反應,比如通過增加或者減少被分配用於服務這些輸入(負載)的資源。 這意味着設計上並沒有爭用點和中央瓶頸, 得以進行組件的分片或者複製, 並在它們之間分佈輸入(負載)。 通過提供相關的實時性能指標, 反應式系統能支持預測式以及反應式的伸縮算法。 這些系統可以在常規的硬件以及軟件平臺上實現成本高效的彈性

  • 消息驅動:反應式系統依賴異步的消息傳遞,從而確保了松耦合、隔離、位置透明的組件之間有着明確邊界。 這一邊界還提供了將失敗作爲消息委託出去的手段。 使用顯式的消息傳遞,可以通過在系統中塑造並監視消息流隊列, 並在必要時應用回壓, 從而實現負載管理、 彈性以及流量控制。 使用位置透明的消息傳遞作爲通信的手段, 使得跨集羣或者在單個主機中使用相同的結構成分和語義來管理失敗成爲了可能。 非阻塞的通信使得接收者可以只在活動時才消耗資源, 從而減少系統開銷。

Publisher/Flux和Mono

由於響應流的特點,我們不能再返回一個簡單的POJO對象來表示結果了。必須返回一個類似Java中的Future的概念,在有結果可用時通知消費者進行消費響應。

Reactive Stream規範中這種被定義爲Publisher

Publisher是一個可以提供0-N個序列元素的提供者,並根據其訂閱者Subscriber<? super T>的需求推送元素。

一個Publisher可以支持多個訂閱者,並可以根據訂閱者的邏輯進行推送序列元素。

下面這個Excel計算就能說明一些Publisher的特點。

img

A1-A9就可以看做Publisher及其提供的元素序列。

A10-A13分別是求和函數SUM(A1:A9)、平均函數AVERAGE(A1:A9)、最大值函數MAX(A1:A9)、最小值函數MIN(A1:A9)

A10-A13可以看作訂閱者Subscriber

假如說我們沒有A10-A13,那麼A1-A9就沒有實際意義,它們並不產生計算。

這也是響應式的一個重要特點:當沒有訂閱時發佈者什麼也不做。而Flux和Mono都是PublisherReactor 3實現。

Publisher提供了subscribe方法,允許消費者在有結果可用時進行消費。

如果沒有消費者Publisher不會做任何事情,他根據消費情況進行響應。

Publisher可能返回零或者多個,甚至可能是無限的,爲了更加清晰表示期待的結果就引入了兩個實現模型Mono和Flux。

Flux

Flux 是一個發出(emit)0-N個元素組成的異步序列的Publisher,可以被onComplete信號或者onError信號所終止。

在響應流規範中存在三種給下游消費者調用的方法 onNext, onComplete, 和onError。下面這張圖表示了Flux的抽象模型:

img

以上的的講解對於初次接觸反應式編程的依然是難以理解的,所以這裏有一個循序漸進的理解過程。

有些類比並不是很妥當,但是對於你循序漸進的理解這些新概念還是有幫助的。

傳統數據處理

我們在平常是這麼寫的:

public List<ClientUser> allUsers() {
    return Arrays.asList(new ClientUser("felord.cn", "reactive"),
            new ClientUser("Felordcn", "Reactor"));
}

我們通過迭代返回值Listget這些元素進行再處理(消費),不管有沒有消費者, 菜品都會生產出來。

流式數據處理

Java 8中我們可以改寫爲流的表示:

public Stream<ClientUser> allUsers() {
    return  Stream.of(new ClientUser("felord.cn", "reactive"),
            new ClientUser("Felordcn", "Reactor"));
}

反應式數據處理

Reactor中我們又可以改寫爲Flux表示:

public Flux<ClientUser> allUsers(){
    return Flux.just(new ClientUser("felord.cn", "reactive"),
            new ClientUser("Felordcn", "Reactor"));
}

這時候食客來了,發生了訂閱,廚師纔開始做。

Flux 的創建Demo

Flux ints = Flux.range(1, 4);
Flux seq1 = Flux.just("bole1", "bole2", "bole3");
List iterable = Arrays.asList("bole_01", "bole_02", "bole_03");
Flux seq2 = Flux.fromIterable(iterable);
seq2.subscribe(i -> System.out.println(i));

Mono

Mono 是一個發出(emit)0-1個元素的Publisher,可以被onComplete信號或者onError信號所終止。

img

mono 整體和Flux差不多,只不過這裏只會發出0-1個元素。也就是說不是有就是沒有。

象Flux一樣,我們來看看Mono的演化過程以幫助理解。

傳統數據處理

public ClientUser currentUser () {
    return isAuthenticated ? new ClientUser("felord.cn", "reactive") : null;
}

直接返回符合條件的對象或者null`。

Optional的處理方式

public Optional<ClientUser> currentUser () {
    return isAuthenticated ? Optional.of(new ClientUser("felord.cn", "reactive"))
            : Optional.empty();
}

這個Optional我覺得就有反應式的那種味兒了,當然它並不是反應式。當我們不從返回值Optional取其中具體的對象時,我們不清楚裏面到底有沒有,但是Optional是一定客觀存在的,不會出現NPE問題。

反應式數據處理

public Mono<ClientUser> currentUser () {
    return isAuthenticated ? Mono.just(new ClientUser("felord.cn", "reactive"))
            : Mono.empty();
}

和Optional有點類似的機制,當然Mono不是爲了解決NPE問題的,它是爲了處理響應流中單個值(也可能是Void)而存在的。

Mono的創建Demo

Mono data = Mono.just("bole");

Mono noData = Mono.empty();

m.subscribe(i -> System.out.println(i));

Flux和Mono總結

Flux和Mono是Java反應式中的重要概念,但是很多同學包括我在開始都難以理解它們。這其實是規定了兩種流式範式,這種範式讓數據具有一些新的特性,比如基於發佈訂閱的事件驅動,異步流、背壓等等。另外數據是推送(Push)給消費者的以區別於平時我們的拉(Pull)模式。同時我們可以像Stream Api一樣使用類似mapflatmap等操作符(operator)來操作它們。

函數編程

反應式編程,常常和函數式編程結合,這就是讓大家困擾的地方

函數編程接口

接口函數名 說明
BiConsumer 表示接收兩個輸入參數和不返回結果的操作。
BiFunction 表示接受兩個參數,併產生一個結果的函數。
BinaryOperator 表示在相同類型的兩個操作數的操作,生產相同類型的操作數的結果。
BiPredicate 代表兩個參數謂詞(布爾值函數)。
BooleanSupplier 代表布爾值結果的提供者。
Consumer 表示接受一個輸入參數和不返回結果的操作。
DoubleBinaryOperator 代表在兩個double值操作數的運算,併產生一個double值結果。
DoubleConsumer 表示接受一個double值參數,不返回結果的操作。
DoubleFunction 表示接受double值參數,併產生一個結果的函數。
DoublePredicate 代表一個double值參數謂詞(布爾值函數)。
DoubleSupplier 表示表示接受double值參數,併產生一個結果的函數。值結果的提供者。
DoubleToIntFunction 表示接受一個double值參數,不返回結果的操作。
DoubleFunction 表示接受double值參數,併產生一個結果的函數。
DoublePredicate 代表一個double值參數謂詞(布爾值函數)。
DoubleSupplier DoubleToIntFunction
DoubleToIntFunction 表示接受double值參數,併產生一個int值結果的函數。
DoubleToLongFunction 表示上產生一個double值結果的單個double值操作數的操作。
Function 代表接受一個double值參數,併產生一個long值結果的函數。
DoubleUnaryOperator 表示上產生一個double值結果的單個double值操作數的操作。
Function 表示接受一個參數,併產生一個結果的函數。
IntConsumer 表示接受單個int值的參數並沒有返回結果的操作。
IntFunction 表示接受一個int值參數,併產生一個結果的函數。
IntPredicate 表示一個整數值參數謂詞(布爾值函數)。
IntSupplier 代表整型值的結果的提供者。
IntToLongFunction 表示接受一個int值參數,併產生一個long值結果的函數。
IntUnaryOperator 表示產生一個int值結果的單個int值操作數的運算。
LongBinaryOperator 表示在兩個long值操作數的操作,併產生一個ObjLongConsumer值結果。
LongFunction 表示接受long值參數,併產生一個結果的函數。
LongPredicate 代表一個long值參數謂詞(布爾值函數)。
LongSupplier 表示long值結果的提供者。
LongToDoubleFunction 表示接受double參數,併產生一個double值結果的函數。
LongToIntFunction 表示接受long值參數,併產生一個int值結果的函數。
LongUnaryOperator 表示上產生一個long值結果單一的long值操作數的操作。
ObjDoubleConsumer 表示接受對象值和double值參數,並且沒有返回結果的操作。
ObjIntConsumer 表示接受對象值和整型值參數,並返回沒有結果的操作。
ObjLongConsumer 表示接受對象值和整型值參數,並返回沒有結果的操作。
ObjLongConsumer 表示接受對象值和double值參數,並且沒有返回結果的操作。
ObjIntConsumer 表示接受對象值和整型值參數,並返回沒有結果的操作。
ObjLongConsumer 表示接受對象的值和long值的說法,並沒有返回結果的操作。
Predicate 代表一個參數謂詞(布爾值函數)。
Supplier 表示一個提供者的結果。
ToDoubleBiFunction 表示接受兩個參數,併產生一個double值結果的功能。
ToDoubleFunction 代表一個產生一個double值結果的功能。
ToIntBiFunction 表示接受兩個參數,併產生一個int值結果的函數。
ToIntFunction 代表產生一個int值結果的功能。
ToLongBiFunction 表示接受兩個參數,併產生long值結果的功能。
ToLongFunction 代表一個產生long值結果的功能。
UnaryOperator 表示上產生相同類型的操作數的結果的單個操作數的操作。

常用函數編程示例

Consumer 一個輸入 無輸出

Product product=new Product();
//類名+靜態方法  一個輸入T 沒有輸出
Consumer consumer1 = Product->Product.nameOf(product);//lambda
consumer1.accept(product);
Consumer consumer = Product::nameOf;//方法引用
consumer.accept(product);

Funtion<T,R> 一個輸入 一個輸出

//對象+方法  一個輸入T 一個輸出R
Function<Integer, Integer> function = product::reduceStock;
System.out.println("剩餘庫存:" + function.apply(10));
//帶參數的構造函數
Function<Integer,Product> function1=Product::new;
System.out.println("新對象:" +function1.apply(200));

Predicate 一個輸入T, 一個輸出 Boolean

//Predicate 一個輸入T 一個輸出Boolean
Predicate predicate= i -> product.isEnough(i);//lambda
System.out.println("庫存是否足夠:"+predicate.test(100));
Predicate predicate1= product::isEnough;//方法引用
System.out.println("庫存是否足夠:"+predicate1.test(100));

UnaryOperator 一元操作符 輸入輸出都是T

//一元操作符  輸入和輸出T
UnaryOperator integerUnaryOperator =product::reduceStock;
System.out.println("剩餘庫存:" + integerUnaryOperator.apply(20));
IntUnaryOperator intUnaryOperator = product::reduceStock;
System.out.println("剩餘庫存:" + intUnaryOperator.applyAsInt(30));

Supplier 沒有輸入 只有輸出

//無參數構造函數
Supplier supplier = Product::new;
System.out.println("創建新對象:" + supplier.get());
Supplier supplier1=()->product.getStock();
System.out.println("剩餘庫存:" + supplier1.get());

BiFunction 二元操作符 兩個輸入<T,U> 一個輸出

//類名+方法
BiFunction<Product, Integer, Integer> binaryOperator = Product::reduceStock;
System.out.println(" 剩餘庫存(BiFunction):" + binaryOperator.apply(product, 10));

BinaryOperator 二元操作符 ,二個輸入 一個輸出

//BinaryOperator binaryOperator1=(x,y)->product.reduceStock(x,y);
BinaryOperator binaryOperator1=product::reduceStock;
System.out.println(" 剩餘庫存(BinaryOperator):" +binaryOperator1.apply(product.getStock(),10));

Flux類中的靜態方法:

簡單的創建方法

just():

可以指定序列中包含的全部元素。創建出來的Flux序列在發佈這些元素之後會自動結束

fromArray(),fromIterable(),fromStream():

可以從一個數組,Iterable對象或Stream對象中穿件Flux對象

empty():

創建一個不包含任何元素,只發布結束消息的序列

error(Throwable error):

創建一個只包含錯誤消息的序列

never():

傳建一個不包含任務消息通知的序列

range(int start, int count):

創建包含從start起始的count個數量的Integer對象的序列

interval(Duration period)和interval(Duration delay, Duration period):

創建一個包含了從0開始遞增的Long對象的序列。其中包含的元素按照指定的間隔來發布。除了間隔時間之外,還可以指定起始元素髮布之前的延遲時間

intervalMillis(long period)和intervalMillis(long delay, long period):

與interval()方法相同,但該方法通過毫秒數來指定時間間隔和延遲時間

例子

Flux.just("Hello", "World").subscribe(System.out::println);
Flux.fromArray(new Integer[]{1, 2, 3}).subscribe(System.out::println);
Flux.empty().subscribe(System.out::println);
Flux.range(1, 10).subscribe(System.out::println);
Flux.interval(Duration.of(10, ChronoUnit.SECONDS)).subscribe(System.out::println);
Flux.intervalMillis(1000).subscirbe(System.out::println);

複雜的序列創建 generate()

當序列的生成需要複雜的邏輯時,則應該使用generate()或create()方法。

generate()方法通過同步和逐一的方式來產生Flux序列。

序列的產生是通過調用所提供的的SynchronousSink對象的next(),complete()和error(Throwable)方法來完成的。

逐一生成的含義是在具體的生成邏輯中,next()方法只能最多被調用一次。

在某些情況下,序列的生成可能是有狀態的,需要用到某些狀態對象,此時可以使用

generate(Callable<S> stateSupplier, BiFunction<S, SynchronousSink<T>, S> generator),

其中stateSupplier用來提供初始的狀態對象。

在進行序列生成時,狀態對象會作爲generator使用的第一個參數傳入,可以在對應的邏輯中對改狀態對象進行修改以供下一次生成時使用。

Flux.generate(sink -> {
  sink.next("Hello");
  sink.complete();  
}).subscribe(System.out::println);


final Random random = new Random();
Flux.generate(ArrayList::new, (list, sink) -> {
  int value = random.nextInt(100);
  list.add(value);
  sink.next(value);
  if( list.size() ==10 )
    sink.complete();
  return list;
}).subscribe(System.out::println);

複雜的序列創建 create()

 

create()方法與generate()方法的不同之處在於所使用的是FluxSink對象。

FluxSink支持同步和異步的消息產生,並且可以在一次調用中產生多個元素。

Flux.create(sink -> {
  for(int i = 0; i < 10; i ++)
    sink.next(i);
  sink.complete();
}).subscribe(System.out::println);

Mono靜態方法

  Mono類包含了與Flux類中相同的靜態方法:just(),empty()和never()等。

除此之外,Mono還有一些獨有的靜態方法:

    fromCallable(),fromCompletionStage(),fromFuture(),fromRunnable()和fromSupplier():分別從Callable,CompletionStage,CompletableFuture,Runnable和Supplier中創建Mono

    delay(Duration duration)和delayMillis(long duration):創建一個Mono序列,在指定的延遲時間之後,產生數字0作爲唯一值

    ignoreElements(Publisher source):創建一個Mono序列,忽略作爲源的Publisher中的所有元素,只產生消息

    justOrEmpty(Optional<? extends T> data)和justOrEmpty(T data):從一個Optional對象或可能爲null的對象中創建Mono。只有Optional對象中包含之或對象不爲null時,Mono序列才產生對應的元素

Mono.fromSupplier(() -> "Hello").subscribe(System.out::println);
Mono.justOrEmpty(Optional.of("Hello")).subscribe(System.out::println);
Mono.create(sink -> sink.success("Hello")).subscribte(System.out::println);

操作符

操作符buffer和bufferTimeout

這兩個操作符的作用是把當前流中的元素收集到集合中,並把集合對象作爲流中的新元素。

在進行收集時可以指定不同的條件:所包含的元素的最大數量或收集的時間間隔。方法buffer()僅使用一個條件,而bufferTimeout()可以同時指定兩個條件。

指定時間間隔時可以使用Duration對象或毫秒數,即使用bufferMillis()或bufferTimeoutMillis()兩個方法。

除了元素數量和時間間隔外,還可以通過bufferUntil和bufferWhile操作符來進行收集。這兩個操作符的參數時表示每個集合中的元素索要滿足的條件的Predicate對象。

bufferUntil會一直收集直到Predicate返回true。

使得Predicate返回true的那個元素可以選擇添加到當前集合或下一個集合中;bufferWhile則只有當Predicate返回true時纔會收集。一旦爲false,會立即開始下一次收集。

Flux.range(1, 100).buffer(20).subscribe(System.out::println);
Flux.intervalMillis(100).bufferMillis(1001).take(2).toStream().forEach(System.out::println);
Flux.range(1, 10).bufferUntil(i -> i%2 == 0).subscribe(System.out::println);
Flux.range(1, 10).bufferWhile(i -> i%2 == 0).subscribe(System.out::println);

操作符Filter

  對流中包含的元素進行過濾,只留下滿足Predicate指定條件的元素。

Flux.range(1, 10).filter(i -> i%2 == 0).subscribe(System.out::println);

操作符zipWith

  zipWith操作符把當前流中的元素與另一個流中的元素按照一對一的方式進行合併。在合併時可以不做任何處理,由此得到的是一個元素類型爲Tuple2的流;也可以通過一個BiFunction函數對合並的元素進行處理,所得到的流的元素類型爲該函數的返回值。

Flux.just("a", "b").zipWith(Flux.just("c", "d")).subscribe(System.out::println);
Flux.just("a", "b").zipWith(Flux.just("c", "d"), (s1, s2) -> String.format("%s-%s", s1, s2)).subscribe(System.out::println);

操作符take

  take系列操作符用來從當前流中提取元素。提取方式如下:

    take(long n),take(Duration timespan)和takeMillis(long timespan):按照指定的數量或時間間隔來提取

    takeLast(long n):提取流中的最後N個元素

    takeUntil(Predicate<? super T> predicate) :提取元素直到Predicate返回true

    takeWhile(Predicate<? super T> continuePredicate):當Predicate返回true時才進行提取

    takeUntilOther(Publisher<?> other):提取元素知道另外一個流開始產生元素

Flux.range(1, 1000).take(10).subscribe(System.out::println);
Flux.range(1, 1000).takeLast(10).subscribe(System.out::println);
Flux.range(1, 1000).takeWhile(i -> i < 10).subscribe(System.out::println);
Flux.range(1, 1000).takeUntil(i -> i == 10).subscribe(System.out::println);

操作符reduce和reduceWith

  reduce和reduceWith操作符對流中包含的所有元素進行累計操作,得到一個包含計算結果的Mono序列。累計操作是通過一個BiFunction來表示的。在操作時可以指定一個初始值。若沒有初始值,則序列的第一個元素作爲初始值。

Flux.range(1, 100).reduce((x, y) -> x + y).subscribe(System.out::println);
Flux.range(1, 100).reduceWith(() -> 100, (x + y) -> x + y).subscribe(System.out::println);

操作符merge和mergeSequential

  merge和mergeSequential操作符用來把多個流合併成一個Flux序列。merge按照所有流中元素的實際產生序列來合併,而mergeSequential按照所有流被訂閱的順序,以流爲單位進行合併。

Flux.merge(Flux.intervalMillis(0, 100).take(5), Flux.intervalMillis(50, 100).take(5)).toStream().forEach(System.out::println);
Flux.mergeSequential(Flux.intervalMillis(0, 100).take(5), Flux.intervalMillis(50, 100).take(5)).toStream().forEach(System.out::println);

操作符flatMap和flatMapSequential

  flatMap和flatMapSequential操作符把流中的每個元素轉換成一個流,再把所有流中的元素進行合併。flatMapSequential和flatMap之間的區別與mergeSequential和merge是一樣的。

Flux.just(5, 10).flatMap(x -> Flux.intervalMillis(x * 10, 100).take(x)).toStream().forEach(System.out::println);

操作符concatMap

  concatMap操作符的作用也是把流中的每個元素轉換成一個流,再把所有流進行合併。concatMap會根據原始流中的元素順序依次把轉換之後的流進行合併,並且concatMap堆轉換之後的流的訂閱是動態進行的,而flatMapSequential在合併之前就已經訂閱了所有的流。

Flux.just(5, 10).concatMap(x -> Flux.intervalMillis(x * 10, 100).take(x)).toStream().forEach(System.out::println);

操作符combineLatest

  combineLatest操作符把所有流中的最新產生的元素合併成一個新的元素,作爲返回結果流中的元素。只要其中任何一個流中產生了新的元素,合併操作就會被執行一次,結果流中就會產生新的元素。

Flux.combineLatest(Arrays::toString, Flux.intervalMillis(100).take(5), Flux.intervalMillis(50, 100).take(5)).toStream().forEach(System.out::println);

  

消息處理

  當需要處理Flux或Mono中的消息時,可以通過subscribe方法來添加相應的訂閱邏輯。

在調用subscribe方法時可以指定需要處理的消息類型。

Flux.just(1, 2).concatWith(Mono.error(new IllegalStateException())).subscribe(System.out::println, System.err::println);

Flux.just(1, 2).concatWith(Mono.error(new IllegalStateException())).onErrorReturn(0).subscribe(System.out::println);

  第2種可以通過switchOnError()方法來使用另外的流來產生元素。

Flux.just(1, 2).concatWith(Mono.error(new IllegalStateException())).switchOnError(Mono.just(0)).subscribe(System.out::println);

 第三種是通過onErrorResumeWith()方法來根據不同的異常類型來選擇要使用的產生元素的流。

Flux.just(1, 2).concatWith(Mono.error(new IllegalArgumentException())).onErrorResumeWith(e -> {
  if(e instanceof IllegalStateException)
    return Mono.just(0);
  else if(e instanceof IllegalArgumentException)
    return Mono.just(-1);
  return Mono.epmty();
}).subscribe(System,.out::println);

  當出現錯誤時還可以使用retry操作符來進行重試。重試的動作是通過重新訂閱序列來實現的。在使用retry操作時還可以指定重試的次數。

 Flux.just(1, 2).concatWith(Mono.error(new IllegalStateException())).retry(1).subscrible(System.out::println);

調度器Scheduler

  通過調度器可以指定操作執行的方式和所在的線程。有以下幾種不同的調度器實現

    當前線程,通過Schedulers.immediate()方法來創建

    單一的可複用的線程,通過Schedulers.single()方法來創建

    使用彈性的線程池,通過Schedulers.elastic()方法來創建。線程池中的線程是可以複用的。當所需要時,新的線程會被創建。若一個線程閒置時間太長,則會被銷燬。該調度器適用於I/O操作相關的流的處理

    使用對並行操作優化的線程池,通過Schedulers.parallel()方法來創建。其中的線程數量取決於CPU的核的數量。該調度器適用於計算密集型的流的處理

    使用支持任務調度的調度器,通過Schedulers.timer()方法來創建

    從已有的ExecutorService對象中創建調度器,通過Schedulers.fromExecutorService()方法來創建

  通過publishOn()和subscribeOn()方法可以切換執行操作調度器。publishOn()方法切換的是操作符的執行方式,而subscribeOn()方法切換的是產生流中元素時的執行方式

Flux.create(sink -> {
  sink.next(Thread.currentThread().getName());
  sink.complete();  
}).publishOn(Schedulers.single())
.map(x ->  String.format("[%s] %s", Thread.currentThread().getName(), x))
.publishOn(Schedulers.elastic())
.map(x -> String.format("[%s] %s", Thread.currentThread().getName(), x))
.subscribeOn(Schedulers.parallel())
.toStream()
.forEach(System.out::println);

測試

  StepVerifier的作用是可以對序列中包含的元素進行逐一驗證。通過StepVerifier.create()方法對一個流進行包裝之後再進行驗證。expectNext()方法用來聲明測試時所期待的流中的下一個元素的值,而verifyComplete()方法則驗證流是否正常結束。verifyError()來驗證流由於錯誤而終止。

StepVerifier.create(Flux.just(a, b)).expectNext("a").expectNext("b").verifyComplete();

  使用StepVerifier.withVirtualTime()方法可以創建出使用虛擬時鐘的SteoVerifier。通過thenAwait(Duration)方法可以讓虛擬時鐘前進。

StepVerifier.withVirtualTime(() -> Flux.interval(Duration.ofHours(4), Duration.ofDays(1)).take(2))
 .expectSubscription()
 .expectNoEvent(Duration.ofHours(4))
 .expectNext(0L)
 .thenAwait(Duration.ofDays(1))
 .expectNext(1L)
 .verifyComplete();

  TestPublisher的作用在於可以控制流中元素的產生,甚至是違反反應流規範的情況。通過create()方法創建一個新的TestPublisher對象,然後使用next()方法來產生元素,使用complete()方法來結束流。

final TestPublisher<String> testPublisher = TestPublisher.creater();
testPublisher.next("a");
testPublisher.next("b");
testPublisher.complete();

StepVerifier.create(testPublisher)
    .expectNext("a")
    .expectNext("b")
    .expectComplete();

調試

  在調試模式啓用之後,所有的操作符在執行時都會保存額外的與執行鏈相關的信息。當出現錯誤時,這些信息會被作爲異常堆棧信息的一部分輸出。

Hooks.onOperator(providedHook -> providedHook.operatorStacktrace());

  也可以通過checkpoint操作符來對特定的流處理鏈來啓用調試模式。

Flux.just(1, 0).map(x -> 1/x).checkpoint("test").subscribe(System.out::println);

日誌記錄

  可以通過添加log操作把流相關的事件記錄在日誌中,

Flux.range(1, 2).log("Range").subscribe(System.out::println);

冷熱序列

  冷序列的含義是不論訂閱者在何時訂閱該序列,總是能收到序列中產生的全部消息。熱序列是在持續不斷的產生消息,訂閱者只能獲取到在其訂閱之後產生的消息。

  

final Flux<Long> source = Flux.intervalMillis(1000).take(10).publish.autoConnect();
source.subscribe();
Thread.sleep(5000);
source.toStream().forEach(System.out::println);

ServerWebExchange交換機

ServerWebExchange與過濾器的關係:

Spring Cloud Gateway同zuul類似,有“pre”和“post”兩種方式的filter。

客戶端的請求先經過“pre”類型的filter,然後將請求轉發到具體的業務服務,收到業務服務的響應之後,再經過“post”類型的filter處理,最後返回響應到客戶端。

引用Spring Cloud Gateway官網上的一張圖:
image

與zuul不同的是,filter除了分爲“pre”和“post”兩種方式的filter外,在Spring Cloud Gateway中,filter從作用範圍可分爲另外兩種,

一種是針對於單個路由的gateway filter,它在配置文件中的寫法同predict類似;

一種是針對於所有路由的global gateway filer

現在從作用範圍劃分的維度來講解這兩種filter。

我們在使用Spring Cloud Gateway的時候,注意到過濾器(包括GatewayFilterGlobalFilter和過濾器鏈GatewayFilterChain)。

Spring Cloud Gateway根據作用範圍劃分爲GatewayFilter和GlobalFilter,二者區別如下:

  • GatewayFilter : 需要通過spring.cloud.routes.filters 配置在具體路由下,只作用在當前路由上或通過spring.cloud.default-filters配置在全局,作用在所有路由上
  • GlobalFilter : 全局過濾器,不需要在配置文件中配置,作用在所有的路由上,最終通過GatewayFilterAdapter包裝成GatewayFilterChain可識別的過濾器,它爲請求業務以及路由的URI轉換爲真實業務服務的請求地址的核心過濾器,不需要配置,系統初始化時加載,並作用在每個路由上。

Spring Cloud Gateway框架內置的GlobalFilter如下:

img

上圖中每一個GlobalFilter都作用在每一個router上,能夠滿足大多數的需求。

但是如果遇到業務上的定製,可能需要編寫滿足自己需求的GlobalFilter。

過濾器都依賴到ServerWebExchange

public interface GlobalFilter {

    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

public interface GatewayFilter extends ShortcutConfigurable {

	Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

public interface GatewayFilterChain {

    Mono<Void> filter(ServerWebExchange exchange);
}    

這裏的設計和Servlet中的Filter是相似的,

當前過濾器可以決定是否執行下一個過濾器的邏輯,由GatewayFilterChain#filter()是否被調用來決定。

ServerWebExchange就相當於當前請求和響應的上下文。

ServerWebExchange實例不單存儲了RequestResponse對象,還提供了一些擴展方法,如果想實現改造請求參數或者響應參數,就必須深入瞭解ServerWebExchange

理解ServerWebExchange

先看ServerWebExchange的註釋:

Contract for an HTTP request-response interaction.

Provides access to the HTTP request and response and also exposes additional server-side processing related properties and features such as request attributes.

翻譯一下大概是:

ServerWebExchange是一個HTTP請求-響應交互的契約。提供對HTTP請求和響應的訪問,並公開額外的服務器端處理相關屬性和特性,如請求屬性。

其實,ServerWebExchange命名爲服務網絡交換器,存放着重要的請求-響應屬性、請求實例和響應實例等等,有點像Context的角色。

ServerWebExchange接口

ServerWebExchange接口的所有方法:

public interface ServerWebExchange {

    // 日誌前綴屬性的KEY,值爲org.springframework.web.server.ServerWebExchange.LOG_ID
    // 可以理解爲 attributes.set("org.springframework.web.server.ServerWebExchange.LOG_ID","日誌前綴的具體值");
    // 作用是打印日誌的時候會拼接這個KEY對飲的前綴值,默認值爲""
    String LOG_ID_ATTRIBUTE = ServerWebExchange.class.getName() + ".LOG_ID";
    String getLogPrefix();

    // 獲取ServerHttpRequest對象
    ServerHttpRequest getRequest();

    // 獲取ServerHttpResponse對象
    ServerHttpResponse getResponse();
    
    // 返回當前exchange的請求屬性,返回結果是一個可變的Map
    Map<String, Object> getAttributes();
    
    // 根據KEY獲取請求屬性
    @Nullable
    default <T> T getAttribute(String name) {
        return (T) getAttributes().get(name);
    }
    
    // 根據KEY獲取請求屬性,做了非空判斷
    @SuppressWarnings("unchecked")
    default <T> T getRequiredAttribute(String name) {
        T value = getAttribute(name);
        Assert.notNull(value, () -> "Required attribute '" + name + "' is missing");
        return value;
    }

     // 根據KEY獲取請求屬性,需要提供默認值
    @SuppressWarnings("unchecked")
    default <T> T getAttributeOrDefault(String name, T defaultValue) {
        return (T) getAttributes().getOrDefault(name, defaultValue);
    } 

    // 返回當前請求的網絡會話
    Mono<WebSession> getSession();

    // 返回當前請求的認證用戶,如果存在的話
    <T extends Principal> Mono<T> getPrincipal();  
    
    // 返回請求的表單數據或者一個空的Map,只有Content-Type爲application/x-www-form-urlencoded的時候這個方法纔會返回一個非空的Map -- 這個一般是表單數據提交用到
    Mono<MultiValueMap<String, String>> getFormData();   
    
    // 返回multipart請求的part數據或者一個空的Map,只有Content-Type爲multipart/form-data的時候這個方法纔會返回一個非空的Map  -- 這個一般是文件上傳用到
    Mono<MultiValueMap<String, Part>> getMultipartData();
    
    // 返回Spring的上下文
    @Nullable
    ApplicationContext getApplicationContext();   

    // 這幾個方法和lastModified屬性相關
    boolean isNotModified();
    boolean checkNotModified(Instant lastModified);
    boolean checkNotModified(String etag);
    boolean checkNotModified(@Nullable String etag, Instant lastModified);
    
    // URL轉換
    String transformUrl(String url);    
   
    // URL轉換映射
    void addUrlTransformer(Function<String, String> transformer); 

    // 注意這個方法,方法名是:改變,這個是修改ServerWebExchange屬性的方法,返回的是一個Builder實例,Builder是ServerWebExchange的內部類
    default Builder mutate() {
	     return new DefaultServerWebExchangeBuilder(this);
    }

    interface Builder {      
         
        // 覆蓋ServerHttpRequest
        Builder request(Consumer<ServerHttpRequest.Builder> requestBuilderConsumer);
        Builder request(ServerHttpRequest request);
        
        // 覆蓋ServerHttpResponse
        Builder response(ServerHttpResponse response);
        
        // 覆蓋當前請求的認證用戶
        Builder principal(Mono<Principal> principalMono);
    
        // 構建新的ServerWebExchange實例
        ServerWebExchange build();
    }
}    

ServerWebExchange#mutate()方法

注意到ServerWebExchange#mutate()方法,ServerWebExchange實例可以理解爲不可變實例,

如果我們想要修改它,需要通過mutate()方法生成一個新的實例,例如這樣:

public class CustomGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 這裏可以修改ServerHttpRequest實例
        ServerHttpRequest newRequest = ...
        ServerHttpResponse response = exchange.getResponse();
        // 這裏可以修改ServerHttpResponse實例
        ServerHttpResponse newResponse = ...
        // 構建新的ServerWebExchange實例
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).response(newResponse).build();
        return chain.filter(newExchange);
    }
}

ServerHttpRequest接口

ServerHttpRequest實例是用於承載請求相關的屬性和請求體,

Spring Cloud Gateway中底層使用Netty處理網絡請求,通過追溯源碼,

可以從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpRequest實例的具體實現是ReactorServerHttpRequest

之所以列出這些實例之間的關係,是因爲這樣比較容易理清一些隱含的問題,例如:

  • ReactorServerHttpRequest的父類AbstractServerHttpRequest中初始化內部屬性headers的時候把請求的HTTP頭部封裝爲只讀的實例
public AbstractServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers) {
	this.uri = uri;
	this.path = RequestPath.parse(uri, contextPath);
	this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
}

// HttpHeaders類中的readOnlyHttpHeaders方法,
// ReadOnlyHttpHeaders屏蔽了所有修改請求頭的方法,直接拋出UnsupportedOperationException
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
	Assert.notNull(headers, "HttpHeaders must not be null");
	if (headers instanceof ReadOnlyHttpHeaders) {
		return headers;
	}
	else {
		return new ReadOnlyHttpHeaders(headers);
	}
}

所以, 不能直接從ServerHttpRequest實例中直接獲取請求頭HttpHeaders實例並且進行修改

ServerHttpRequest接口如下:

public interface HttpMessage {
    
    // 獲取請求頭,目前的實現中返回的是ReadOnlyHttpHeaders實例,只讀
    HttpHeaders getHeaders();
}    

public interface ReactiveHttpInputMessage extends HttpMessage {
    
    // 返回請求體的Flux封裝
    Flux<DataBuffer> getBody();
}

public interface HttpRequest extends HttpMessage {

    // 返回HTTP請求方法,解析爲HttpMethod實例
    @Nullable
    default HttpMethod getMethod() {
        return HttpMethod.resolve(getMethodValue());
    }
    
    // 返回HTTP請求方法,字符串
    String getMethodValue();    
    
    // 請求的URI
    URI getURI();
}    

public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
    
    // 連接的唯一標識或者用於日誌處理標識
    String getId();   
    
    // 獲取請求路徑,封裝爲RequestPath對象
    RequestPath getPath();
    
    // 返回查詢參數,是隻讀的MultiValueMap實例
    MultiValueMap<String, String> getQueryParams();

    // 返回Cookie集合,是隻讀的MultiValueMap實例
    MultiValueMap<String, HttpCookie> getCookies();  
    
    // 遠程服務器地址信息
    @Nullable
    default InetSocketAddress getRemoteAddress() {
       return null;
    }

    // SSL會話實現的相關信息
    @Nullable
    default SslInfo getSslInfo() {
       return null;
    }  
    
    // 修改請求的方法,返回一個建造器實例Builder,Builder是內部類
    default ServerHttpRequest.Builder mutate() {
        return new DefaultServerHttpRequestBuilder(this);
    } 

    interface Builder {

        // 覆蓋請求方法
        Builder method(HttpMethod httpMethod);
		 
        // 覆蓋請求的URI、請求路徑或者上下文,這三者相互有制約關係,具體可以參考API註釋
        Builder uri(URI uri);
        Builder path(String path);
        Builder contextPath(String contextPath);

        // 覆蓋請求頭
        Builder header(String key, String value);
        Builder headers(Consumer<HttpHeaders> headersConsumer);
        
        // 覆蓋SslInfo
        Builder sslInfo(SslInfo sslInfo);
        
        // 構建一個新的ServerHttpRequest實例
        ServerHttpRequest build();
    }         
}    

注意:

ServerHttpRequest或者說HttpMessage接口提供的獲取請求頭方法HttpHeaders getHeaders();

返回結果是一個只讀的實例,具體是ReadOnlyHttpHeaders類型,

如果要修改ServerHttpRequest實例,那麼需要這樣做:

ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest newRequest = request.mutate().header("key","value").path("/myPath").build();

ServerHttpResponse接口

ServerHttpResponse實例是用於承載響應相關的屬性和響應體,

Spring Cloud Gateway中底層使用Netty處理網絡請求,通過追溯源碼,可以從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpResponse實例的具體實現是ReactorServerHttpResponse

之所以列出這些實例之間的關係,是因爲這樣比較容易理清一些隱含的問題,例如:

// ReactorServerHttpResponse的父類
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory, HttpHeaders headers) {
	Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null");
	Assert.notNull(headers, "HttpHeaders must not be null");
	this.dataBufferFactory = dataBufferFactory;
	this.headers = headers;
	this.cookies = new LinkedMultiValueMap<>();
}

public ReactorServerHttpResponse(HttpServerResponse response, DataBufferFactory bufferFactory) {
	super(bufferFactory, new HttpHeaders(new NettyHeadersAdapter(response.responseHeaders())));
	Assert.notNull(response, "HttpServerResponse must not be null");
	this.response = response;
}

可知ReactorServerHttpResponse構造函數初始化實例的時候,存放響應Header的是HttpHeaders實例,也就是響應Header是可以直接修改的。

ServerHttpResponse接口如下:

public interface HttpMessage {
    
    // 獲取響應Header,目前的實現中返回的是HttpHeaders實例,可以直接修改
    HttpHeaders getHeaders();
}  

public interface ReactiveHttpOutputMessage extends HttpMessage {
    
    // 獲取DataBufferFactory實例,用於包裝或者生成數據緩衝區DataBuffer實例(創建響應體)
    DataBufferFactory bufferFactory();

    // 註冊一個動作,在HttpOutputMessage提交之前此動作會進行回調
    void beforeCommit(Supplier<? extends Mono<Void>> action);

    // 判斷HttpOutputMessage是否已經提交
    boolean isCommitted();
    
    // 寫入消息體到HTTP協議層
    Mono<Void> writeWith(Publisher<? extends DataBuffer> body);

    // 寫入消息體到HTTP協議層並且刷新緩衝區
    Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body);
    
    // 指明消息處理已經結束,一般在消息處理結束自動調用此方法,多次調用不會產生副作用
    Mono<Void> setComplete();
}

public interface ServerHttpResponse extends ReactiveHttpOutputMessage {
    
    // 設置響應狀態碼
    boolean setStatusCode(@Nullable HttpStatus status);
    
    // 獲取響應狀態碼
    @Nullable
    HttpStatus getStatusCode();
    
    // 獲取響應Cookie,封裝爲MultiValueMap實例,可以修改
    MultiValueMap<String, ResponseCookie> getCookies();  
    
    // 添加響應Cookie
    void addCookie(ResponseCookie cookie);  
}    

這裏可以看到除了響應體比較難修改之外,其他的屬性都是可變的。

ServerWebExchangeUtils和上下文屬性

ServerWebExchangeUtils裏面存放了很多靜態公有的字符串KEY值

(這些字符串KEY的實際值是org.springframework.cloud.gateway.support.ServerWebExchangeUtils. + 下面任意的靜態公有KEY),

這些字符串KEY值一般是用於ServerWebExchange的屬性(Attribute,見上文的ServerWebExchange#getAttributes()方法)的KEY,這些屬性值都是有特殊的含義,在使用過濾器的時候如果時機適當可以直接取出來使用,下面逐個分析。

  • PRESERVE_HOST_HEADER_ATTRIBUTE:是否保存Host屬性,值是布爾值類型,寫入位置是PreserveHostHeaderGatewayFilterFactory,使用的位置是NettyRoutingFilter,作用是如果設置爲true,HTTP請求頭中的Host屬性會寫到底層Reactor-Netty的請求Header屬性中。
  • CLIENT_RESPONSE_ATTR:保存底層Reactor-Netty的響應對象,類型是reactor.netty.http.client.HttpClientResponse
  • CLIENT_RESPONSE_CONN_ATTR:保存底層Reactor-Netty的連接對象,類型是reactor.netty.Connection
  • URI_TEMPLATE_VARIABLES_ATTRIBUTEPathRoutePredicateFactory解析路徑參數完成之後,把解析完成後的佔位符KEY-路徑Path映射存放在ServerWebExchange的屬性中,KEY就是URI_TEMPLATE_VARIABLES_ATTRIBUTE
  • CLIENT_RESPONSE_HEADER_NAMES:保存底層Reactor-Netty的響應Header的名稱集合。
  • GATEWAY_ROUTE_ATTR:用於存放RoutePredicateHandlerMapping中匹配出來的具體的路由(org.springframework.cloud.gateway.route.Route)實例,通過這個路由實例可以得知當前請求會路由到下游哪個服務。
  • GATEWAY_REQUEST_URL_ATTRjava.net.URI類型的實例,這個實例代表直接請求或者負載均衡處理之後需要請求到下游服務的真實URI。
  • GATEWAY_ORIGINAL_REQUEST_URL_ATTRjava.net.URI類型的實例,需要重寫請求URI的時候,保存原始的請求URI。
  • GATEWAY_HANDLER_MAPPER_ATTR:保存當前使用的HandlerMapping具體實例的類型簡稱(一般是字符串"RoutePredicateHandlerMapping")。
  • GATEWAY_SCHEME_PREFIX_ATTR:確定目標路由URI中如果存在schemeSpecificPart屬性,則保存該URI的scheme在此屬性中,路由URI會被重新構造,見RouteToRequestUrlFilter
  • GATEWAY_PREDICATE_ROUTE_ATTR:用於存放RoutePredicateHandlerMapping中匹配出來的具體的路由(org.springframework.cloud.gateway.route.Route)實例的ID。
  • WEIGHT_ATTR:實驗性功能(此版本還不建議在正式版本使用)存放分組權重相關屬性,見WeightCalculatorWebFilter
  • ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR:存放響應Header中的ContentType的值。
  • HYSTRIX_EXECUTION_EXCEPTION_ATTRThrowable的實例,存放的是Hystrix執行異常時候的異常實例,見HystrixGatewayFilterFactory
  • GATEWAY_ALREADY_ROUTED_ATTR:布爾值,用於判斷是否已經進行了路由,見NettyRoutingFilter
  • GATEWAY_ALREADY_PREFIXED_ATTR:布爾值,用於判斷請求路徑是否被添加了前置部分,見PrefixPathGatewayFilterFactory

ServerWebExchangeUtils提供的上下文屬性用於Spring Cloud GatewayServerWebExchange組件處理請求和響應的時候,內部一些重要實例或者標識屬性的安全傳輸和使用,使用它們可能存在一定的風險,

因爲沒有人可以確定在版本升級之後,原有的屬性KEY或者VALUE是否會發生改變,如果評估過風險或者規避了風險之後,可以安心使用。

例如我們在做請求和響應日誌(類似Nginx的Access Log)的時候,可以依賴到GATEWAY_ROUTE_ATTR,因爲我們要打印路由的目標信息。舉個簡單例子:

@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        HttpMethod method = request.getMethod();
        // 獲取路由的目標URI
        URI targetUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        return chain.filter(exchange.mutate().build()).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            log.info("請求路徑:{},客戶端遠程IP地址:{},請求方法:{},目標URI:{},響應碼:{}",
                    path, remoteAddress, method, targetUri, statusCode);
        }));
    }
}

修改請求體

修改請求體是一個比較常見的需求。

例如我們使用Spring Cloud Gateway實現網關的時候,要實現一個功能:

把存放在請求頭中的JWT解析後,提取裏面的用戶ID,然後寫入到請求體中。

我們簡化這個場景,假設我們把userId明文存放在請求頭中的accessToken中,請求體是一個JSON結構:

{
    "serialNumber": "請求流水號",
    "payload" : {
        // ... 這裏是有效載荷,存放具體的數據
    }
}

我們需要提取accessToken,也就是userId插入到請求體JSON中如下:

{
    "userId": "用戶ID",
    "serialNumber": "請求流水號",
    "payload" : {
        // ... 這裏是有效載荷,存放具體的數據
    }
}

這裏爲了簡化設計,用全局過濾器GlobalFilter實現,實際需要結合具體場景考慮:

@Slf4j
@Component
public class ModifyRequestBodyGlobalFilter implements GlobalFilter {

    private final DataBufferFactory dataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String accessToken = request.getHeaders().getFirst("accessToken");
        if (!StringUtils.hasLength(accessToken)) {
            throw new IllegalArgumentException("accessToken");
        }
        // 新建一個ServerHttpRequest裝飾器,覆蓋需要裝飾的方法
        ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(request) {

            @Override
            public Flux<DataBuffer> getBody() {
                Flux<DataBuffer> body = super.getBody();
                InputStreamHolder holder = new InputStreamHolder();
                body.subscribe(buffer -> holder.inputStream = buffer.asInputStream());
                if (null != holder.inputStream) {
                    try {
                        
                        // 解析JSON的節點
                        JsonNode jsonNode = objectMapper.readTree(holder.inputStream);
                        Assert.isTrue(jsonNode instanceof ObjectNode, "JSON格式異常");
                        ObjectNode objectNode = (ObjectNode) jsonNode;
                        
                        // JSON節點最外層寫入新的屬性
                        objectNode.put("userId", accessToken);
                        DataBuffer dataBuffer = dataBufferFactory.allocateBuffer();
                        String json = objectNode.toString();
                        log.info("最終的JSON數據爲:{}", json);
                        dataBuffer.write(json.getBytes(StandardCharsets.UTF_8));
                        
                        return Flux.just(dataBuffer);
                    } catch (Exception e) {
                        throw new IllegalStateException(e);
                    }
                } else {
                    return super.getBody();
                }
            }
        };
        // 使用修改後的ServerHttpRequestDecorator重新生成一個新的ServerWebExchange
        return chain.filter(exchange.mutate().request(decorator).build());
    }

    private class InputStreamHolder {

        InputStream inputStream;
    }
}

測試一下:

// HTTP
POST /order/json HTTP/1.1
Host: localhost:9090
Content-Type: application/json
accessToken: 10086
Accept: */*
Cache-Control: no-cache
Host: localhost:9090
accept-encoding: gzip, deflate
content-length: 94
Connection: keep-alive
cache-control: no-cache

{
    "serialNumber": "請求流水號",
    "payload": {
        "name": "doge"
    }
}

// 日誌輸出
最終的JSON數據爲:{"serialNumber":"請求流水號","payload":{"name":"doge"},"userId":"10086"}

最重要的是用到了ServerHttpRequest裝飾器ServerHttpRequestDecorator,主要覆蓋對應獲取請求體數據緩衝區的方法即可,至於怎麼處理其他邏輯需要自行考慮,這裏只是做一個簡單的示範。

一般的代碼邏輯如下:

ServerHttpRequest request = exchange.getRequest();
ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(request) {

     @Override
     public Flux<DataBuffer> getBody() {
         // 拿到承載原始請求體的Flux
         Flux<DataBuffer> body = super.getBody();
         // 這裏通過自定義方式生成新的承載請求體的Flux
         Flux<DataBuffer> newBody = ...
         return newBody;
     }            
}
return chain.filter(exchange.mutate().request(requestDecorator).build());    

修改響應體

修改響應體的需求也是比較常見的,具體的做法和修改請求體差不多。

例如我們想要實現下面的功能:第三方服務請求經過網關,原始報文是密文,我們需要在網關實現密文解密,然後把解密後的明文路由到下游服務,下游服務處理成功響應明文,需要在網關把明文加密成密文再返回到第三方服務。

現在簡化整個流程,用AES加密算法,統一密碼爲字符串"throwable",假設請求報文和響應報文明文如下:

// 請求密文
{
    "serialNumber": "請求流水號",
    "payload" : "加密後的請求消息載荷"
}

// 請求明文(僅僅作爲提示)
{
    "serialNumber": "請求流水號",
    "payload" : "{\"name:\":\"doge\"}"
}

// 響應密文
{
    "code": 200,
    "message":"ok",
    "payload" : "加密後的響應消息載荷"
}

// 響應明文(僅僅作爲提示)
{
    "code": 200,
    "message":"ok",
    "payload" : "{\"name:\":\"doge\",\"age\":26}"
}

爲了方便一些加解密或者編碼解碼的實現,需要引入Apachecommons-codec類庫:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.12</version>
</dependency>

這裏定義一個全局過濾器專門處理加解密,實際上最好結合真實的場景決定是否適合全局過濾器,這裏只是一個示例:

// AES加解密工具類
public enum AesUtils {

    // 單例
    X;

    private static final String PASSWORD = "throwable";
    private static final String KEY_ALGORITHM = "AES";
    private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    public String encrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, provideSecretKey());
            return Hex.encodeHexString(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public byte[] decrypt(String content) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, provideSecretKey());
            return cipher.doFinal(Hex.decodeHex(content));
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    private SecretKey provideSecretKey() {
        try {
            KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM);
            SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
            secureRandom.setSeed(PASSWORD.getBytes(StandardCharsets.UTF_8));
            keyGen.init(128, secureRandom);
            return new SecretKeySpec(keyGen.generateKey().getEncoded(), KEY_ALGORITHM);
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }
}

// EncryptionGlobalFilter
@Slf4j
@Component
public class EncryptionGlobalFilter implements GlobalFilter, Ordered {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public int getOrder() {
        return -2;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 響應體
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
        ServerHttpRequestDecorator requestDecorator = processRequest(request, bufferFactory);
        ServerHttpResponseDecorator responseDecorator = processResponse(response, bufferFactory);
        return chain.filter(exchange.mutate().request(requestDecorator).response(responseDecorator).build());
    }

    private ServerHttpRequestDecorator processRequest(ServerHttpRequest request, DataBufferFactory bufferFactory) {
        Flux<DataBuffer> body = request.getBody();
        DataBufferHolder holder = new DataBufferHolder();
        body.subscribe(dataBuffer -> {
            int len = dataBuffer.readableByteCount();
            holder.length = len;
            byte[] bytes = new byte[len];
            dataBuffer.read(bytes);
            DataBufferUtils.release(dataBuffer);
            String text = new String(bytes, StandardCharsets.UTF_8);
            JsonNode jsonNode = readNode(text);
            JsonNode payload = jsonNode.get("payload");
            String payloadText = payload.asText();
            byte[] content = AesUtils.X.decrypt(payloadText);
            String requestBody = new String(content, StandardCharsets.UTF_8);
            log.info("修改請求體payload,修改前:{},修改後:{}", payloadText, requestBody);
            rewritePayloadNode(requestBody, jsonNode);
            DataBuffer data = bufferFactory.allocateBuffer();
            data.write(jsonNode.toString().getBytes(StandardCharsets.UTF_8));
            holder.dataBuffer = data;
        });
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(request.getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        return new ServerHttpRequestDecorator(request) {

            @Override
            public HttpHeaders getHeaders() {
                int contentLength = holder.length;
                if (contentLength > 0) {
                    headers.setContentLength(contentLength);
                } else {
                    headers.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return headers;
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return Flux.just(holder.dataBuffer);
            }
        };
    }

    private ServerHttpResponseDecorator processResponse(ServerHttpResponse response, DataBufferFactory bufferFactory) {
        return new ServerHttpResponseDecorator(response) {

            @SuppressWarnings("unchecked")
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(flux.map(buffer -> {
                        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                        DataBufferUtils.release(buffer);
                        JsonNode jsonNode = readNode(charBuffer.toString());
                        JsonNode payload = jsonNode.get("payload");
                        String text = payload.toString();
                        String content = AesUtils.X.encrypt(text);
                        log.info("修改響應體payload,修改前:{},修改後:{}", text, content);
                        setPayloadTextNode(content, jsonNode);
                        return bufferFactory.wrap(jsonNode.toString().getBytes(StandardCharsets.UTF_8));
                    }));
                }
                return super.writeWith(body);
            }
        };
    }

    private void rewritePayloadNode(String text, JsonNode root) {
        try {
            JsonNode node = objectMapper.readTree(text);
            ObjectNode objectNode = (ObjectNode) root;
            objectNode.set("payload", node);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private void setPayloadTextNode(String text, JsonNode root) {
        try {
            ObjectNode objectNode = (ObjectNode) root;
            objectNode.set("payload", new TextNode(text));
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private JsonNode readNode(String in) {
        try {
            return objectMapper.readTree(in);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

    private class DataBufferHolder {

        DataBuffer dataBuffer;
        int length;
    }
}  

先準備一份密文:

Map<String, Object> json = new HashMap<>(8);
json.put("serialNumber", "請求流水號");
String content = "{\"name\": \"doge\"}";
json.put("payload", AesUtils.X.encrypt(content));
System.out.println(new ObjectMapper().writeValueAsString(json));

// 輸出
{"serialNumber":"請求流水號","payload":"144e3dc734743f5709f1adf857bca473da683246fd612f86ac70edeb5f2d2729"}

模擬請求:

POST /order/json HTTP/1.1
Host: localhost:9090
accessToken: 10086
Content-Type: application/json
User-Agent: PostmanRuntime/7.13.0
Accept: */*
Cache-Control: no-cache
Postman-Token: bda07fc3-ea1a-478c-b4d7-754fe6f37200,634734d9-feed-4fc9-ba20-7618bd986e1c
Host: localhost:9090
cookie: customCookieName=customCookieValue
accept-encoding: gzip, deflate
content-length: 104
Connection: keep-alive
cache-control: no-cache

{
    "serialNumber": "請求流水號",
    "payload": "FE49xzR0P1cJ8a34V7ykc9poMkb9YS+GrHDt618tJyk="
}

// 響應結果
{
    "serialNumber": "請求流水號",
    "payload": "oo/K1igg2t/S8EExkBVGWOfI1gAh5pBpZ0wyjNPW6e8="   # <--- 解密後:{"name":"doge","age":26}
}

遇到的問題:

  • 必須實現Ordered接口,返回一個小於-1的order值,這是因爲NettyWriteResponseFilter的order值爲-1,我們需要覆蓋返回響應體的邏輯,自定義的GlobalFilter必須比NettyWriteResponseFilter優先執行。
  • 網關每次重啓之後,第一個請求總是無法從原始的ServerHttpRequest讀取到有效的Body,準確來說出現的現象是NettyRoutingFilter調用ServerHttpRequest#getBody()的時候獲取到一個空的對象,導致空指針;奇怪的是從第二個請求開始就能正常調用。筆者把**Spring Cloud Gateway**的版本降低到**Finchley.SR3****Spring Boot**的版本降低到**2.0.8.RELEASE**,問題不再出現,初步確定是**Spring Cloud Gateway**版本升級導致的兼容性問題或者是BUG

最重要的是用到了ServerHttpResponse裝飾器ServerHttpResponseDecorator,主要覆蓋寫入響應體數據緩衝區的部分,至於怎麼處理其他邏輯需要自行考慮,這裏只是做一個簡單的示範。一般的代碼邏輯如下:

ServerHttpResponse response = exchange.getResponse();
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(response) {

            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(flux.map(buffer -> {
                        // buffer就是原始的響應數據的緩衝區
                        // 下面處理完畢之後返回新的響應數據的緩衝區即可
                        return bufferFactory.wrap(...);
                    }));
                }
                return super.writeWith(body);
            }
        };
return chain.filter(exchange.mutate().response(responseDecorator).build());    

參考文獻

https://blog.csdn.net/wpc2018/article/details/122634049

https://www.jianshu.com/p/7d80b94068b3

https://blog.csdn.net/yhj_911/article/details/119540000

http://bjqianye.cn/detail/6845.html

https://blog.csdn.net/hao134838/article/details/110824092

https://blog.csdn.net/hao134838/article/details/110824092

https://blog.csdn.net/weixin_34096182/article/details/91436704

https://blog.csdn.net/fly910905/article/details/121682625

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