Spring MVC のハンドラメソッドの引数について

Spring MVC のハンドラメソッド(コントローラークラスのメソッド)の引数について調べたメモ

バージョンは `6.2.3` 時点の内容です。

結論からいうと `HandlerMethodArgumentResolver` というインターフェースがポイントになっており、自作のリゾルバを登録したい場合は `WebMvcConfigurer#addArgumentResolvers` を使えばよいです。


## ハンドラメソッドの引数で利用できるもの

ハンドラメソッドは以下のように定義し、引数で様々な情報を受け取ることが可能です。

下記の例では `@RequestBody` アノテーションを使うことで、リクエストボディをメソッドの引数として受け取っています。

``` java
@RestController
@RequestMapping("/qiita")
public class QiitaController {

	@PostMapping("/hello")
	public QiitaResponse hello(@RequestBody QiitaRequest request) {
		var response = new QiitaResponse();
		response.setMessage("Hello %s!!".formatted(request.getName()));
		return response;
	}
}

```

これらの他にも
- `@RequestParam`
- `@RequestHeader`
- `HttpServletRequest`
- `HttpSession`

など様々なものを指定する事ができます。詳細は[Handler Methods](https://docs.spring.io/spring-framework/reference/6.2/web/webmvc/mvc-controller/ann-methods.html)を参照してください。

## これらはどのような仕組みで解決されるのか

リクエストが `DispatcherServlet` に到達すると、リクエストのパスに基づいて実行対象のメソッドの特定が行われます。(省略)

その後 [ServletInvocableHandlerMethod#invokeAndHandle](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java#L118) を経由し、
 [InvocableHandlerMethod#invokeForRequest](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java#L180)でハンドラメソッドの実行が行われます。

その中の [InvocableHandlerMethod#getMethodArgumentValues](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-web/src/main/java/org/springframework/web/method/support/InvocableHandlerMethod.java#L206)でハンドラメソッドのそれぞれの引数を解決しており、以下の部分で行われています。

``` java:InvocableHandlerMethod.java
if (!this.resolvers.supportsParameter(parameter)) {
  throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
  args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
```

`resolvers.supportsParameter`, `resolvers.resolveArgument`では [HandlerMethodArgumentResolverComposite#getArgumentResolver](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolverComposite.java#L130) が呼び出されており、ここで 引数の定義に応じた`HandlerMethodArgumentResolver` が選択され、実際の値の作成が行われます。


## HandlerMethodArgumentResolver

[HandlerMethodArgumentResolver](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-web/src/main/java/org/springframework/web/method/support/HandlerMethodArgumentResolver.java)は以下の2つのメソッドが定義されています。

- `supportsParameter`
- `resolveArgument`

`supportsParameter` では、引数の情報が自身がサポートしているかどうかを返却し、
`resolveArgument` で実際の値の作成が行われます。

様々な実装クラスが存在します。ここで少し紹介します。

### RequestResponseBodyMethodProcessor

`@RequestBody` アノテーションが付与された引数を解決する際に利用されます。

### RequestParamMethodArgumentResolver

`@RequestParam` アノテーションが付与された引数を解決する際に利用されます。

### AuthenticationPrincipalArgumentResolver

`Spring Security` の機能で `@AuthenticationPrincipal` アノテーションが付与された引数を解決する際に利用されます。認証したユーザーの情報を取得することができます。

## HandlerMethodArgumentResolver の候補の一覧はどこで登録されるのか

`HandlerMethodArgumentResolverComposite` の中で候補の中から対象のリソルバの特定を行いますが、この候補はどこで登録されるのかを調べてみます。

これらは、アプリケーション起動時に[RequestMappingHandlerAdapter#getDefaultArgumentResolvers](https://github.com/spring-projects/spring-framework/blob/v6.2.3/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java#L685) で登録が行われています。また自分たちで独自で定義したリゾルバは後半部分の

``` java:RequestMappingHandlerAdapter.java
// Custom arguments
if (getCustomArgumentResolvers() != null) {
  resolvers.addAll(getCustomArgumentResolvers());
}
```

の部分で登録が行われます。
この `getCustomArgumentResolvers` で返ってきているものは
`WebMvcConfigurer#addArgumentResolvers` で登録したものが返されます。

例えば以下のように `WebMvcConfigurer` をBean定義しておけば 自作の `MyArgResolver` をリゾルバの一覧に追加することができます。

```java
@Bean
WebMvcConfigurer myWebMvcConfigurer() {
  return new WebMvcConfigurer() {

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

  };
}
```


### HandlerMethodArgumentResolver の実装例

上記の例で登場した `MyArgResolver` は以下のような実装です。

ハンドラメソッドの引数に `@MyArg` アノテーションが付与されていると固定の文字列を渡す実装にしています。

``` java:MyArgResolver.java
public class MyArgResolver implements HandlerMethodArgumentResolver {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
        // 引数に MyArgアノテーションが付与されているか？
		return parameter.hasParameterAnnotation(MyArg.class);
	}

	@Override
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 常に同じ文言を返すサンプル
		return "My arg !!!!!";
	}
}
```

``` java:MyArg.java
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyArg {

}
```

利用箇所のハンドラメソッドでは引数に `@MyArg` アノテーションを付与します。

``` java:QiitaController.java
@RestController
@RequestMapping("/qiita")
public class QiitaController {

	@PostMapping("/hello")
	public QiitaResponse hello(@RequestBody QiitaRequest request, @MyArg String value) {
		System.out.println(value); // My arg !!!!!
		
		var response = new QiitaResponse();
		response.setMessage("Hello %s!!".formatted(request.getName()));
		return response;
	}
}

```

独自のリゾルバが必要になりそうな場面ですが、個人的にはBean定義されているものはControllerクラスにDIすれば取得できるのでわざわざリゾルバを作る必要は無いと思います。
これらが必要になる例としてはリクエストに紐づいた情報を取得するのに利用するのがいいかなと思っています。

## 実はハンドラメソッドの戻り値も似たような方法で解決されている。

今回はハンドラメソッドの引数について説明しましたが、実はハンドラメソッドの戻り値についてもこれと似た方法で `HandlerMethodReturnValueHandler` というインターフェースを使って解決が行われています。


## まとめ

ハンドラメソッドの引数がどのような仕組みで解決され、また自作で定義するにはどのような方法で行うかについての説明をしました。

引数の解決の方法はライブラリを自作する際にも活用できそうなので、覚えておくとSpring以外の場面でも役に立つかもしれません。