業務で Spring WebFlux で API 開発を行いました。その際に事前に知っておくべきことや、解決に時間がかかったことなど Tips として備忘録的に残しておきます。
Spring WebFlux とは?
Spring MVC とは異なる、もう一つの Spring の Web フレームワーク。
その特徴に関連したキーワードを挙げると次の通り
- ノンブロッキング
- 関数型
- リアクティブ
- Netty
- 非同期につよい
- イベントループモデル
このあたりの記事を読むと理解が進むと思います。本記事ではいろいろ割愛します。
- Spring WebFlux リアクティブスタック - リファレンス - 公式ドキュメントの冒頭を読むのおすすめです
- 【連載】マイクロサービス時代に活きるフレームワーク Spring WebFlux 入門 [1] Spring WebFlux とは|開発ソフトウェア| IT 製品の事例・解説記事
- 業務で使いたい WebFlux による Reactive プログラミング / Introduction to Reactive Programming using Spring WebFlux - Speaker Deck
また、WebFlux 実装で重要となる Mono/Flux については次の記事がおすすめです。
Tips
block は使わない
次のように書けば、Mono/Flux でラッピングしない通常オブジェクトとして扱える。
Result result = repository.get() // returns Mono<Result>
.block(); // returns Result
しかし、こうするとブロッキング IO となってしまい WebFlux の利点が半減、通常のスレッドを多様するアプリケーションとなってしまう。In->Out 一貫して Mono/Flux で通信すること。
Mono/Flux の呼び出しの注意
2021/10/13追記: 以下の説明は認識不足で、誤解を招くかもなので訂正。
いわゆるHot vs Coldの話で、Cold Publisherの場合は2回呼び出されてしまう、ということになります。
Hot vs Coldの話は解説記事が多くあるので、それを一読しておくことをオススメします。
- RxのHotとColdについて - Qiita
- Reactor 3 Reference Guide - 9.2. Hot Versus Cold
- Flight of the Flux 1 - Assembly vs Subscription
- Reactor Hot Publisher vs Cold Publisher | Vinsguru
なんとなく Mono/Flux に置き換えるだけって感じでこんなコードを書いていた。
Mono<Context> context = contextRepository(contextKey);
Mono<Item>item = itemRepository.get(context, itemKey);
Mono.zip(context, item)
.subscribe();
一見問題なさそうだが、これだとcontextRepository.get()が 2 回呼び出されてしまい、無駄な IO が発生してしまう。
Mono は subscribe されるたび、その開始時点の publisher からすべて実行されるためである。
次のように書けば OK。
Mono<Context> context = contextRepository(contextKey);
Mono.zipWhen(context, itemRepository.get(context, itemKey)
.subscribe();
API ルーティング方法
Spring WebFlux では、MVC と同様@GetMapping
といったアノテーションでルーティングする方法のほかに、RouterFunctions を用いてルーティング可能。
Introduction to the Functional Web Framework in Spring 5 | Baeldung
より関数型を活かした感じで書けるのでおすすめ。
fire-and-forget パターン
「リクエスト投げっぱなしでレスポンスは待つ必要ない」ってパターンは次のように書くとよい。
repository.get() // returns Mono<Result>
.doOnNext(result -> serv ice.doAsync(result).subscribe()) // serviceに投げっぱなし
.subscribe();
次のページを参考にしました。
- spring - Fire and forget with reactor - Stack Overflow
- java - How make "fire and forget" request sending in spring webflux webclient? - Stack Overflow
キャッシュ
Spring Cache の@Cachable
みたいなのを使うにはどうすれば?ってことで調べると次の記事がヒットした。
Spring Webflux and @Cacheable - proper way of caching result of Mono / Flux type - Stack Overflow
"Hack way"の通り Reactor の.cache()
とアノテーションでも実現可能であるが、
- キャッシュ入れる or 入れないといった複雑な制御が難しい
- ブラックボックス感ある
- テストしにくい といった理由で、後者の Reactor Addons を使うパターンがおすすめ。
reactor/reactor-addons: Official modules for the Reactor project
var id = "785AC2D5-5CBE-4170-90A0-F9E327B09B5C";
CacheMono.lookup(key -> Mono.justOrEmpty(cacheManager.getCache("CACHE_NAME").get(key, Result.class)))
.map(Signal::next), id)
.onCacheMissResume(() -> repository.get(id)) // キャッシュヒットしない場合は取りに行く
.andWriteWith((key, signal) -> Mono.fromRunnable()
-> Optional.ofNullable(signal.get())
.ifPresent(result -> cacheManager.getCache("CACHE_NAME").put(key, result)) // 値がemptyやerrorでない場合、結果をキャッシュに保存
);
WebClient
WebFlux の標準の HTTP クライアントとして WebClient が用意されている。
WebClient webClient = WebClient.builder().builder();
webClient.post()
.uri("https://example.com/do")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(new Request()))
.retrieve()
.bodyToMono(Response.class);
Spring MVC などでも、block()
すれば使える。
(従来の RestTemplate はメンテモードに入って今後の機能拡張は WebClient のみって話をどこかで聞いた気がするが、出典見つからず不明…)
2021/07/05追記: RestTemplateのドキュメントに記載がありました。
NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.
テスト
Mono で返却される値をテストする場合、block()
を使って頑張るのもありだがもっと便利なのが reactor-test で用意されている。
StepVerifier (reactor-test 3.4.7)
基本的に expectNextMatches で assertion
var actual = repository.get(key); // returns Mono<Result>
StepVerifier.create(actual)
.expectNextMatches(result -> result.isOk())
.verifyComplete();
モックの verify をしたい場合、actual を complete させないといけない。
var actual = service.doAsync();
StepVerifier.create(actual)
.expectNextCount(1) // # of results are only 1
.verifyComplete();
verify(repository, times(1)).get(eq("key"));
logger
ロガーとして従来どおり SLF4J + Logback が使えるが、ブロッキング IO なので非同期として設定しておいたほうがよい。ch.qos.Logback.classic.AsyncAppender
を使うのがよい。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="ASYNC_STDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_STDOUT"/>
</root>
</configuration>
次のページを参考にした。
maven 3 - Is logging a non-blocking operation in Spring Webflux? - Stack Overflow
まとめ
Spring WebFlux は日本語情報も少なくつまずきまくりますが、やはりパフォーマンス面で優位性は大きく、Mono/Flux も慣れてくると楽しいのでおすすめです。