Spring MVCでHandlerMethodArgumentResolverを利用してバインドしたカスタムオブジェクトに入力チェックを適用する

>Spring MVC覚書
> Spring MVC 4.3.11.RELEASE

Spring MVCのControllerで、ハンドラメソッド引数に独自オブジェクトをバインドしたい場合は、以下のいずれかの方法を採ると思いますが、

* フォームオブジェクトを利用する
* `HandlerMethodArgumentResolver`実装クラスを利用する

`HandlerMethodArgumentResolver`実装クラスを利用する場合、バインド先の独自オブジェクトはフォームオブジェクトのように自動的にSpring MVCの`Model`に登録されず、入力チェックもされません。

`Model`に登録されないのでは、何度もバインドするには向かないし、何より使用感が変わってきます。
入力チェックされないのでは、システム情報のような安全なデータしかバインドできません。

ということで、実現したい要件によってはこれらを実装しなければならなくなり、それなりにSpring MVCを理解していないと正しく実装できず、後々トラブルになったりしそうです。

## `ModelAttributeMethodProcessor`クラスを利用する

`ModelAttributeMethodProcessor`クラスは、`HandlerMethodArgumentResolver`実装クラスで`@ModelAttribute`を付与した引数へのバインドを処理します。

バインドしたい独自オブジェクトが上記のような標準的な使い方を想定しているなら、このクラスをオーバーライドすると、簡単に実現することができます。

実装例を見ていきますが、ここではリクエストヘッダをまとめてバインドする独自オブジェクトを実装し、入力チェックを適用します。

### 独自オブジェクト

```java
@Getter
@Setter
public class RequestHeader {
    @SupportedBrowser("FireFox") // 入力チェックアノテーション
    private String userAgent;
    private String accept;
    private String acceptLanguage;
    private String acceptEncoding;
}
```

独自オブジェクトには、リクエストヘッダをバインドするプロパティを定義し、ユーザエージェントにはFireFoxのみ許可するようにしてみます。（入力チェックはテキトーですw）

### `ModelAttributeMethodProcessor`拡張クラス

```java
public class RequestHeaderModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {

    // コンストラクタ
    // ここでは、`@ModelAttribute`アノテーションが不要（true）なことを親コンストラクタに渡しています
    public RequestHeaderModelAttributeMethodProcessor() {
        super(true);
    }

    // サポートする型
    // オーバーライドして、さきほど作成した独自オブジェクトを対象にしています
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return RequestHeader.class.isAssignableFrom(parameter.getParameterType());
    }

    // リクエストパラメータをバインドする
    // オーバーライドして、任意のバインドロジックを定義します
    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {

        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.addPropertyValue("userAgent", request.getHeader("user-agent"));
        pvs.addPropertyValue("accept", request.getHeader("accept"));
        pvs.addPropertyValue("acceptLanguage", request.getHeader("accept-language"));
        pvs.addPropertyValue("acceptEncoding", request.getHeader("accept-encoding"));
        binder.bind(pvs);
    }

}
```

`bindRequestParameters`メソッドでは、Controllerの`@InitBinder`メソッドと同じ方法でバインドする値とバインド先プロパティを指定します。
バインド先のオブジェクト（`RequestHeader`オブジェクト）は親クラスで生成してくれるので、ここでは各プロパティへのバインド方法を指定するだけでOKです。

なお、ここでは扱いませんが、ArgumentResolverはBeanとしてSpring MVCに登録するので、任意の`@Service`などをインジェクションできます。複雑なロジックやDB接続はそちらに実装すると良いですね。

以下のように、作成したArgumentResolverを適用します。

* Java Config

```java
@Configuration
public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new RequestHeaderModelAttributeMethodProcessor());
    }
}
```

* XML

```xml
<mvc:annotation-driven>
    <mvc:argument-resolvers>
        <bean class="x.x.RequestHeaderModelAttributeMethodProcessor"/>
    </mvc:argument-resolvers>
</mvc:annotation-driven>
```

### Controller

```java
@Controller
public class SampleController {

    // ハンドラメソッド
    // フォームオブジェクトと同じ方法でバインドできます
    @GetMapping("sample")
    public String handle(@Validated RequestHeader header, BindingResult result, Model model) {

        // 入力チェック
        // フォームオブジェクトと同じ方法でチェック結果を取得できます
        if (result.hasErrors()) {
            return "sample/failure";
        }
        
        // Modelからも取得できます
        RequestHeader headerFormModel = model.asMap().get("requestHeader");

        return "sample/success";
    }
}
```

Controllerでは、フォームオブジェクトと同じように引数として定義するだけです。
`Model`に登録されるので、当然`@SessionAttribute`などにも対応できるはず。

## まとめ

`ModelAttributeMethodProcessor`クラスを利用することで、入力チェックなどを自分で実装することなく、バインドだけに集中して簡単に実装することができました。

せっかくSpring MVCが提供してくれる仕組みがあるので、なるべく標準的に作りたいなーと思ったのですが、意外と情報がなくてSpringのソースコードを見ながら作りましたorz...

誰かの参考になると嬉しいです！