kagamihogeの日記

kagamihogeの日記です。

spring-bootでControllerAdviceのExceptionHandlerが呼ばれるまでの流れ

spring-bootでは@ControllerAdvice / @RestControllerAdviceによりcontollerの例外処理を一か所にまとめられる。このエントリでは例外が発生してadviceのメソッドが実際に呼び出されるまでのコードを追う。

ソースコードのバージョンはこれを書いてる 2024/11 月頃のgithubを参照しているが、余程大規模な変更がされない限りそこまで大きくは変わらないとは思う。

例外発生時の流れ

DispatcherServletから例外リゾルバに解決を依頼

まずはDispatcherServletから始まる。これの詳細は省略するが、簡単に言えば各controllerへのdispatchを担当している。以下のha.handleがそのdispatchに相当する。そのdispatch、つまりcontrollerの各メソッド呼出、が例外をthrowしたらcatchする。その例外はprocessDispatchResultに渡す。

public class DispatcherServlet extends FrameworkServlet {
..
  protected void doDispatch(...) .. {
    try {
      ...
      mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
      ...
    } catch (Exception ex) {
      dispatchException = ex;
    ..
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

processDispatchResultはconroller呼出結果に対する処理。もし例外が発生していたらprocessHandlerExceptionを呼ぶ。

private void processDispatchResult(...) throws Exception {
  ...
  if (exception != null) {
    ...
  else {...
     mv = processHandlerException(request, response, handler, exception);

processHandlerExceptionは例外に対する処理。自身に登録されている例外リゾルバーに解決を依頼する。その戻り値型はModelAndViewなので、例外リゾルバーの実装に応じてエラーページ表示したりなんだりが最終的に行われる。

protected ModelAndView processHandlerException(...) throws Exception {
  ModelAndView exMv = null;
  for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
    exMv = resolver.resolveException(request, response, handler, ex);

ExceptionHandlerExceptionResolverが例外を解決

例外の解決にはspring-bootのデフォルトではExceptionHandlerExceptionResolverが使用される*1。ここではmapで例外型に対するメソッドを保持しており、例外の解決をそのメソッドに依頼する、という処理になる。

このクラスはexceptionHandlerAdviceCacheというプロパティがあり、キーがcontroller-adviceで値はそれに対応する例外処理クラスのmapを保持している。

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
  implements ApplicationContextAware, InitializingBean {
  
  private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>();

キーのControllerAdviceBean は名前そのままのcontroller-adviceを表現するためのクラスなので詳細は省略する。値のExceptionHandlerMethodResolverは以下のように、キーが例外型で値がそれに対応するMethodのmapを保持している。つまり、controller-advice -> 例外型 -> Method(@ExceptionHandlerのメソッド)、というマッピングをここで保持している。

public class ExceptionHandlerMethodResolver {
  private final Map<ExceptionMapping, ExceptionHandlerMappingInfo> mappedMethods = new HashMap<>(16);

  private record ExceptionMapping(Class<? extends Throwable> exceptionType, MediaType mediaType) {
public class ExceptionHandlerMappingInfo {
  ..
  private final Method handlerMethod;

以上がcontrollerが例外をthrowするとcontroller-adviceの@ExceptionHandlerが呼ばれる大まかな流れとなる。

例外リゾルバー登録の流れ

ApplicationContext経由でExceptionHandlerExceptionResolverを登録

次に、DispatcherServletExceptionHandlerExceptionResolverが登録されるまでの流れを見る。WebApplicationContextリフレッシュでinitHandlerExceptionResolversでリゾルバを初期化している。この初期化ではHandlerExceptionResolverのmanaged-beanをすべて取得する。その一つが先に見たExceptionHandlerExceptionResolverである。

public class DispatcherServlet extends FrameworkServlet {
  ..
  @Override
  protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
    ..

  protected void initStrategies(ApplicationContext context) {
    ..
    initHandlerExceptionResolvers(context);
    ...

  private void initHandlerExceptionResolvers(ApplicationContext context) {
    ..
    Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);

spring-bootが生成したExceptionHandlerExceptionResolverのmanaged-beanがApplicationContext(WebApplicationContext)を介してDispatcherServletに登録されているのが分かる。

auto-configによるデフォルトのmanaged-bean生成

ExceptionHandlerExceptionResolverのmanaged-beanはどこで生成されるのか。簡単に言えばspring-bootのauto-configの仕組みで生成される。auto-configの詳細は省略するが、今回のエントリで関係する部分だけを抜粋する。

デフォルトでは、まずauto-configでWebMvcAutoConfigurationが有効となり、そのクラス内のstaticなconfig WebMvcAutoConfigurationAdapterが有効となり、その@ImportEnableWebMvcConfigurationが有効となる。

..
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, .. })
public class WebMvcAutoConfiguration {

  ..
  @Import(EnableWebMvcConfiguration.class)
  public static class WebMvcAutoConfigurationAdapter ..

  ..
  @Configuration(proxyBeanMethods = false)
  public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration ..

EnableWebMvcConfigurationの親クラス階層にWebMvcConfigurationSupportがある。ここでHandlerExceptionResolverのmanaged-beanを生成する。ここの詳細は省略するが、下記のように、デフォルトではExceptionHandlerExceptionResolverのmanaged-beanが生成される。

public class WebMvcConfigurationSupport ..
  @Bean
  public HandlerExceptionResolver handlerExceptionResolver(
    ..
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);

  protected final void addDefaultHandlerExceptionResolvers(..) {
    ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
    ..

  protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
    return new ExceptionHandlerExceptionResolver();

auto-configがデフォルトのExceptionHandlerExceptionResolverのmanaged-beanを生成しているのが分かる。

ControllerAdviceBeanマップの生成

ExceptionHandlerExceptionResolverでキーがcontroller-adviceで値はそれに対応する例外処理クラスのmapを保持しているのは先に述べた。では、そのmapはどのように生成しているのか。以下のように、このmanaged-bean生成時にControllerAdviceBean.findAnnotatedBeansでadviceのmanaged-beanリストを取得している。

public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver .. {
  ..
  private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
            new LinkedHashMap<>();

  @Override
  public void afterPropertiesSet() {
    initExceptionHandlerAdviceCache();
  ..

  private void initExceptionHandlerAdviceCache() {
    List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
  for (ControllerAdviceBean adviceBean : adviceBeans) {
    Class<?> beanType = adviceBean.getBeanType();
    ..
    ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
    if (resolver.hasExceptionMappings()) {
      this.exceptionHandlerAdviceCache.put(adviceBean, resolver);

このメソッドは単に@ControllerAdviceを持つmanaged-beanのリストを取得する。

public class ControllerAdviceBean implements Ordered {
  ..
  public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
    ..
    for (String name : ...) {
      ControllerAdvice controllerAdvice = beanFactory.findAnnotationOnBean(name, ControllerAdvice.class);
      ..
        adviceBeans.add(new ControllerAdviceBean(name, beanFactory, controllerAdvice));

@ExceptionHandlerの付与されたメソッドの取得は下記あたりで行っている。

public class ExceptionHandlerMethodResolver {
  private static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);

  ..
  public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
      ExceptionHandlerMappingInfo mappingInfo = detectExceptionMappings(method);
      ..

  private ExceptionHandlerMappingInfo detectExceptionMappings(Method method) {
    ExceptionHandler exceptionHandler = readExceptionHandlerAnnotation(method);
    ..

  private ExceptionHandler readExceptionHandlerAnnotation(Method method) {
    ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);

その他

ControllerAdviceとRestControllerAdviceは内部的には同一

アノテーションは内部的にはどちらもControllerAdviceと同一扱いされる、と考えてよい。以下の定義に見るように、RestControllerAdviceにはControllerAdvice(とResponseBody)が付与されている。このためcontext.findAnnotationOnBean("adviceBeanName", ControllerAdvice.class);とかやるとどちらかのアノテーションがあればControllerAdviceが付与されたbeanとして取得できる。また@AliasForRestControllerAdviceのパラメータはすべてControllerAdviceの同名のものに引き渡される。

@Component
public @interface ControllerAdvice {
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
  @AliasFor(annotation = ControllerAdvice.class)
  String name() default "";

関連ソースコード

https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java

https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMethodResolver.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/method/annotation/ExceptionHandlerMappingInfo.java

https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java

https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/bind/annotation/ControllerAdvice.java

https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/bind/annotation/RestControllerAdvice.java

*1:実際には他のリゾルバも登録される。なので、正確には、このケースではExceptionHandlerExceptionResolverが使用される、と書いた方が良いかもしれない。ただ今回のエントリの主旨では無いので省略。