最近Spring Frameworkを触っているのだけど、バリデーション時の不可思議な挙動で2時間くらいハマったのでメモ。Spring Boot固有の問題かもしれない(そこまでは調べてない)。
掲題の通り、一つのFormに複数のModelAttributeを割り当て、リクエスト値をアクションメソッドの@Validated @ModelAttributeな引数でバインドして受け取るとき、いずれかのModelAttributeでバリデーションエラーが発生するとなぜか400エラーとなってしまい、アクションメソッドの処理に入らずエラーページに飛ばされてしまう。
400エラー(Spring Boot使用時のため、Whitelabelエラーページに飛ばされる):
There was an unexpected error (type=Bad Request, status=400). Validation failed for object='model1'. Error count: 1
これを回避し、期待する動作(アクションメソッド内でBindingResult#hasErrors()を使用してバリデーションエラー時の処理を行う)をさせるには、ModelAttribute引数ごとに、個別のBindingResult引数を設定する。
つまり、下記のように、model1、model2それぞれの直後にBindingResult引数を配置する。
Controller:
@RequestMapping(value = "/hoge", method = RequestMethod.POST) public ModelAndView newStoryComplete( @Validated @ModelAttribute("model1") Model1 model1, BindingResult result1, @Validated @ModelAttribute("model2") Model2 model2, BindingResult result2 ) { if (result1.hasErrors() || result2.hasErrors()) { // エラー時処理
View (Thymeleaf):
<form action="#" th:action="@{/hoge}" method="post"> <div>Param1: <input type="text" th:field="${model1.param1}" /><span th:errors="${model1.param1}"></span></div> <div>Param2: <input type="text" th:field="${model2.param2}" /><span th:errors="${model2.param2}"></span></div> <button type="submit">Send</button> </form>
これで400エラーは発生せず、アクションメソッド内に入ってくれるようになる。
下記参考記事が大きなヒントになった。そろそろSpringのソースを追うしかないかな……とあきらめかけていたところだったので本当に救われた。
@ModelAttributeとBindingResultの順序を正しく設定しないとリクエストがマッピングされない件 – Springバッドノウハウ | Developers.IO
ちなみに、一つのFormに複数のModelAttributeを割り当てるのがいいことなのかは知らない。一般的なお作法では、Commandクラス(もしくはFormオブジェクト)と呼ぶPOJOを1フォームごとに作って1対1にするみたいなのだけど……。値の詰め直しとか、バリデーションの定義がCommandクラスごとに散らばる(重複する)可能性とか率直に面倒と思うが、そういうものなんですかね。そもそもJSPの場合、