理系学生日記

おまえはいつまで学生気分なのか

@ControllerAdvice を使(わ|え)ないときの、コントローラ横断例外ハンドラの実装

コントローラベースの例外ハンドラ

Spring MVC といえば、その名の通り Spring が提供する MVC フレームワークで、Controller 等を POJO で実装することができます。
で、Controller 層で例外を送出した場合、単純に HTTP STATUS 500 を返却するのではなく、送出される例外クラスによって独自のハンドラを Controller のメソッドとして定義し、クライアントへのレスポンスを制御することもできます。具体例を 16. Web MVC framework から引用するとこんなかんじ。

@Controller
public class SimpleController {

  // other controller method omitted

  @ExceptionHandler(IOException.class)
  public String handleIOException(IOException ex, HttpServletRequest request) {
    return ClassUtils.getShortName(ex.getClass());
  }
}

このケースだと、SimpleController という Controller 内で送出された IOException は、handleIOException で処理されることになります。

コントローラ横断の例外ハンドラ

上述の例外ハンドリングの仕組みは、通常、Controller のクラス内に閉じています。例えば、ControllerA で定義した IOException 例外ハンドラを ControllerB で使い回すことはできません。例外ハンドラは、あくまでクラス内のメソッドとして定義されているためです。
しかし、例外ハンドラを、コントローラ横断で使用したいケースというのは、ままあります。例えば、

  1. Controller に渡ってくるリクエストパラメータにバリデーションエラーが発生した場合は、Controller は一律 IllegalRequestException を送出する
  2. その上で、コントローラ横断で IllegalRequestException の例外ハンドラを定義し、そこで HTTP Status 400 (BAD_REQUEST) をクライアントに返却する

ような場合です。

この実装については、Spring 3.2 からはずっと楽になりました。なぜなら、Spring 3.2 から、@ControllerAdvice アノテーションが導入されたためです。しかし、Spring 3.1 にはそれがない。じゃぁどうすれば良いのか、今日のエントリのテーマはそこです。

実装方法

結論としては、まぁここを読みましょう。

上記ページで実装方法は分かります。
が、何がどうなっているのかという点は補足が必要かもしれません。

まず、重要なのは、独自の HandlerExceptionResolver を定義する必要がある、ということです。Spring MVC の文脈における HandlerExceptionResolver は、Controller から送出された例外をどのハンドラで処理するかを解決するコンポーネントを意味します。Application Context で明示的に設定しない場合はデフォルト実装である DefaultHandlerExceptionResolverが使用されます。これは、Spring の標準例外に対し、それぞれ適切な HTTP Status をマッピングしてクライアントに返却してくれる例外ハンドラへの解決方法を提供します。

しかし、我々には別の例外ハンドラが必要です。この例外ハンドラに要求されることは、

  1. Controller から送出された例外を、(例外送出元の Controller から探すのではなく)完全に別のクラスに定義された例外ハンドラに解決できること
  2. 一方で、Controller に当該例外に対する例外ハンドラが定義されていれば、そちらを優先して使用すること
    • つまり、あくまで Controller 横断の例外ハンドラは、(他に解決しようがないときに)最終的に fall back される例外ハンドラであること

になります。

これを実装しているのが以下のコードですね。

 @Component  
 public class GlobalExceptionResolver extends ExceptionHandlerExceptionResolver {  
   @Resource  
   private List<GlobalExceptionHandler> globalExceptionHandlers;  

   @Resource  
   private ExceptionHandlerExceptionResolver defaultResolver;  
     
   @PostConstruct  
   public void afterPropertiesSet() {  
     setMessageConverters(defaultResolver.getMessageConverters());  
     setOrder(2); 
     super.afterPropertiesSet();  
   }  
   
   @Override protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
     for (GlobalExceptionHandler exceptionHandler : globalExceptionHandlers) { 
       ExceptionHandlerMethodResolver methodResolver 
         = new ExceptionHandlerMethodResolver(exceptionHandler.getClass()); 
       Method method = methodResolver.resolveMethod(exception); 
       if (method != null) { 
         return new ServletInvocableHandlerMethod(exceptionHandler, method); 
       } 
     } 
     return null; 
   }
   
 }
GlobalExceptionHandler って?

GlobalExceptionHandler は、勝手に定義した単純なマーカインタフェースで、コントローラ横断の例外ハンドラを定義しているクラスが implement します。これをすることによって、上記クラス (GlobalExceptionResolver) の持つ List に、Spring Core が自動的に DI してくれる仕組みです。
具体的な例外ハンドラの定義の仕方は以下のような形になります。

// マーカ Interface 
public interface GlobalExceptionHandler {}

// 例外ハンドラを実装するクラス
@Component
public class GlobalExceptionHandlerImpl implements GlobalExceptionHandler {

    @ExceptionHandler(MyException.class)
    public String handleException(MyException e) {
       // hogehoge
       return hoge;
    }
}
ExceptionHandlerExceptionResolver って?

親クラスとして指定されている ExceptionHandlerExceptionResolver は、クラスの中から @ExceptionHandler のアノテーション指定がなされたメソッドを探す能力を持っている HandlerExceptionResolver 実装です。

オーバライドしている getExceptionHandlerMethod が、実際の例外ハンドラを探すメソッドですが、先程の globalExceptionHandlers を走査し、ExceptionHandlerMethodresolver#resolveMethod を使って、当該クラスに適切な例外ハンドラが定義されているようであれば、そのメソッドを実行する仕組みです。