Spring BootでHandlerメソッドのパラメータに任意のクラスを書き、引数にそのインスタンスを受け取る

#概要

HandlerメソッドのパラメータにLocaleやTimeZoneなどのクラスを書くと自動的にそのインスタンスが渡されますが(下記の例のように)、

```java:例
@Controller
public class MyController {

  @RequestMapping(value = {"", "/"}, method = RequestMethod.GET)
  public String index(Locale locale, TimeZone timezone) {

    //springがlocale,timezoneにインスタンスをセットしてくれる
    logger.info("locale:{}", locale);
    // locale:ja
    logger.info("timezone:{}", timezone);
    // timezone:sun.util.calendar.ZoneInfo[id="Asia/Tokyo",offset=32400000,dstSavings=0,useDaylight=false,transitions=10,lastRule=null]
  }

}
```

これと同じように任意のクラスをパラメータに書き、そのインスタンスを引数として受け取るにはHandlerMethodArgumentResolverインタフェースを実装したクラスを作成することで可能になります。

**環境**

* Windows10 Professional
* Java 1.8.0_101
* Spring Boot 1.4.1

**参考**

* [22. Web MVC framework / 22.3.3 Defining @RequestMapping handler methods] (http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-methods)
* [Spring From the Trenches: Creating a Custom HandlerMethodArgumentResolver] (https://www.petrikainulainen.net/programming/spring-framework/spring-from-the-trenches-creating-a-custom-handlermethodargumentresolver/)
* [Interface HandlerMethodArgumentResolver] (http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/method/support/HandlerMethodArgumentResolver.html)


## サンプルコードの説明

HandlerメソッドがMyCustomModelというクラスのインスタンスを引数に受け取る例です。

### パラメータにするクラス

Handlerメソッドのパラメータにするクラスは下記のようにしました。

```java
public class MyCustomModel {

    private String hoge;
    private String fuga;
    private String myHeader;
    private String myCookie;
    private Date mySession;

    // getter/setterは省略

}
```

### コントローラーのHandlerメソッド

引数を受け取るHandlerメソッドのパラメータの指定方法はいくつかありますが、この記事では下記の3種類について説明します。

* メソッドアノテーションで指定する
* パラメータアノテーションで指定する
* パラメータのクラスの型／名前で指定する

```java:MyController
@Controller
public class MyController {

    /*
     * メソッドのアノテーションで指定する
     */
    @MyCustomAnnotation
    @RequestMapping(value = "/piyo", method = RequestMethod.GET)
    public String piyo(MyCustomModel myCustom) {

        // myCustomには自動的にインスタンスがセットされている
        logger.info("{}", myCustom);

        return "my/piyo";
    }

    /*
     * パラメータのアノテーションで指定する
     */
    @RequestMapping(value = "/poyo", method = RequestMethod.GET)
    public String poyo(@MyCustomAnnotation MyCustomModel myCustom) {

        // myCustomには自動的にインスタンスがセットされている
        logger.info("{}", myCustom);

        return "my/poyo";
    }

    /*
     * パラメータの型／名前で指定する
     */
    @RequestMapping(value = "/puyo", method = RequestMethod.GET)
    public String puyo(MyCustomModel myCustom) {

        // myCustomには自動的にインスタンスがセットされている
        logger.info("{}", myCustom);

        return "my/puyo";
    }

}
```

### アノテーション

このサンプルコードで使用するマーカーアノテーションです。

```java:MyCustomAnnotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface MyCustomAnnotation {
}
```


### HandlerMethodArgumentResolverを実装する

このインターフェースを実装するクラスを用意します。
実装が必要なメソッドはsupportsParameterとresolveArgumentです。

```java:MyCustomHandlerMethodArgumentResolver
public class MyCustomHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public Object resolveArgument(MethodParameter parameter
                                , ModelAndViewContainer container
                                , NativeWebRequest request
                                , WebDataBinderFactory factory) throws Exception {

        // 実装する

    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {

        // 実装する

    }

}
```

#### supportsParameterメソッド

このメソッドはHandlerメソッドのパラメータの1つ1つに対して呼ばれます。（ただしSpringが自動的に値をセットするパラメータは除外されます。たとえば@PathVariableアノテーションが付いているパラメータやModel、UriComponentsBuilderなど）
このメソッドがtrueを返した場合に、次にresolveArgumentメソッドが実行されます。

##### メソッドアノテーションで指定

メソッドにMyCustomAnnotationアノテーションが付いている場合。
MyControllerの例で言うとHandlerメソッドpiyoのMyCustomModelパラメータにインスタンスが渡されます。

```java:MethodParameter#hasMethodAnnotation
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasMethodAnnotation(MyCustomAnnotation.class);
}
```

##### パラメータアノテーションで指定

パラメータにMyCustomAnnotationアノテーションが付いている場合。
MyControllerの例で言うとHandlerメソッドpoyoのMyCustomModelパラメータにインスタンスが渡されます。

```java:MethodParameter#hasParameterAnnotation
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(MyCustomAnnotation.class);
}
```

##### パラメータの型／名前で指定

アノテーションを使わなくても、パラメータの型又は名前で指定することができます。
MyControllerの例で言うとすべてのHandlerメソッドのMyCustomModelパラメータにインスタンスが渡されます。

パラメータの型がMyCustomModelの場合。

```java:MethodParameter#getParameterType
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.getParameterType().equals(MyCustomModel.class);
}
```

パラメータの名前が"myCustom"の場合。
ただし、そのパラメータの型がMyCustomModelではない場合は例外がスローされます。

```java:MethodParameter#getParameterName
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.getParameterName().equals("myCustom");
}
```

#### resolveArgumentメソッド

supportsParameterメソッドがtrueを返した場合に実行され、このメソッドが返すオブジェクトが引数として使用されます。

```java:resolveArgument
@Override
public Object resolveArgument(MethodParameter parameter
                            , ModelAndViewContainer container
                            , NativeWebRequest request
                            , WebDataBinderFactory factory) throws Exception {

    MyCustomModel myCustom = new MyCustomModel();

    String hoge = request.getParameter("hoge");
    String fuga = request.getParameter("fuga");
    myCustom.setHoge(hoge);
    myCustom.setFuga(fuga);

    String myHeader = request.getHeader("User-Agent");
    myCustom.setMyHeader(myHeader);

    return myCustom;
}
```

##### NativeWebRequest#getNativeRequest

HttpServletRequestクラスのインスタンスを取得することができます。

```java
HttpServletRequest httpServletRequest = request.getNativeRequest(HttpServletRequest.class);

// Cookieを扱うことができます。
Cookie myCookie = WebUtils.getCookie(httpServletRequest, "myCookie");
if (myCookie != null) {
    myCustom.setMyCookie(myCookie.getValue());
}

// Sessionを扱うことができます。
HttpSession session = httpServletRequest.getSession(false);
if (session != null && session.getAttribute("mySession") != null) {
    Date mySession = (Date) session.getAttribute("mySession");
    myCustom.setMySession(mySession);
}
```

##### ModelAndViewContainer#addAttribute

Modelにインスタンスをセットして直接ビュー(例えばthymeleaf)へ渡すことができます。

```java
MyCustomModel myCustom = new MyCustomModel();

// myCustomにデータを詰めるコード

container.addAttribute("myCustom", myCustom);
```

### WebMvcConfigurerAdapterを継承したクラスを用意する

作成したMyCustomHandlerMethodArgumentResolverを有効にするには、addArgumentResolversメソッドで追加します。


```java
import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.example.web.aws.resolver.MyCustomHandlerMethodArgumentResolver;

@Configuration
public class MyWebMvcConfigurer extends WebMvcConfigurerAdapter {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new MyCustomHandlerMethodArgumentResolver());
    }

}
```