何が問題?
Spring boot, MySQL, Thymeleafを基本に、RESTAPIを通して取ってくるDTOをWebFluxのMonoに格納して使いたかったが、あまり参考にできる資料がなく、例えば、webclientでとってきてMonoにするまでは書いてあっても、それをThymeleafにどう読ませるかまで書いてあるものがなかったため結構ハマりました。
(blockしてとる..という方法を書いてるとこもいくつかありましたが、ノンブロッキングで処理したいのにブロックしてしまうのは意味ないので)
WebFluxとは
from https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/web-reactive.html#spring-webflux
Springが提供するノンブロッキング処理を行うためのflameworkです。
誤解を恐れず簡単にいうと、処理できる(データが利用できる)時になるべく多くの処理を1つのスレッドで一気に行う。結果スレッド数も少なくなり必要なメモリも少なく済む、という認識です
結論
mapによってdataをとりだせる
@GetMapping("/hello")
public Mono<String> hello(Model model) {
Mono<DataDTO> dataMono = dataService.getData();
return dataMono.map(data -> {
model.addAttribute("data", data);
return "hello";
});
}
- return typeが"Mono<String>"になってるところが重要です
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello WebFlux</title>
</head>
<body>
<h1>Message:</h1>
<p th:text="${data}">Loading...</p>
</body>
</html>
flatMapをつかうこともできるみたいです(後で説明します)
更新
@GetMapping("/hello")
public Mono<String> hello(Model model) {
Mono<DataDTO> dataMono = dataService.getData();
return dataMono.doOnNext(data ->
model.addAttribute("data", data)).then(Mono.just("hello"));
}
doOnNextで確実にdataが格納されて、さらにthenを使ってviewの名前が先にreturnされてthymeleaf側でnullにならないようにする、という方法もありました。ListなどをもったFluxで使うと効果がありそうです
結論に至るまでに試したこと
1. そのままattributeとして追加
@GetMapping("/hello")
public String hello(Model model) {
// Add a Mono to the model
Mono<DataDTO> dataMono = dataService.getData();
model.addAttribute("dataMono", dataMono);
return "hello";
}
画面に "reactor.core.publisher.MonoPeekTerminal" と表示された
2. ReactiveDataDriverContextVariableを利用
@GetMapping("/hello")
public String hello(Model model) {
Mono<DataDTO> dataMono = dataService.getData();
model.addAttribute("dataMono", new ReactiveDataDriverContextVariable(dataMono));
return "hello";
}
画面に"org.thymeleaf.spring6.context.webflux.ReactiveDataDriverContextVariable@4d6029d"と表示された
3.Renderingを利用
@GetMapping("/hello")
public Mono<Rendering> hello() {
Mono<DataDTO> dataMono = dataService.getData();
return dataMono.map(data -> Rendering.view("hello")
.modelAttribute("data", data)
.build());
}
エラー発生 "Error resolving template , template might not exist or might not be accessible by any of the configured Template Resolvers"
この時点で念の為、Thymeleaf configを設定し直したり、templateの配置が間違っていないかは一応確認しました。
mavenのclean and rebuildも実行、念の為。
4. mapの代わりにMono.justを使用
@GetMapping("/hello")
public Mono<Rendering> hello() {
Mono<DataDTO> dataMono = dataService.getData();
return Mono.just(Rendering.view("hello")
.modelAttribute("data", data)
.build());
}
3と同じエラー
Logging
ログを出力するにはlog()をくっつけます
Mono<DataDTO> dataMono = dataService.getData();
dataMono.log({category}) //categoryのdefaultはinfoでslf4jがあればそれを利用します
map vs flatmap
map自体はblocking処理なので、そこも非同期にしたい場合はflatmapが使えます。
@GetMapping("/hello")
public Mono<String> hello(Model model) {
Mono<DataDTO> dataMono = dataService.getData();
return dataMono.flatMap(data -> {
model.addAttribute("data", data);
return Mono.just("hello");
});
}
- その場合はmap内のreturnはMono.just()でMonoにしてあげる必要があります
mapは one-to-one, flatmapはone-to-manyの処理に適してます。
Monoは基本emptyかsingle valueを扱い、特に今回の用途ではflatMap(非同期処理)はあまり重要ではなかったですが、
今回は触れていない、WebFluxのもう一人の主役、Fluxは複数の値を非同期で扱うために存在しているので、それと使うと能力をさらに発揮するのではと思います
まとめ
今回はデータの存在をreactiveに感知し扱うというノンブロッキング処理を行えるWebFluxを使いました。大量のデータをサーバーとやりとりするのに良さそうだと思い使い始めましたが、意外にあまり情報が落ちていなく(特にfrontendと関わるものに関して)少し焦りました。
今回はあまり深掘りしませんでしたが、doOnNextやdoOnSubscribe等のそれぞれのステージで処理することもできるようでそれぞれの目的にそった使い方もできそうです。
参考資料
Thymeleaf + WebFlux
https://medium.com/@azakhiyah/exploring-the-features-of-java-spring-web-flux-and-thymeleaf-3f39561700e1
flux vs mono
https://www.baeldung.com/java-reactor-flux-vs-mono
Extract Mono Content
https://www.baeldung.com/java-string-from-mono
map vs flatmap
https://www.baeldung.com/java-reactor-map-flatmap
Spring WebFlux 入門
https://www.flywheel.jp/topics/spring-webflux-intro/
ノンブロッキングI/O
https://news.mynavi.jp/techplus/article/techp5260/