Spring WebFluxでWebAPI開発するためのTipsいろいろ

業務で Spring WebFlux で API 開発を行いました。その際に事前に知っておくべきことや、解決に時間がかかったことなど Tips として備忘録的に残しておきます。

Spring WebFlux とは?

Spring MVC とは異なる、もう一つの Spring の Web フレームワーク。
その特徴に関連したキーワードを挙げると次の通り

  • ノンブロッキング
  • 関数型
  • リアクティブ
  • Netty
  • 非同期につよい
  • イベントループモデル

このあたりの記事を読むと理解が進むと思います。本記事ではいろいろ割愛します。

また、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の話は解説記事が多くあるので、それを一読しておくことをオススメします。

なんとなく 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 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 も慣れてくると楽しいのでおすすめです。