JavaのWebアプリケーション開発フレームワークによる、Webサイト開発の顛末記です。

EclipseのMavenを使った、Spring-MVC、Thymeleaf、MyBatis 等のプログラミングテクニックを、
備忘録的に記録しています。実際に動くソースコードを多用して説明していますので、
これからEclipseや、Spring-MVCを始めたいと思っている人にとって、少しでも参考になれば幸いです。
■SpringMVC の小径 ちょっと寄り道 ロギングの小径
9-X)補足 例外処理
このページは、時系列が少し逆転して、2017年1月に執筆しています。
前々からの残課題だった、例外のハンドリングついて
まじめに検証したかったのですが
諸々の事情があり、すっかり時間が空いてしまいました。
時間が逆転してしまっている部分もあるかもしれませんが、
前節で解説した、ロギングに絡めて例外処理のハンドリングについて検証してみました。

では本題です。
前節で AOPによる例外処理(@AfterThrowing)を経験しましたが、
Spring-MVCによる例外処理(Exception Handling)は実はこれだけではありません。
例外処理の実装方法については、
テックノート :springの3種類の例外ハンドリング方法を紹介します
がわかりやすいかと思うので、ご紹介しておきます。
ざっくり説明しておきますと
1)@AfterThrowing による例外ハンドリング
  前節 4-4)ロギング で説明した通り、例外が発生すると @AfterThrowingで定義されたメソッドが実行されます。
  @Aspectクラスで指定されたパッケージのスコープ内で有効となります。

2)@ExceptionHandler による例外ハンドリング
  このアノテーションが宣言されているメソッドの、呼び出し元のコントローラで有効となります。

3)HandlerExceptionResolverによる例外ハンドリング
  HandlerExceptionResolverクラスを継承するハンドラクラスを作成しDIコンテナに登録しておくと、
  このアプリケーション全体で有効となります。

で、テックノートさんのサイトはこの3例ですが、もう一つ伝統的な例外キャッチの方法として、
4)try {} catch() {} の例外ハンドリング
  あまりにも普遍的なので説明の必要も無いとは思いますが、一応。
  try {} catch() {} ブロック内で有効となる例外処理ハンドリングです。

2)@ExceptionHandlerと、3)HandlerExceptionResolver の具体的な実装方法は、
springMVCの例外ハンドラの実装例(@ExceptionHandler編)  とか、
springMVCの例外ハンドラの実装例(HandlerExceptionResolver編)
に詳しいく載っていますので、ぜひご一読ください。

で、ここでは何を言いたいのかというと、
上記の4種類の例外処理を、全て実装した状態で例外が発生すると、どのハンドラに引っ掛かるのか?
つまり、例外処理の優先順位はどうなっているのか、詳しく検証したサイトが見つからないので
ここで実験してみた次第です。
まず実験方法
手っ取り早い方法として、コントローラのメソッド内で、0割を行う計算式を定義して0除算エラー(ArithmeticException)を発生させます。

テスト手順
1:すべての例外処理を組み込んでサーブレットを実行
 結果:4)try {} catch() {} で例外がキャッチされ、1)、2)、3)は動作しない。
 ⇒つまり、最も伝統的・普遍的な方法が最優先でそれ以外は無視される

2:1.の手順を受けて4)以外の組み合わせを実施
 結果:1)@AfterThrowing で例外がキャッチされ、
    続いて2)@ExceptionHandlerで例外がキャッチされた。
    3)HandlerExceptionResolverは無視された。

3:2.の手順を受けて2)、4)以外の組み合わせを実施
 結果:1)@AfterThrowing で例外がキャッチされ、
    続いて3)HandlerExceptionResolverで例外がキャッチされた。

まとめ
最優先的に実行されるのは4)try {} catch() {} で、ここで例外が握りつぶされるので、
ロギング含め例外処理(エラージャンプなど)は、catch()ブロックで記述する必要があります。
次は、1)@AfterThrowing が実行され、例外はキャッチされるもののここで握りつぶされず
後続の処理に引き継がれます。
続いて、2)@ExceptionHandlerが実行され、ここで例外が握りつぶされます。
2)が実装されていない場合は、3)HandlerExceptionResolverが実行されます。

結論として、4)try {} catch {}で、ちまちまと例外処理を書くより
@AfterThrowingでログを取りながら、@ExceptionHandlerか、HandlerExceptionResolverで共通エラー画面に飛ばす。
というような実装方法が、例外処理を共通化できて楽ができるということが分かりました。
@ExceptionHandlerは、そのコントローラの特別な例外事情がある場合などで、
通常は、HandlerExceptionResolverでアプリケーション共通のエラー処理にしておけば
かなり、コードの量が減らせるはずです。
それでは、今回のテストサンプルコードを具体的に見てみましょう。
AOPのロギングクラスは、前節でご紹介しているので、それは前節で確認してもらうとして
まず、例外処理コントローラ
src/main/java/jp/dip/arimodoki/cntl/Exception.java

リクエストマッピングハンドラ except() で、0除算を行って強制的にArithmeticException例外を発生させています。
ここで、4)try{} catch(){} による例外処理をハンドリングします。
さらに、@ExceptionHandler例外処理メソッドExceptionHandler()を実装します。
4)try{} catch(){}がない場合は、@AfterThrowingが実施された後、このハンドラが実施されます。

 

次に、3)HandlerExceptionResolverの拡張クラス
src/main/java/jp/dip/arimodoki/common/GlobalExceptionResolver.java

実行時に例外が発生すると、原則このハンドラに飛び、エラーページにジャンプします。
今回のテストの手順でいうと、3のケースで、@AfterThrowingが実施された後、このハンドラが実施されます。
クラスに@Componentアノテーションを宣言することで、
このクラスがDIコンテナに自動的に登録され使用可能となります。


最後に、HandlerExceptionResolverによりジャンプするエラーページ
WebContent/error.html

GlobalExceptionResolverクラスでセットされたエラーメッセージを表示するページです。

 

おわりに。
いくつかのパターンの例外を実験してみたのですが、
全ての例外が、ここで説明した法則に従って処理されるわけではないようです。
具体的には、ファイルのアップロードサイズ制限エラー(MaxUploadSizeExceededException)
の例外処理を実施してみたところ、@AfterThrowingも、@ExceptionHandlerも、さらには最強のはずの
try {} catch() {} ですら例外をキャッチできずに、いきなりHandlerExceptionResolverで処理される
というようなケースもありました。
その他、様々なExceptionについてはどのように動作するかすべて検証している訳ではないので
このテストケースは、あくまで標準パターンとして考えてください。