WebFluxではWebMVCのようにメソッド引数でBindingResult
でvalidation結果を取得できないので、その書き換え方について。
WebMVCではcontrollerでvalidation結果を取得するにはメソッドの引数にBindingResult
を追加する。例えば以下のようなコードになる。以下では、もしvalidationエラーが発生したら、一旦BindException
に変換してcontroller-adviceでエラーハンドリングを行う、というのを想定している。これを書き換えていく。
import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SpringMvcConrtoller { @PostMapping("/item1") public Item item(@Validated Item item, BindingResult result) throws BindException { if (result.hasErrors()) { throw new BindException(result); } return new Item(); } }
ソースコード
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.4.0' id 'io.spring.dependency-management' version '1.1.6' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webflux' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.projectreactor:reactor-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
書き換え例
引数にBindingResult
があると実行時エラーになるので削除する。また、引数オブジェクトをMono
とかでラップし、そのMono
を使用して処理を記述する。以下はmap
で単に空オブジェクトを返すだけだが、こういう感じにロジックを記述する。もしvalidationが失敗するとWebExchangeBindException
になるのでこれをonErrorMap
などエラーハンドラで処理する。ここでは最初のSpringMVCの例に合わせて単にBindException
に詰め替えしている。*1
import org.springframework.validation.BindException; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.support.WebExchangeBindException; import reactor.core.publisher.Mono; @RestController public class WebFluxController { @PostMapping("item001") public Mono<Item> item(@Validated Mono<Item> request) { return request .map(item -> new Item()) .onErrorMap(WebExchangeBindException.class, e -> { return new BindException(e.getBindingResult()); }); } }
エラーハンドリングはAbstractErrorWebExceptionHandler
の継承クラスを用意し、例外ごとの処理を記述する。こういうのはJDK 21のswitchの使用例になるだろうか。
import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.stereotype.Component; import org.springframework.validation.BindException; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; @Component public class CustomErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { public CustomErrorWebExceptionHandler(ErrorAttributes errorAttributes, ApplicationContext applicationContext, ServerCodecConfigurer serverCodecConfigurer) { super(errorAttributes, new Resources(), applicationContext); super.setMessageWriters(serverCodecConfigurer.getWriters()); super.setMessageReaders(serverCodecConfigurer.getReaders()); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(RequestPredicates.all(), r -> { ErrorAttributeOptions eao = ErrorAttributeOptions.defaults(); Throwable error = errorAttributes.getError(r); return switch (error) { case BindException e -> ServerResponse.status(400).bodyValue("validation error"); default -> ServerResponse.status(500).bodyValue("default"); } }); } }
試行錯誤
以下はあーだこーだと調べたり試したりした際の作業メモ。
まずは単に戻り値型をMono
に変えるだけで実行してみる。
import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController public class WebFluxController { @PostMapping("item001") public Mono<Item> item(@RequestBody @Validated Item item, BindingResult result) throws BindException { if (result.hasErrors()) { throw new BindException(result); } return Mono.just(new Item()); }
以下のエラーになる。
java.lang.IllegalStateException: An Errors/BindingResult argument is expected immediately after the @ModelAttribute argument to which it applies. For @RequestBody and @RequestPart arguments, please declare them with a reactive type wrapper and use its onError operators to handle WebExchangeBindException: public reactor.core.publisher.Mono org.example.app.WebFluxController.item(org.example.app.Item,org.springframework.validation.BindingResult) throws org.springframework.validation.BindException at org.springframework.util.Assert.state(Assert.java:101) ~[spring-core-6.2.0.jar:6.2.0] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ? HTTP POST "/item001" [ExceptionHandlingWebHandler] Original Stack Trace:
エラーメッセージに従い、引数オブジェクトをMono
でラップする。
@PostMapping("item001") public Mono<Item> item(@RequestBody @Validated Mono<Item> item, BindingResult result) throws BindException { ...
以下のエラーになる。
java.lang.IllegalStateException: An @ModelAttribute and an Errors/BindingResult argument cannot both be declared with an async type wrapper. Either declare the @ModelAttribute without an async wrapper type or handle a WebExchangeBindException error signal through the async type. at org.springframework.util.Assert.state(Assert.java:79) ~[spring-core-6.2.0.jar:6.2.0] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ? HTTP POST "/item001" [ExceptionHandlingWebHandler] Original Stack Trace:
エラーメッセージに従い、引数からBindingResult
を削除してWebExchangeBindException
のエラーハンドリングに切り替える。これは上述の通り。
もし以下のようにWebExchangeBindException
をそのまま返すだけにしたらどうなるか? AbstractErrorWebExceptionHandler
に行かない。
@PostMapping("item001") public Mono<Item> item(@Validated Mono<Item> request) { return request .map(item -> new Item()) .onErrorMap(WebExchangeBindException.class, e -> { return e; }); }
参考文献
- https://www.baeldung.com/spring-webflux-errors
- https://www.vinsguru.com/spring-webflux-validation/ -
@ControllerAdvice
も使えるようだけど、WebExceptionHandler
との使い分けがイマイチ分かってない。2か所に分かれるのもなんか微妙な気がする。
*1:WebFluxほぼ経験ゼロなのでreactive由来の用語の使い方が変だと思うがスルーして欲しい