Spring Web MVC のエラーハンドリング方法について調べたのでメモ
調べたバージョンは
- Spring Boot 3.2.1
- Spring Web MVC 6.1.2
例外をハンドリングできるところはいろいろあるかもしれないですが
-
@ExceptionHandler
(Controller, ControllerAdviceに付与) HandlerExceptionResolver
ErrorController
についてメモを残します。
HandlerExceptionResolver
説明の都合上先にこれから説明します。
HandlerExceptionResolver
インターフェースを実装したクラスをBean定義をしておくことで例外のハンドリングを行うことができます。
resolveException
を実装し、解決できた場合はModelAndView
を返し、未解決の場合はnull
を返す実装を行います。
定義したBeanは DispatcherServletのhandlerExceptionResolversフィールド で List<HandlerExceptionResolver>
として保持されます。
initHandlerExceptionResolversメソッドで取得が行われます。
この時 AnnotationAwareOrderComparator
によりソートが行われるのでOrdered
インターフェースもしくは@Order
アノテーションを付与することで優先順位をつけることができます。
何も優先順位をつけていない場合は 数値が最大(つまり最後)となります。
Spring Bootで動かした場合デフォルトでは DefaultErrorAttributes
と HandlerExceptionResolverComposite
が存在しています。
DefaultErrorAttributes
は例外のハンドリングは行わず、後述のErrorController
で利用できるように例外のインスタンスを退避します。
HandlerExceptionResolverComposite
は内部にHandlerExceptionResolver
の一覧を持っており後述の@ExceptionHandler
をハンドリングしている ExceptionHandlerExceptionResolver
などが存在します。
例外のハンドリングが行われるのはDispatcherServletのprocessHandlerExceptionメソッドでハンドリングが行われます。
意識しておいたほうがいいこと
なるべくOrderedをつけておいた方がいいかなと思います。
HandlerExceptionResolverComposite
の DefaultHandlerExceptionResolver が一部の例外を解決するため、自作のHandlerExceptionResolver
がこれよりも後の優先順位だと、すでに解決済みであるためresolveExceptionが呼ばれない場合があります。
他の注意点としてはハンドリング箇所を見れば明らかですが、サーブレットフィルターなどで例外が発生した場合はハンドリングが行われません。
ExceptionHandlerアノテーション
Controller
や @ControllerAdvice
のクラスに@ExceptionHandler
にハンドリング対象の例外を指定し、以下のような実装をすることで例外のハンドリングを行うことができます。
また未解決の場合はnullを返すようにします。
@ExceptionHandler({ MyException.class })
public Map<String, Object> errorHandling(Exception e) {
return Map.of(
"message", "例外です",
"exceptionClass", e.getClass().getName(),
"exceptionMessage", e.getMessage());
}
例外が起きた場合はExceptionHandlerExceptionResolverのgetExceptionHandlerMethodメソッドで対象のメソッドの特定が行われます。前半部分でControllerに付与したものからの特定、後半部分でControllerAdviceに付与したものからの特定が行われます。
その後対象メソッドの実行が行われます。
ExceptionHandlerExceptionResolverはHandlerExceptionResolverの実装クラスなので 同様にDispatcherServletのprocessHandlerExceptionから呼び出されます。
こちらも同様にHandlerExceptionResolverの仕組みを使っているためサーブレットフィルターなどで発生した例外はハンドリングできません。
ErrorController
Spring Bootで追加された仕組みです。
例外やsendError
が発生したときに
warで起動しているときはErrorPageFilter、jarでTomcatで起動している時はStandardHostValveによって/error
へリクエストがフォワードされます。
デフォルトだとBasicErrorControllerが使用されますが ErrorController
の実装クラスをBean定義することで上書きが可能です。
実装方法の詳細は BasicErrorController
を参考で ErrorAttributes
などから情報を取得しレスポンスを返します。
意識しておいたほうがいいこと
ErrorPageFilter
や StandardHostValve
は一連の処理の手前の方で適用されるためサーブレットフィルターで発生した例外などもハンドリングすることが可能です。
/error
への折り返しの際、ErrorController
へ到達するまでに サーブレットフィルターやHandlerInterceptor
を通過するため、場合によっては 以下のような適応対象外の対応が必要になるかもしれません。
(例えばHandlerInterceptor
のなかで 何かのチェックを行っており、例外をスローしている場合などは /error
でも同様のチェックが実行されて 再度例外がスローされてしまう可能性があります。)
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//↓/errorの時は 適用しない
registry.addInterceptor(new MyHandlerInterceptor()).excludePathPatterns("/error/**");
}
}
また /error
に折り返されているため HttpServletRequest
などからパスを取得した際も元々のリクエストではなく /error
となっているため注意が必要です。
元々のリクエストの情報を退避しておきたい場合は DefaultErrorAttributes
を継承したクラスを作って退避しておくとよいかもしれないですね
まとめ
上記のポイントがよく使う箇所になるかなと思います。
それぞれ注意すべきポイントがあるので、それらを意識しながら使うとよいかなと思います。