Spring MVC のハンドラメソッド(コントローラークラスのメソッド)の引数について調べたメモ
バージョンは 6.2.3
時点の内容です。
結論からいうと HandlerMethodArgumentResolver
というインターフェースがポイントになっており、自作のリゾルバを登録したい場合は WebMvcConfigurer#addArgumentResolvers
を使えばよいです。
ハンドラメソッドの引数で利用できるもの
ハンドラメソッドは以下のように定義し、引数で様々な情報を受け取ることが可能です。
下記の例では @RequestBody
アノテーションを使うことで、リクエストボディをメソッドの引数として受け取っています。
@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を参照してください。
これらはどのような仕組みで解決されるのか
リクエストが DispatcherServlet
に到達すると、リクエストのパスに基づいて実行対象のメソッドの特定が行われます。(省略)
その後 ServletInvocableHandlerMethod#invokeAndHandle を経由し、
InvocableHandlerMethod#invokeForRequestでハンドラメソッドの実行が行われます。
その中の InvocableHandlerMethod#getMethodArgumentValuesでハンドラメソッドのそれぞれの引数を解決しており、以下の部分で行われています。
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 が呼び出されており、ここで 引数の定義に応じたHandlerMethodArgumentResolver
が選択され、実際の値の作成が行われます。
HandlerMethodArgumentResolver
HandlerMethodArgumentResolverは以下の2つのメソッドが定義されています。
supportsParameter
resolveArgument
supportsParameter
では、引数の情報が自身がサポートしているかどうかを返却し、
resolveArgument
で実際の値の作成が行われます。
様々な実装クラスが存在します。ここで少し紹介します。
RequestResponseBodyMethodProcessor
@RequestBody
アノテーションが付与された引数を解決する際に利用されます。
RequestParamMethodArgumentResolver
@RequestParam
アノテーションが付与された引数を解決する際に利用されます。
AuthenticationPrincipalArgumentResolver
Spring Security
の機能で @AuthenticationPrincipal
アノテーションが付与された引数を解決する際に利用されます。認証したユーザーの情報を取得することができます。
HandlerMethodArgumentResolver の候補の一覧はどこで登録されるのか
HandlerMethodArgumentResolverComposite
の中で候補の中から対象のリソルバの特定を行いますが、この候補はどこで登録されるのかを調べてみます。
これらは、アプリケーション起動時にRequestMappingHandlerAdapter#getDefaultArgumentResolvers で登録が行われています。また自分たちで独自で定義したリゾルバは後半部分の
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
の部分で登録が行われます。
この getCustomArgumentResolvers
で返ってきているものは
WebMvcConfigurer#addArgumentResolvers
で登録したものが返されます。
例えば以下のように WebMvcConfigurer
をBean定義しておけば 自作の MyArgResolver
をリゾルバの一覧に追加することができます。
@Bean
WebMvcConfigurer myWebMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MyArgResolver());
}
};
}
HandlerMethodArgumentResolver の実装例
上記の例で登場した MyArgResolver
は以下のような実装です。
ハンドラメソッドの引数に @MyArg
アノテーションが付与されていると固定の文字列を渡す実装にしています。
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 !!!!!";
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyArg {
}
利用箇所のハンドラメソッドでは引数に @MyArg
アノテーションを付与します。
@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以外の場面でも役に立つかもしれません。