【Spring Boot】Spring Bootにおける例外ハンドリング

# これまでの記事紹介
|  | URL |
|:-----------|:------------|
| 第1回       | [【Spring Boot】個人的に推奨するディレクトリ／ファイル構成](https://qiita.com/rpc_tk/items/b60adb5e55206585d98a "【Spring Boot】個人的に推奨するディレクトリ／ファイル構成")        |
| 第2回       | [【Spring Boot】【Java】定数宣言はfinal staticが良いのか、enumが良いのか](https://qiita.com/rpc_tk/items/c525c04f157f376e6c1e "【Spring Boot】個人的に推奨するディレクトリ／ファイル構成")        |

9/5：記事タイトルをREST APIにおける例外ハンドリング→Spring Bootにおける例外ハンドリングに修正しました。内容的にREST APIについて語り切れていないので本記事、もしくは次回記事に記載します。

# はじめに
皆さん例外ハンドリングに悩んだことはありませんか。
アプリケーションに例外はつきものなので、対処する必要があります。

適切に例外を拾って、適切な対処を行っていますか。
発生する可能性のある例外を放置していませんか。
無作為にtry-catchをしてエラーを無かったことにしてませんか。
本記事では例外そのものを知ってもらった上で、どう対処するかをSpringBootの実装で紹介します。

# 前提
SpringBootを使ったREST APIアプリケーションに焦点を当てています。
Webアプリケーションの場合はErrorControllerなどを使った方法などがありますが本記事では紹介しません。

# 読者ターゲット
- Java の Web/API アプリケーション開発経験がある方
- RESTful API がわかる方

# 例外とは
例外とは一般的にプログラム「実行」時に発生するエラーのことを指します。
例外の中でも「予期せぬ」例外と「予期する」例外とで分かれます。
「予期せぬ」例外は更に「検査例外」と「非検査例外」に分かれます。

私は「予期せぬ」例外をシステムエラー、「予期する」例外をビジネスエラーと呼んでいます。
アプリケーションではこれらの例外を適切に対処（ハンドリング）する必要があります。

### 検査例外とは
コンパイル時に検査される例外で、そのままではコンパイルエラーとなるためアプリケーションが動きません。検査例外が発生する可能性のあるメソッドを呼び出す場合に以下2つのいずれかの対処が必要です。
- try-catchで例外を捕捉する
```Java
// try-catchがないとコンパイルエラー
try {
  // FileReaderはFileNotFoundExceptionの検査例外が発生する可能性がある
  // 該当ファイルが存在しない場合はFileNotFoundExceptionが発生し、catchへ
  FileReader in = new FileReader("data.txt");
} catch (FileNotFoundException e) {
  // ここではスタックトレースを出力しているが、本来は適切な例外ハンドリングを行う
  e.printStackTrace();
}
```
- throwsで検査例外を上流（呼び出し元）へ伝達する
```Java
// throwsがないとコンパイルエラー
private void read() throws FileNotFoundException {
  // FileReaderはFileNotFoundExceptionの検査例外が発生する可能性がある
  // 該当ファイルが存在しない場合はFileNotFoundExceptionが発生し、上流（呼び出し元）へ伝達
  FileReader in = new FileReader("data.txt");
}

// 上流（呼び出し元）はtry-catchもしくは、更に上流へ伝達するthrowsによるハンドリングが必要
// ここではtry-catchによるハンドリングを紹介
private void main() {
  try {
    this.read();
  } catch (FileNotFoundException e) {
    // ここではスタックトレースを出力しているが本来は適切な例外ハンドリングを行う
    e.printStackTrace();
  }
}
```

### 非検査例外とは
実行時に発生する例外です。検査例外と違い、コンパイルには影響ありません。
プログラムのバグや「予期せぬ」例外が原因で発生することがあり、通常は防ぐことが難しいです。
代表例は「NullPointerException」でしょうか。
```Java
String str = null;
// null値（定義されていない値）を参照しようとしてNullPointerExceptionが発生
// 実行することでプログラムにバグがあることが分かる
System.out.println(str.length());
```

### システムエラーとは
システムは正常稼働していたが、「予期せぬ」事象が発生した

- 存在するはずのファイルが存在しない（FileNotFoundException
- null値の参照型変数を参照しようとした（NullPointerException
- データベースに接続できない（SQLException

### ビジネスエラーとは
システムは正常稼働しており、「予期する」事象が発生した

- 入力フォームからリクエストされた内容に必須項目がない（必須入力エラー
- ECサイトでユーザが商品を購入しようとしたが在庫がなかった（在庫なしエラー
```Java
// 在庫が存在しなかった場合の独自例外クラス
public class StockNotFoundException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  public StockNotFoundException(String message) {
    super(message);
  }
}
```
```Java
// 在庫が存在しなかった場合にStockNotFoundExceptionの独自例外を発生させる
if (stock == 0) {
  throw new StockNotFoundException("在庫がありません");
}
```

# なぜ例外をハンドリングするのか
- 例えば必須入力エラーが発生した場合に、何が起きたか（必須入力項目が不足）、次に何をすれば良いのか（必須入力項目に対して入力を促す）といったアプリケーションを利用しているユーザへの通知
- システム保守運用者が全ての例外を1つ1つ見ることは現実的に不可能です。必須入力エラーなど「予期する」例外は監視対象外にして、データベース接続エラーなど「予期せぬ」例外を監視対象にする

といったことが例外をハンドリングする上で重要となります。

## どのように例外をハンドリングするのか
- try-catchを使った例外の捕捉
- ExceptionHandlerを使った例外の捕捉

### try-catchを使った例外の捕捉
try-catchを使った構文で実際に説明します。
```Java
// data.txtファイルを読み込む
FileReader in = new FileReader("data.txt");
```
FileReaderを用いてdata.txtファイルを読み込むといった処理ですがdata.txtが存在しなかった場合はどうなるでしょうか。
```Java
java.io.FileNotFoundException: data.txt (指定されたファイルが見つかりません。)
	at java.base/java.io.FileInputStream.open0(Native Method) ~[na:na]
	at java.base/java.io.FileInputStream.open(FileInputStream.java:216) ~[na:na]
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157) ~[na:na]
	at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111) ~[na:na]
	at java.base/java.io.FileReader.<init>(FileReader.java:60) ~[na:na]
```
data.txtが存在しなかった場合はFileNotFoundException例外が発生します。
```Java
try {
  // 該当ファイルが存在しない場合はFileNotFoundExceptionが発生し、catchへ
  FileReader in = new FileReader("data.txt");
} catch (FileNotFoundException e) {
  // 以降該当ファイルが存在しない場合の例外ハンドリングを記述
}
```
data.txtが存在しないことを考慮してFileNotFoundExceptionの例外を捕捉できるようにtry-catchを使います。

## ExceptionHandlerを使った例外の捕捉
本質的に理解にするには「Servlet Container」「Dispatch Servlet」やら難しい話が出てきます。
個人的には理解したほうがベターであり、ベストではありません。
本記事では理解重視で説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586238/eeff3d2d-11e4-89d6-b4f6-2265ebf620fa.png)

①ECサイトを利用しているユーザから商品を購入するリクエストが発生
②状況に応じて以下のいずれかが発生
　1.リクエストにカード番号など購入に必要な情報がなかったので「必須入力エラー」発生
　2.在庫がなかったので「在庫なしエラー」発生
　3.在庫を確認するためのデータベースに接続できず「データベース接続エラー」発生
③(Rest)ControllerAdvice+ExceptionHandlerにより各種エラーをハンドリング
④エラーレスポンスを返却

といった流れになります。
Controller全体に共通処理を提供する「RestControllerAdvice」において「ExceptionHandler」を組み合わせることで、Controller経由で呼び出された処理において例外を捕捉することができます。とても便利ですね。

「ExceptionHandler」は特定の例外を捕捉することができます。try-catchのcatchに該当すると言えば分かりやすいでしょうか。

### RestControllerAdviceとExceptionHandlerを使った例外の捕捉
RestControllerAdviceとExceptionHandlerを使った構文で実際に説明します。
※絵に合わせていずれかの例外の説明が良いかと思いつつ、try-catchとの対比としたいのでFileReaderを使った紹介となります。ご了承ください。

```Java
@RestController
@RequestMapping("/hoge")
public class HogeController {

  @GetMapping("/file")
  public void getFile() throws FileNotFoundException {
    // data.txtが存在しないためFileNotFoundExceptionが発生！
    FileReader in = new FileReader("data.txt"); 
  }
}
```
```Java
@RestControllerAdvice
public class RestApiControllerAdvice extends ResponseEntityExceptionHandler {

  // FileNotFoundException例外を捕捉
  @ExceptionHandler(FileNotFoundException.class)
  public ResponseEntity<Object> handleFileNotFoundException(FileNotFoundException e) {
    // 500エラー返却
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("ファイルが存在しません。");
  }
}
```
実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586238/ef906022-fa06-760e-1c6a-889ab126dcda.png)


