メモです。
背景
@RestController
で REST な API をつくって、以下みたいなリクエストを受け取るとする。
GET /foo?hoge[]=1&hoge[]=2
jQuery の $.param とかで、$.param({hoge: [1, 2]})
すると上記のパラメータ文字列になるので良くある形式なのだと思う。
ところが、SpringMVC で上記を @ModelAttribute
でうけ取ろうとすると NumberFormatException
になって InternalServerError
になる。
@RequestParam
なんじゃないか?という話もあるけど、@Validated
によるバリデーションをかけつつ、DTO にマップしたいので @ModelAttribute
を使う。
DTO への値設定時に、パラメータ名をプロパティ名とみなして設定するのだけど、[]
は配列アクセスとみなして、[
と ]
の間にインデクスが入っている前提で処理される。実際にはなにも入っていないので、空文字を整数に変換しようとして NumberFormatException
になる。
あと、対象プロパティが Set<...>
の場合も、InvalidPropertyException
が発生したりする。
@ExceptionHandler
で NumberFormatException
を拾えるが、発生状況がクエリパラメータの処理時に限定できないので、本当に ISE の場合も気づけなくなってしまう。
思うに、@ModelAttribute
による値のバインドは、REST API で使われることは想定していない。普通のフォームからの POST とかみたいに、呼び出し元もおなじシステム(≒開発者が入力値をコントロール可能)であることを前提としているのかと思う。
ただ、REST API のときに、適当なクエリパラメータで ISE おこせるとかイケていないので、とりあえず何とかする。
方法
SpringMVC ではコントローラメソッドの引数を解決するための部品として HandlerMethodArgumentResolver
というのがある。@ModelAttribute
だったり BindingResult
だったり、どれもこの仕組でコントローラメソッドの引数に提供されている。
この HandlerArgumentResolver
に自作のものを追加し、パラメータ処理時の NumberFormatException に限り、違う例外でラップして再送出する。
@Configuration
public class XxxxConfiguration {
@Bean
public WebMvcConfigurer getConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolver) {
argumentResolver.add(new HandlerMethodArgumentResolver() {
private final HandlerMethodArgumentResolver underlying = new ServletModelAttributeMethodProcessor(false);
@Override
public boolean supportsParameter(MethodParameter parameter) {
// Default の ArgumentResolver の方が優先順位が高いので、ModelAttribute.class を目印にすると、デフォルトの方で処理されてしまう
return parameter.hasParameterAnnotation(なんか自作のアノテーション.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, .....) throws Exception {
try {
return underlying.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
} catch (NumberFormatException ex) {
throw new なんか自作の例外(ex);
}
}
});
}
};
}
}
たぶんこんなんじで、なんとかなる。