Goslings開発メモ - その2: Spring Boot続編 (DI)」の続き。

Spring Boot続続編で、例外処理について。

Spring MVCアプリにおける例外処理

Goslingsは前々回書いたようにspring-boot-starter-webというスターターを使っていて、つまりSpring MVCアプリだ。

Spring MVCアプリにおける例外処理についてはちょっと古いがこの記事に詳しい。

まず、Goslingsの構成で例外処理を何も書かなかった場合、コントローラのリクエストハンドラから例外が投げられると、ログにスタックトレースが出力され、クライアントにはHTTPステータスコード500 (Internal Server Error)とともに以下の様なデフォルトのエラーページが返る。

err_page.png


なんだかこれでも十分な気がするが、実際にはちゃんと明示的に例外処理をしたほうがいいだろう。 エラー時に返すHTTPステータスコードをカスタマイズしたり、遷移するページを変えたりしたくなるだろうから。

記事によれば、リクエストハンドラ内で例外をキャッチして処理するのはイケてなくて、関心事の分離のために別の場所に処理を書くのが良いらしい。

Spring MVCアプリにおける例外処理には以下の3つの段階がある。

  1. 投げる例外をカスタマイズする
  2. 例外クラス毎の例外ハンドラをコントローラに実装する
  3. コントローラ間で共用する例外ハンドラクラスを作る

以下それぞれについて書く。

1. 投げる例外をカスタマイズする

リクエストハンドラから投げる例外に@ResponseStatusをつけることで、クライアントに返すHTTPステータスコード(とリーズンフレーズ)をカスタマイズできる。

例えば以下のような例外を投げると、HTTPステータスコード500 (Internal Server Error)の代わりに400 (Bad Request)がクライアントに返る。

@ResponseStatus(HttpStatus.BAD_REQUEST)
public final class BadRequestException extends RuntimeException {
  // 省略
}

2. 例外クラス毎の例外ハンドラをコントローラに実装する

コントローラのメソッドに@ExceptionHandlerをつけてやると、そのメソッドは例外ハンドラになり、そのコントローラのリクエストハンドラから特定の例外が投げられたときの処理を書くことができる。 さらに例外ハンドラに@ResponseStatusをつければ、HTTPステータスコードをカスタマイズできる。 例外ハンドラの戻り値はリクエストハンドラのと同様に処理されるので、遷移するページ等も自由にカスタマイズできる。

Goslingsでは、上記BadRequestExceptionからは@ResponseStatusを削除したうえで、RestApiV1Controllerに以下の様に例外ハンドラを書いた。

public final class RestApiV1Controller {

  // 例外ハンドラ
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler(BadRequestException.class)
  ErrorInfo handleBadRequestException(HttpServletRequest req, Exception ex) {
    return new ErrorInfo(req.getRequestURL().toString(), ex);
  }

}

(RestApiV1Controller.javaの完全なソースはこちら)


こう書くと、RestApiV1Controllerの任意のリクエストハンドラからBadRequestExceptionが投げられると、handleBadRequestExceptionが呼び出され、HTTPステータスコード400 (Bad Request)とともにクライアントにHTTPレスポンスが返る。 RestApiV1ControllerはREST APIコントローラなので、このHTTPレスポンスのボディは、handleBadRequestExceptionの戻り値であるErrorInfoオブジェクトをJSONに変換したものになる。

例外ハンドラの仮引数は、上のコードに書いたもののほか、サーブレット関係のクラスなど(e.g. HttpServletResponseHttpSession。詳しくはJavadoc参照)を適当に書いておくとSpring MVCがよしなに渡してくれる。

冒頭に貼った記事には例外ハンドラはModelを受け取れないとあるが、これは古い情報で、今は受け取れるっぽい。

3. コントローラ間で共用する例外ハンドラクラスを作る

コントローラから例外処理を完全に分離したい場合や、複数のコントローラで例外ハンドラを共有したい場合は、コントローラアドバイスクラスを書けばいい。

コントローラアドバイスクラスは@ControllerAdviceを付けて定義したクラスで、このクラスに例外ハンドラを書いておくと複数のコントローラで有効になる。

コントローラアドバイスクラスには例外ハンドラ以外も書ける。 コントローラアドバイスクラスが適用されるのはデフォルトでは全てのコントローラクラスだが、@ControllerAdviceの値により適用範囲を絞ることもできる。 詳しくはJavadoc参照。

Goslingsではコントローラアドバイスクラスは作らなかった。


今日はここまで。 次回もまたSpring Bootで、ロギングについて。