FileNotFoundExceptionが発生するとRestApiControllerAdviceクラスの@ExceptionHandler(FileNotFoundException.class)が付与されているメソッド(handleFileNotFoundException)にて捕捉されます。
ここではstatusとbodyにそれぞれ【HTTPステータス500】、【ファイルが存在しません。】を詰めてレスポンスとして返却しています。

#### bodyは独自オブジェクトも格納できる
bodyに文字列を詰めていますが、独自オブジェクトを指定することも可能です。
```Java
// lombokアノテーションを用いてコンストラクタやgetter/setterを生成
@Data
@Builder
@AllArgsConstructor
@RequiredArgsConstructor
// エラーレスポンスオブジェクトクラスを宣言
public class ErrorResponse {
  private String errorCode;
  private String errorMessage;
}
```

```Java
@ExceptionHandler(FileNotFoundException.class)
public ResponseEntity<ErrorResponse> handleFileNotFoundException(FileNotFoundException e) {
  // エラーレスポンスオブジェクトを生成
  ErrorResponse error =
      ErrorResponse.builder().errorCode("ERROR_001").errorMessage(e.getMessage()).build();
  // bodyにエラーレスポンスオブジェクトをセット
  return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
```
実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586238/211f9a84-efce-60e6-65c7-5bf6d994538c.png)

