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を登録
次に、DispatcherServlet
にExceptionHandlerExceptionResolver
が登録されるまでの流れを見る。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
が有効となり、その@Import
のEnableWebMvcConfiguration
が有効となる。
.. @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として取得できる。また@AliasFor
でRestControllerAdvice
のパラメータはすべてControllerAdvice
の同名のものに引き渡される。
@Component public @interface ControllerAdvice {
@ControllerAdvice @ResponseBody public @interface RestControllerAdvice { @AliasFor(annotation = ControllerAdvice.class) String name() default "";