Spring Boot で REST API のエラー時のレスポンスをカスタマイズする（前編）

# 概要
Spring Boot でエラー時のレスポンスをカスタマイズしてみたので備忘録としてここに記載します。
今回は、下記のようにレスポンスのボディ部をカスタマイズしたいと思います。

変更前

```
{
  "timestamp": "2019-03-14T15:07:50.631+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "MyException 発生",
  "path": "/××××/××××"
}
```

変更後

```
{
  "timestamp": "2019-03-14T15:07:50.631+0000",
  "status": 500,
  "error": "Internal Server Error",
  "message": "MyException 発生",
  "path": "/××××/××××",
  "errorDetail": {
    "detailMessage": "××××××××××××"
  }
}
```

# 前提条件
## 環境
- SpringBoot 2.1.3
- Java 1.8.0_181

# 処理の流れ
今回の大まかな登場人物としては、下記の4つです。

- リクエストを受け付けるコントローラークラス
- 独自に定義した例外クラス
- レスポンスボディ用のクラス
- 発生した独自例外を捕捉するクラス

また、これから実装しようとしている処理の大まかな流れは下記です。

1. リクエストと同時に独自に定義した例外をコントローラーから明示的に `throw`
2. （その例外をハンドルするクラスを作成しておく） `throw` された例外をハンドルクラスでキャッチ
3. カスタマイズしたレスポンスボディを返す

以降、上記処理の実装をしていきます。（順番はバラバラですが。。。）

# 実装
## マッピングクラスの定義
ここでは、レスポンスボディのマッピングクラスを定義します。

```ErrorResponseBody.java
package com.example.demo.errorResponse;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Setter;

import java.time.ZonedDateTime;

@Setter
public class ErrorResponseBody {

        @JsonProperty("timestamp")
        private ZonedDateTime exceptionOccurrenceTime;
        @JsonProperty("status")
        private int status;
        @JsonProperty("error")
        private String error;
        @JsonProperty("message")
        private String message;
        @JsonProperty("path")
        private String path;
        @JsonProperty("errorDetail")
        private ErrorDetail errorDetail;
}
```

```ErrorDetail.java
package com.example.demo.errorResponse;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Setter;

@Setter
public class ErrorDetail {
    @JsonProperty("detailMessage")
    String detailMessage;
}
```

## 独自例外クラスの定義
```MyException.java
package com.example.demo.errorResponse;

import lombok.Getter;

@Getter
public class MyException extends RuntimeException {

    private ErrorDetail errorDetails;

    public MyException(String message, ErrorDetail errorDetails) {
        super(message);
        this.errorDetails = errorDetails;
    }
}
```

## コントローラの定義

```HelloController.java
package com.example.demo.errorResponse;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/exceptionhandle")
public class HelloController {

    @RequestMapping("/test")
    public String get() {
        ErrorDetail errorDetail = new ErrorDetail();
        errorDetail.setDetailMessage("詳細なメッセージ");
        throw new MyException("MyException 発生", errorDetail);
    }
}
```

## Exception Handler クラスの定義
今回で一番大事な部分です。
ポイントは4つです。
1. ` @RestControllerAdvice `アノテーションをハンドルクラスに付与する
2. ` ResponseEntityExceptionHandler `クラスを継承する
3. ` @ExceptionHandler `アノテーションを付与し、補足対象の独自に定義した例外クラスを明記する
4. ` super.handleExceptionInternal `メソッドの2つ目のパラメータにレスポンスボディ用に定義したクラスのインスタンスを渡してあげる（余談ですが、このメソッドの引数は下記のような順番になっています。）

- 対象の例外クラス
- レスポンスのボディ部
- ヘッダー
- レスポンスステータス
- リクエスト

そして最終的には `ResponseEntity` を返しています。

```HelloExceptionHandler.java
package com.example.demo.errorResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.ZonedDateTime;

@RestControllerAdvice
public class HelloExceptionHandler extends ResponseEntityExceptionHandler {
    // Controller から throw される MyException を捕捉
    @ExceptionHandler(MyException.class)
    public ResponseEntity<Object> handleMyException(MyException exception, WebRequest request) {
        HttpHeaders headers = new HttpHeaders();

        return super.handleExceptionInternal(exception,
                createErrorResponseBody(exception, request),
                headers,
                HttpStatus.BAD_REQUEST,
                request);
    }

    // レスポンスのボディ部を作成
    private ErrorResponseBody createErrorResponseBody(MyException exception, WebRequest request) {

        ErrorResponseBody errorResponseBody = new ErrorResponseBody();
        int responseCode = HttpStatus.BAD_REQUEST.value();
        String responseErrorMessage = HttpStatus.BAD_REQUEST.getReasonPhrase();
        String uri = ((ServletWebRequest) request).getRequest().getRequestURI();

        errorResponseBody.setExceptionOccurrenceTime(ZonedDateTime.now());
        errorResponseBody.setStatus(responseCode);
        errorResponseBody.setError(responseErrorMessage);
        errorResponseBody.setMessage(exception.getMessage());
        errorResponseBody.setPath(uri);
        errorResponseBody.setErrorDetail(exception.getErrorDetails());

        return errorResponseBody;
    }
}
```

余談ですが、親クラス `ResponseEntityExceptionHandler` クラスの `handleExceptionInternal` メソッドは下記のようになっています。エラーレスポンス用のインスタンスを渡してあげる部分が `@Nullable Object body` になっているので、ちゃんと渡さないとボディ部が表示されません。

```ResponseEntityExceptionHandler.java
	/**
	 * A single place to customize the response body of all Exception types.
	 * <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE}
	 * request attribute and creates a {@link ResponseEntity} from the given
	 * body, headers, and status.
	 * @param ex the exception
	 * @param body the body for the response
	 * @param headers the headers for the response
	 * @param status the response status
	 * @param request the current request
	 */
	protected ResponseEntity<Object> handleExceptionInternal(
			Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {

		if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
			request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
		}
		return new ResponseEntity<>(body, headers, status);
	}
```

# 結果
```
{
  "timestamp": "2019-03-18T10:11:13.795+09:00",
  "status": 400,
  "error": "Bad Request",
  "message": "MyException 発生",
  "path": "/exceptionhandle/test",
  "errorDetail": {
    "detailMessage": "詳細なメッセージ"
  }
}
```

# 関連記事
[Spring Boot で REST API のエラー時のレスポンスをカスタマイズする（後編）](https://qiita.com/Keisuke1119/items/88b2e7956db34e7778a8)

# 参考記事
今回、記事を記載する上で下記の記事を参考にさせて頂きました。

[Spring Bootで作成したREST APIのエラーレスポンスをカスタムする](https://qiita.com/YutaKase6/items/434d36901e3a3360e9b0)
[SpringBootでエラーハンドリング](https://qiita.com/kws9/items/55982154a26a65426503)
[SpringBootの@RestControllerで例外処理をする](https://qiita.com/shotana/items/f3627e45feb912946c7c)