## それぞれの用途
|対処法  |使う場面  |備考  |
|---|---|---|
|try-catch  |①例外が発生することを想定しており、処理を継続したい場合。私がよく使う言葉で例えると「例外を握りつぶす」<br>②別の例外クラスにて再度例外を発生させたい場合。この場合はExceptionHandlerと組み合わせることも可能 |個別対処が必要  |
|ExceptionHandler  |例外単位でハンドリングしたい場合。FileNotFoundExceptionといった特定のタイミングのみ発生する例外の他に、ExceptionやRuntimeExceptionといった上位クラスを捕捉することも可能。 |共通対処  |

①例：FileNotFoundExceptionが発生したが、想定しているのでcatchで捕捉するがそのまま処理継続
```Java
StringBuilder readData = new StringBuilder();
try {
  FileReader in = new FileReader("data.txt");
  // ~ readDataに詰める処理 ~
} catch (FileNotFoundException e) {
  // 想定している例外なので握りつぶして処理続行
  readData.append("data.txtが存在しなかった用の文字列データ");
}
// 以降はreadDataを用いた同一処理
```

②例：ApplicationExceptionといった独自例外クラスを作成し、try-catchにてFileNotFoundExceptionを捕捉したらApplicationExceptionを発生させる。ApplicationExceptionをExceptionHandlerで捕捉する。
```Java
// 独自例外クラス
public class ApplicationException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  public ApplicationException(Throwable cause) {
    super(cause);
  }
}
```
```Java
try {
  FileReader in = new FileReader("data.txt");
} catch (FileNotFoundException e) {
  // ApplicationExceptionで再度例外を発生
  throw new ApplicationException(e);
}
```
```Java
// ExceptionHandlerによるApplicationException例外を捕捉
@ExceptionHandler(ApplicationException.class)
public ResponseEntity<ErrorResponse> handleApplicationException(ApplicationException e) {
  ErrorResponse error =
      ErrorResponse.builder().errorCode("ERROR_002").errorMessage(e.getMessage()).build();
  return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
```

## 最後に
RestControllerAdviceとExceptionHandlerの組み合わせによる例外ハンドリングはアプリケーション全体をカバーできるとても強力な機能です。例外ハンドリングに悩んでる方への一助になれば幸いです。