- content {:toc}
前言
反應式編程是一種可以替代命令式編程的編程範式。這種可替代性存在的原因在於反應式編程解決了命令式編程中的一些限制。理解這些限制,有助於你更好地理解反應式編程模型的優點
反應式流規範
-
對比 Java 中的流
Java的流和反應式流Java的流和反應式流之間有很多相似之處。首先,它們的名字中都有流(Stream)這個詞。
它們還提供了用於處理數據的函數式API。事實上,正如你稍後將會在介紹Reactor時看到的那樣,它們甚至可以共享許多相同的操作。
Java的流通常都是同步的,並且只能處理有限的數據集。從本質上來說,它們只是使用函數來對集合進行迭代的一種方式。
反應式流支持異步處理任意大小的數據集,同樣也包括無限數據集。只要數據就緒,它們就能實時地處理數據,並且能夠通過回壓來避免壓垮數據的消費者。
-
反應式流規範
反應式流規範可以總結爲4個接口:Publisher、Subscriber、Subscription和Processor。
Publisher負責生成數據,並將數據發送給Subscription(每個Subscriber對應一個Subscription)。
Publisher接口聲明瞭一個方法subscribe(),Subscriber可以通過該方法向Publisher發起訂閱。
public interface Publisher<T> {
void subscribe(Subscriber<? super T> var1);
}
public interface Publisher<T> {
void subscribe(Subscriber<? super T> var1);
}
public interface Subscriber<T> {
void onSubscribe(Subscription var1);
void onNext(T var1);
void onError(Throwable var1);
void onComplete();
}
public interface Subscription {
void request(long var1);
void cancel();
}
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}
初識Reactor
Reactor項目是反應式流規範的一個實現,提供了一組用於組裝反應式流的函數式API。
反應式編程要求我們採取和命令式編程不一樣的思維方式。此時我們不會再描述每一步要進行的步驟,反應式編程意味着要構建數據將要流經的管道。當數據流經管道時,可以對它們進行某種形式的修改或者使用。
命令式編程:
String a = "Apple";
String s = a.toUpperCase();
String s1 = "hello" + s + "!";
System.out.println(s1);
反應式編程:
Mono.just("Apple")
.map(String::toUpperCase)
.map(x-> "hello" + x + "!")
.subscribe(System.out::println);
控制檯:
helloAPPLE!
14:36:38.685 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
helloAPPLE!
這個例子中的Mono是Reactor的兩種核心類型之一,另一個類型是Flux。兩者都實現了反應式流的Publisher接口。Flux代表具有零個、一個或者多個(可能是無限個)數據項的管道.
添加Reactor依賴
要開始使用Reactor,請將下面的依賴項添加到項目的構建文件中:
<!--reactor core-->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<!--reactor test-->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<version>3.2.6.RELEASE</version>
<scope>compile</scope>
</dependency>
使用常見的反應式操作
Flux和Mono是Reactor提供的最基礎的構建塊,而這兩種反應式類型所提供的操作符則是組合使用它們以構建數據流動管線的黏合劑。
Flux和Mono共有500多個操作,這些操作都可以大致歸類爲:
•創建操作;
•組合操作;
•轉換操作;
•邏輯操作。
- 1 創建
Flux<String> fruitFlux = Flux.just("Apple","Orange");
fruitFlux.subscribe(System.out::println);
這裏傳遞給subscribe()方法的lambda表達式實際上是一個java.util.Consumer,用來創建反應式流的Subscriber。在調用subscribe()之後,數據會開始流動。在這個例子中,沒有中間操作,所以數據從Flux直接流向訂閱者
要驗證預定義的數據是否流經了fruitFlux,我們可以編寫如下所示的測試代碼:
StepVerifier.create(fruitFlux)
.expectNext("Apple")
.expectNext("Orange")
.verifyComplete();
在這個例子中,StepVerifier訂閱了fruitFlux,然後斷言Flux中的每個數據項是否與預期的水果名稱相匹配。最後,它驗證Flux在發佈完“Strawberry”之後,整個fruitFlux正常完成。
還可以從數組 集合 Java Stream來作爲Flux的源。
List<String> list = Lists.newArrayList();
list.add("Apple");
list.add("Orange");
Flux<String> stringFlux = Flux.fromIterable(list);
例如,要創建一個每秒發佈一個值的Flux,你可以使用Flux上的靜態interval() 方法,如下所示:
Flux<Long> take = Flux.interval(Duration.ofSeconds(1)).take(5);
通過interval()方法創建的Flux會從0開始發佈值,並且後續的條目依次遞增。此外,因爲interval()方法沒有指定最大值,所以它可能會永遠運行。我們也可以使用take()方法將結果限制爲前5個條目
- 2 組合反應式類型
有時候,我們會需要操作兩種反應式類型,並以某種方式將它們合併在一起。或者,在其他情況下,我們可能需要將Flux拆分爲多種反應式類型
合併:
Flux<String> fruitFluxA = Flux.just("Apple","Orange");
Flux<String> fruitFluxB = Flux.just("Banana","watermelon");
fruitFluxA.mergeWith(fruitFluxB).subscribe(System.out::println);
com.ckj.superlearn.superlearn.base.ReactorStrategy
16:03:07.343 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
Apple
Orange
Banana
watermelon
Process finished with exit code 0
mergeWith()方法不能完美地保證源Flux之間的先後順序,所以我們可以考慮使用zip()方法
Flux<String> fruitFluxA = Flux.just("Apple","Orange").delayElements(Duration.ofMillis(10));
Flux<String> fruitFluxB = Flux.just("Banana","watermelon").delayElements(Duration.ofMillis(50));
Flux<String> allFlux = fruitFluxA.mergeWith(fruitFluxB);
allFlux.subscribe(x-> System.out.println("allFlux:"+x));
Flux<Tuple2<String, String>> zip = Flux.zip(fruitFluxA, fruitFluxB);
zip.subscribe(x-> System.out.println("zip:"+x));
Thread.sleep(1000);
控制檯:
/com.ckj.superlearn.superlearn.base.ReactorStrategy
16:49:44.543 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
allFlux:Apple
allFlux:Orange
allFlux:Banana
zip:[Apple,Banana]
allFlux:watermelon
zip:[Orange,watermelon]
Process finished with exit code 0
- 3 轉換和過濾反應式流
針對具有多個數據項的Flux,skip操作將創建一個新的Flux,它會首先跳過指定數量的數據項,然後從源Flux中發佈剩餘的數據項。下面的測試方法展示如何使用skip()方法:
Flux<String> fruitFluxA = Flux.just("Apple","Orange","Banana","watermelon").skip(2);
fruitFluxA.subscribe(x->{
System.out.println(x);
});
/com.ckj.superlearn.superlearn.base.ReactorStrategy
17:05:00.141 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
Banana
watermelon
Process finished with exit code 0
與之對應相反的是take()
Flux<String> fruitFluxA = Flux.just("Apple","Orange","Banana","watermelon").take(2);
fruitFluxA.subscribe(x->{
System.out.println(x);
});
com.ckj.superlearn.superlearn.base.ReactorStrategy
17:20:59.483 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
Apple
Orange
Process finished with exit code 0
filter()的過濾效果
Flux<String> fruitFluxA = Flux.just("Apple","Orange","Banana","watermelon").take(2);
fruitFluxA.filter(x->x.equals("Apple")).subscribe(x->{
System.out.println(x);
});
com.ckj.superlearn.superlearn.base.ReactorStrategy
17:24:03.242 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
Apple
Process finished with exit code 0
如何使用flatMap()方法和subscribeOn()方法
Flux<String> fruitFluxA = Flux.just("Apple", "Orange", "Banana", "watermelon", "Apple", "Orange", "Banana",
"watermelon", "Apple", "Orange", "Banana", "watermelon", "Apple", "Orange", "Banana", "watermelon");
fruitFluxA.flatMap(Mono::just).map(String::toUpperCase).subscribeOn(Schedulers.parallel());
使用flatMap()和subscribeOn()的好處是:我們可以在多個並行線程之間拆分工作,從而增加流的吞吐量。因爲工作是並行完成的,無法保證哪項工作首先完成,所以結果Flux中數據項的發佈順序是未知的
原創不易,如果覺得有點用的話,請毫不留情點個贊,轉發一下,這將是我持續輸出優質文章的最強動力。