Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Spring MVCでConversion失敗をまとめて処理したいときは@ModelAttributeを使うしかないのかな?

Spring MVCでこういうコントローラメソッドがあるとする。

    @RequestMapping(path = "hoge")
    public String index(@RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date start, @RequestParam @DateTimeFormat(pattern="yyyyMMdd") Date end) {
        return "hoge";
    }

start、endという日付はブラウザでテキストボックスに文字列として日付を入力するみたいな想定で、日付として無効な文字列を入力したときにエラーメッセージを出すような、よくある感じにしたい。

だけど、実際に無効な文字列を入れてsubmitしてみると、ステータスコード400になる。スタックトレースも出る。

Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type @org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.util.Date for value 'hoge'; nested exception is java.lang.IllegalArgumentException: Invalid format: "hoge"
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:192) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:173) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:108) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:64) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:47) [spring-beans-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.validation.DataBinder.convertIfNecessary(DataBinder.java:688) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:107) [spring-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ... 49 common frames omitted
Caused by: java.lang.IllegalArgumentException: Invalid format: "hoge"
    at org.joda.time.format.DateTimeFormatter.parseDateTime(DateTimeFormatter.java:866) [joda-time-2.1.jar:2.1]
    at org.springframework.format.datetime.joda.DateTimeParser.parse(DateTimeParser.java:49) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.datetime.joda.DateTimeParser.parse(DateTimeParser.java:33) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.support.FormattingConversionService$ParserConverter.convert(FormattingConversionService.java:194) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.format.support.FormattingConversionService$AnnotationParserConverter.convert(FormattingConversionService.java:311) [spring-context-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:35) [spring-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ... 56 common frames omitted

org.springframework.web.method.support.InvocableHandlerMethodの実装はこうなってる。

   private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {

        MethodParameter[] parameters = getMethodParameters();
        Object[] args = new Object[parameters.length];
        for (int i = 0; i < parameters.length; i++) {
            MethodParameter parameter = parameters[i];
            parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
            GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
            args[i] = resolveProvidedArgument(parameter, providedArgs);
            if (args[i] != null) {
                continue;
            }
            if (this.argumentResolvers.supportsParameter(parameter)) {
                try {
                    args[i] = this.argumentResolvers.resolveArgument(
                            parameter, mavContainer, request, this.dataBinderFactory);
                    continue;
                }
                catch (Exception ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
                    }
                    throw ex;
                }
            }
            if (args[i] == null) {
                String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
                throw new IllegalStateException(msg);
            }
        }
        return args;
    }

ループでまわしていて、今回のようなConversionFailedExceptionも含めて例外が出た時点でキャッチしてリスローしているので、たとえばコントローラメソッドのすべての引数を変換してみて失敗したフィールドにはすべてエラーメッセージを出したい、という要件は実現できない。

メソッドの引数に@RequestParamをつけるパターンでは無理そうなので、別の方法である@ModelAttributeとBindingResultを使うしかなさそう。

    @RequestMapping(path = "fuga")
    @ResponseBody
    public String fuga(@ModelAttribute Fuga fuga, BindingResult result) {
        return "fuga";
    }
import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

public class Fuga {

   @DateTimeFormat(pattern="yyyyMMdd")
    private Date start;
    @DateTimeFormat(pattern="yyyyMMdd")
    private Date end;

    // setters and getters...
}

こうすると、BindingResult#hasErrors()でConversion失敗があったかはわかるし、BindingResult#getAllErrors()で各失敗の詳細情報も取得できる。