Java 響應式編程模式
響應式編程
在 Spring Boot 中,響應式編程模式已經被引入,它可以讓我們在 Spring Boot 的應用中使用響應式編程模式。
響應式編程帶來的性能上和內存使用上的優化。
詳見:
困難
但是不同於 async/await
模式,響應式編程也給編碼帶來了一些困難,主要如下:
- 對
null
處理不友好,甚至是災難性的。 - 對異常處理不友好。
這個問題在一定程度上可以使用RuntimeException
來規避。 - 一個代碼塊只能最多調用一個響應式 API。
響應式編程的規則
- 控制層,返回響應式對象,大多數情況下使用
Mono<T>
。 - 服務層,使用
@Transactional
的 API 必須返回響應式對象。 - 數據訪問層(R2DBC)返回響應式對象:
Mono<T>
,Flux<T>
- 使用響應式方法的 API 儘量返回響應式對象。
- 不要使用任何
block()
,blockFirst()
,share().block()
等 API,會引起嚴重的性能問題。 - 避免使用
subscribe()
API,可能會因此性能問題。
在我的經驗中,只有在重載傳統接口的情況下才使用subscribe()
API。 - 建議: 避免定義返回
Mono<Void>
的方法。
這種返回不能調用隨後的映射方法map()
、flatMap()
、transform()
等方法。
一個例外是,這個方法在控制層的最後被調用。 - Mono.empty() 不能調用隨後的映射方法
map()
、flatMap()
、transform()
等。 - Flux.empty() 可以調用隨後的映射方法
colllectList()
等方法。 - 響應式 API 不能返回
null
或者Mono.just(null)
或者其等價方式。
會引起下面的錯誤:Caused by: java.lang.NullPointerException: The mapper returned a null value.
響應式編程模式
響應式編程是一種流編程,我把編程模式分爲: 啓動模式、映射模式、返回模式、異常模式。
異常模式
響應式編程對於異常處理,建議使用下面的方法:
- 拋出
RuntimeException
。 - 對於異常,返回
Mono.error(t)
方法。 doOnXXX()
方法。
啓動模式
- 常用的啓動模式
Mono.just(data);
Mono.fromXXX(xxx);
- 冷響應式
Mono.defer(() -> supplier);
冷響應式是指,在啓動時,不會立即執行,而是在被訂閱時才執行。
下面 IllegalArgumentException 會在 subscribe 後纔會被調用。
// Sample code
private Mono<Integer> monoAdd(Integer a, Integer b) {
return Mono.defer(() -> {
if (a == null) {
throw new IllegalArgumentException("a is null");
}
if (b == null) {
throw new IllegalArgumentException("b is null");
}
return Mono.just(a + b);
});
}
映射模式
這裏討論的映射模式,大都是關於多個響應式 API 之間的協作。
平行模式(flat pattern)
主要是用 flatMap()
方法。代碼成 flatMap().flatMap().flatMap()
形狀。
用於後面的 API 只使用前面 API 輸出結果的情況。
public static Mono<Integer> monoFlat(Integer a) {
return Mono.defer(() -> {
if (a == null) {
throw new IllegalArgumentException("a is null");
}
return Mono.just(a);
}).flatMap(data -> Mono.just(data * 2))
.flatMap(data -> Mono.just(data + 100));
}
monoFlat(1).log().subscribe();
/*
00:15:17.005 [main] DEBUG reactor.util.Loggers - Using Slf4j logging framework
00:15:17.164 [main] INFO reactor.Mono.FlatMap.1 - | onSubscribe([Fuseable] MonoFlatMap.FlatMapMain)
00:15:17.168 [main] INFO reactor.Mono.FlatMap.1 - | request(unbounded)
00:15:17.171 [main] INFO reactor.Mono.FlatMap.1 - | onNext(102)
00:15:17.173 [main] INFO reactor.Mono.FlatMap.1 - | onComplete()
*/
嵌套模式(nested pattern)
對於後面的 API 需要使用多個前面 API 輸出結果的情況,可以使用嵌套模式。
在嵌套模式中,後面的 API 可以直接使用前面 API 的結果。
public static Mono<Integer> monoNested(Integer a, Integer b) {
// return a * 100 + b * 100
return Mono.defer(() -> {
if (a == null) {
throw new IllegalArgumentException("a is null");
}
return Mono.just(a * 100).flatMap(o1 -> {
if (b == null) {
throw new IllegalArgumentException("a is null");
}
return Mono.just(b * 100).map(
// 在這裏可以同時使用 o1 和 o2
o2 -> o1 + o2);
});
});
}
monoNested(1, 2).log().subscribe();
/*
00:22:43.816 [main] INFO reactor.Mono.Defer.2 - onSubscribe([Fuseable] FluxMapFuseable.MapFuseableSubscriber)
00:22:43.817 [main] INFO reactor.Mono.Defer.2 - request(unbounded)
00:22:43.817 [main] INFO reactor.Mono.Defer.2 - onNext(300)
00:22:43.818 [main] INFO reactor.Mono.Defer.2 - onComplete()
*/
拉鍊模式(zip pattern)
對於後面的 API 需要使用多個前面 API 輸出結果的情況,可以使用拉鍊模式。
在拉鍊模式中,後面的 API 可以通過參數獲取前面 API 的結果。
public static Mono<Integer> monoZip(Integer a, Integer b) {
// return a * 100 + b * 100
return Mono.zip(
Mono.defer(() -> {
if (a == null) {
throw new IllegalArgumentException("a is null");
}
return Mono.just(a * 100);
}),
Mono.defer(() -> {
if (b == null) {
throw new IllegalArgumentException("b is null");
}
return Mono.just(b * 100);
}), (o1, o2) -> o1 + o2);
}
monoZip(1, 2).log().subscribe();
/*
00:32:22.326 [main] INFO reactor.Mono.Zip.3 - onSubscribe([Fuseable] MonoZip.ZipCoordinator)
00:32:22.326 [main] INFO reactor.Mono.Zip.3 - request(unbounded)
00:32:22.327 [main] INFO reactor.Mono.Zip.3 - onNext(300)
00:32:22.328 [main] INFO reactor.Mono.Zip.3 - onComplete()
*/
原子模式(atomic pattern)
拉鍊模式和嵌套模式都不能處理 null 值,原子模式可以。
注意下面示例中的 return Mono.just(0)
可以確保不會忽略 null 值的情況。
public static Mono<Integer> monoAtomic(Integer a, Integer b) {
AtomicReference<Integer> a100Ref = new AtomicReference<>(0);
AtomicReference<Integer> b100Ref = new AtomicReference<>(0);
// return a * 100 + b * 100
return Mono.defer(() -> {
if (a == null) {
a100Ref.set(null);
} else {
a100Ref.set(a * 100);
}
return Mono.just(0);
}).flatMap(o -> {
if (b == null) {
b100Ref.set(null);
} else {
b100Ref.set(b * 100);
}
return Mono.just(0);
}).map(o -> {
if (a100Ref.get() == null || b100Ref.get() == null) {
return 0;
}
return a100Ref.get() + b100Ref.get();
});
}
monoAtomic(1, 2).log().subscribe();
/*
11:03:46.162 [main] INFO reactor.Mono.Map.4 - onSubscribe(FluxMap.MapSubscriber)
11:03:46.163 [main] INFO reactor.Mono.Map.4 - request(unbounded)
11:03:46.163 [main] INFO reactor.Mono.Map.4 - onNext(0)
11:03:46.164 [main] INFO reactor.Mono.Map.4 - onComplete()
*/
null 對象模式(null pattern)
我們還可以使用默認值來處理 null 值的情況。
在處理 null 值時,一個常見的需求是:
在一個 lambda 閉包中:
- 可以知道這個值是 null 還是非 null。
- 可以獲取這個值。
- 可以調用並返回一個新的響應式對象(發佈者)
一個技巧是使用 .defaultIfEmpty()
方法來處理 null 值。
這個技巧對於數值或者 String 類型的值可能有效的,但是對於類實例就不好用了。
在這種情況下,可以考慮定義一個接口。
public interface Nullable {
boolean isNone();
}
package demo.reactive;
public class Employee implements Nullable {
private static final Employee none = new Employee(true);
public static Employee none() {
return none;
}
private Employee(boolean isNone) {
this.isNone = isNone;
}
private Employee() {
}
private int id;
private String name;
private boolean isNone;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean isNone() {
return isNone;
}
public void setNull(boolean isNone) {
this.isNone = isNone;
}
}
public static Mono<Employee> monoNullable() {
// return nullable object
return Mono.defer(() -> {
return Mono.<Employee>empty();
}).defaultIfEmpty(Employee.none());
}
monoNullable().map(o -> {
logger.info(MessageFormat.format("map.isNone: {0}", o.isNone()));
return o;
}).doOnSuccess(o -> {
logger.info(MessageFormat.format("doOnSuccess: {0}", o));
}).log().subscribe();
/*
18:28:06.789 [main] INFO reactor.Mono.PeekTerminal.1 - | onSubscribe([Fuseable] MonoPeekTerminal.MonoTerminalPeekSubscriber)
18:28:06.794 [main] INFO reactor.Mono.PeekTerminal.1 - | request(unbounded)
18:28:06.796 [main] INFO demo.ReactiveDemo - map.isNone: true
18:28:06.796 [main] INFO demo.ReactiveDemo - doOnSuccess: demo.reactive.Employee@120d6fe6
18:28:06.797 [main] INFO reactor.Mono.PeekTerminal.1 - | onNext(demo.reactive.Employee@120d6fe6)
18:28:06.799 [main] INFO reactor.Mono.PeekTerminal.1 - | onComplete()
*/
返回模式
下面是常見的返回模式。
Mono.empty();
Mono.then();
Mono.then(mono);
Mono.thenReturn(data);