SpringBootのexception handlerを使ってみましょう



## はじめに
筆者はSIerでエンジニアをしています。

今日はexception handlerについてまとめてみます。

  - [はじめに](#はじめに)
  - [exception handlerとは](#exception-handlerとは)
  - [まずはexceptionが発生するAPIを用意する](#まずはexceptionが発生するapiを用意する)
  - [てことでexception発生時の処理を実装する](#てことでexception発生時の処理を実装する)
  - [exception handlerを実装する](#exception-handlerを実装する)
  - [それぞれのexceptionをそれぞれ処理する。](#それぞれのexceptionをそれぞれ処理する)
  - [それでは全部のexceptionのhandlerを実装しないといけないのか？](#それでは全部のexceptionのhandlerを実装しないといけないのか)
  - [まとめ](#まとめ)

## exception handlerとは
アプリケーションから発生した例外に対する処理を一箇所で管理できます。
また、それぞれのexceptionに対して、処理を実装できます。

実際に見てみましょう。

## まずはexceptionが発生するAPIを用意する
なんらかexceptionが発生するAPIがあるとします。

この実装だと、エラーがSpringBootデフォルトのハンドリングが行われます。
そして、`Whitelabel Error Page`のようなものがレスポンスとして返されます。

あまり嬉しくないですね。

```java
@RestController
@RequestMapping("/api")
public class SampleApi2 {

    @GetMapping("/hello2")
    public ResponseEntity<String> getHello() {
        // エラーが発生する処理
        doSomething();
        return ResponseEntity.ok("Hello, World!");
    }

    private void doSomething() {
        throw new RuntimeException();
    }
}
```

## てことでexception発生時の処理を実装する

try-catchを使用して、エラー時の処理を実装します。
これでしっかりアプリケーションとして、例外の制御もできるようになりました。

```java
@RestController
@RequestMapping("/api")
public class SampleApi2 {

    @GetMapping("/hello2")
    public ResponseEntity<String> getHello() {

        try {
            // エラーが発生する処理
            doSomething();
            return ResponseEntity.ok("Hello, World!");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Internal Server Error");
        }
    }

    private void doSomething() {
        throw new RuntimeException();
    }
}
```

ただし、このAPIがたくさんあったら、いちいち面倒ですね。
ここでexception handlerです。

## exception handlerを実装する

`@RestControllerAdvice`と`@ExceptionHandler`を使用して、exception handlerを実装しましょう。

:::note info
`@RestControllerAdvice`自体は、ExceptionHandler以外にも使用します。今回はexception handlerの実装を目的として使用します。
:::

```java
@RestControllerAdvice
public class SampleExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        // エラーが発生した場合の処理
        return ResponseEntity.status(500).body("Internal Server Error");
    }
}
```

これによって、controllerでの個別実装が不要になります。

```java
@GetMapping("/hello3")
public ResponseEntity<String> getHello3() {

    // エラーが発生する処理
    doSomething();
    return ResponseEntity.ok("Hello, World!");
}
```

ということで、APIが増えても一箇所にまとめられて良いですね。

## それぞれのexceptionをそれぞれ処理する。
さて、それでも発生する例外は一種類であありません。各exceptionにそれぞれ処理を実装したい場合は、アノテーションに渡すクラスをそれぞれ記載します。

```java
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        // エラーが発生した場合の処理
        return ResponseEntity.status(500).body("Internal Server Error");
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleRuntimeException(RuntimeException ex) {
        // エラーが発生した場合の処理
        return ResponseEntity.status(500).body("Runtime Error");
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        // エラーが発生した場合の処理
        return ResponseEntity.status(400).body("Bad Request");
    }
```

で、（絶対ないですが）こんなAPIがあったとします。
```java
    // runtime exceptionを発生させるAPI
    @GetMapping("/hello4")
    public ResponseEntity<String> getHello4() {
        throw new RuntimeException();
    }

    // illegal argument exceptionを発生させるAPI
    @GetMapping("/hello5")
    public ResponseEntity<String> getHello5() {
        throw new IllegalArgumentException();
    }

```

この結果、各APIで発生した例外が、各handlerで捕捉されます。

| API エンドポイント | 発生する例外             | 処理するハンドラー             | HTTPステータス | レスポンスボディ        |
| ------------------ | ------------------------ | ------------------------------ | -------------- | ----------------------- |
| /api/hello4        | RuntimeException         | handleRuntimeException         | 500            | "Runtime Error"         |
| /api/hello5        | IllegalArgumentException | handleIllegalArgumentException | 400            | "Bad Request"           |
| その他             | Exception                | handleException                | 500            | "Internal Server Error" |

独自例外も含めて個別に処理を実装できていい感じですね。

## それでは全部のexceptionのhandlerを実装しないといけないのか？

と思いますが、そうではありません、`java.lang.Exception`に対するhandlerを実装すれば、他のhandler`@ExceptionHandler`で捕捉されないものが、全て捕捉されます。

これはJavaの例外クラスの継承構造によるものです。すべての例外は`java.lang.Exception`クラスを継承しています。

```
Exception（↑ここが頂点）
    ├── RuntimeException
    │       └── IllegalArgumentException
    ├── IOException
    └── その他の例外クラス
```

このように、全ての例外は`Exception`クラスの子クラスになっているため、`@ExceptionHandler(Exception.class)`と記述することで、他のhandlerで捕捉されなかった全ての例外をキャッチすることができます。

なので、よく発生する例外は個別にhandlerを実装し、想定外の例外は`Exception`のhandlerで捕捉する、という実装で十分なのです。

:::note info
継承とか、全ての例外はExceptionの子クラスとか、その辺はJavaの言語として理解を進めましょう。
:::

## まとめ
今回はexception handlerについてまとめました。始めた時は個別にエラーハンドリングを実装しがちだったのですが、この仕組みはとても便利ですね。

今回は簡単な例でしたが、エラー用のログを出力したり、いろいろできそうです。
では今回は以上です。