Spring Bootを使ったRESTAPIで、共通的な例外処理を実装する

# はじめに
ここのところ、新規で開発をするための準備として共通的に実装すべきことについて整理を続けています。
ここでは、例外処理について、改めて整理をします。

# 環境
Spring Boot
Lombok

# ControllerAdviceと、ExceptionHandler

Spring Bootでは、ControllerAdviceアノテーションをクラスに付与し、そのメソッドにExceptionHanlderアノテーションを付与することで、特定の例外を捕捉してその場合の処理を記載する仕組みがあります。
ここで、Runtime例外のサブクラスを捕捉することにより、各レイヤーで発生する例外をすべて処理することができます。

今回はこれを、利用してやってみようと思います。
また、HandlerExceptionResolverインタフェースをインプリメントしたクラスでもエラーの捕捉は可能です。
これは、上記のExceptionHandlerで定義されていない例外を捕捉するのに利用するようです。

# やってみる
## 例外クラス
まずは例外クラスを作ります。
ExceptionHandlerで捕捉するために、個別の例外を作ります。
プロジェクト共通的なものは基底クラスに押し込んで、用途別にクラスを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/3a1485d0-3b3d-6160-f9aa-c6087a23c47b.png)

今回は適当な名前で作成してますが、用途別にわかりやすいネーミングにするのがよいでしょう。

```java:HogeFugaException.java
import java.util.Arrays;
import java.util.List;

import lombok.Getter;

public abstract class HogeFugaException extends RuntimeException {

	@Getter
	private List<ErrorEntry> errors;

	public HogeFugaException(Throwable cause, List<ErrorEntry> errors) {
		super(cause);
		this.errors = errors;
	}

	public HogeFugaException(Throwable cause, ErrorEntry error) {
		super(cause);
		this.errors = Arrays.asList(error);
	}

	public HogeFugaException(List<ErrorEntry> errors) {
		this.errors = errors;
	}

	public HogeFugaException(ErrorEntry error) {
		this.errors = Arrays.asList(error);
	}

}

```

```java:HogeException.java
import java.util.List;

public class HogeException extends HogeFugaException {

	public HogeException(Throwable cause, List<ErrorEntry> errors) {
		super(cause, errors);
	}

	public HogeException(Throwable cause, ErrorEntry error) {
		super(cause, error);
	}

	public HogeException(List<ErrorEntry> errors) {
		super(errors);
	}

	public HogeException(ErrorEntry error) {
		super(error);
	}
}
```

```java:FugaException.java
import java.util.List;

public class FugaException extends HogeFugaException {

	public FugaException(Throwable cause, List<ErrorEntry> errors) {
		super(cause, errors);
	}

	public FugaException(Throwable cause, ErrorEntry error) {
		super(cause, error);
	}

	public FugaException(List<ErrorEntry> errors) {
		super(errors);
	}

	public FugaException(ErrorEntry error) {
		super(error);
	}
}

```

なお、エラーの情報は、エラーコード、エラーメッセージを持つ以下のクラスで管理します。

```java:ErrorEntry.java
import lombok.Getter;

@Getter
public class ErrorEntry {

	private String id;
	private String message;

	private ErrorEntry() {
	}

	private ErrorEntry(String id, String message) {
		this.id = id;
		this.message = message;
	}

	public static ErrorEntry createErrorEntry(String id, String message) {
		return new ErrorEntry(id, message);
	}
}
```

## レスポンスクラス
ここでは、RESTAPIのレスポンスクラスを定義します。
正常時のクラスと、エラー時のクラスを定義しておきます。

```java:TestResponse.java
import lombok.Data;

@Data
public class TestResponse {
	long id;
	String name;
}
```

```java:ErrorResponse.java
import java.util.ArrayList;
import java.util.List;

import lombok.Data;
import lombok.Getter;

@Getter
public class ErrorResponse {
	List<ErrorInfo> errors;

	@Data
	class ErrorInfo {
		String messageId;
		String message;
	}

	private ErrorResponse(HogeFugaException e) {
		List<ErrorInfo> errors = new ArrayList<>();

		for (ErrorEntry entry : e.getErrors()) {
			ErrorInfo info = new ErrorInfo();
			info.setMessageId(entry.getId());
			info.setMessage(entry.getMessage());

			errors.add(info);
		}
		this.errors = errors;
	}

	private ErrorResponse(String id, String message) {
		List<ErrorInfo> errors = new ArrayList<>();

		ErrorInfo info = new ErrorInfo();
		info.setMessageId(id);
		info.setMessage(message);

		errors.add(info);
		this.errors = errors;
	}

	public static ErrorResponse createErrorResponse(HogeFugaException e) {

		return new ErrorResponse(e);
	}

	public static ErrorResponse createErrorResponse(String id, String message) {

		return new ErrorResponse(id, message);
	}
}
```

## コントローラークラス
次にコントローラークラスを定義します。
ここではあえて、例外が発生するような実装も作っています。

```java:TestRestController.java
import java.util.ArrayList;
import java.util.List;

import com.example.demo.exception.ErrorEntry;
import com.example.demo.exception.FugaException;
import com.example.demo.exception.HogeException;
import com.example.demo.model.response.TestResponse;

@RestController
public class TestRestController {

	@GetMapping("/test")
	public TestResponse test() {
		TestResponse obj = new TestResponse();
		obj.setId(1);
		obj.setName("test");
		return obj;
	}

	@GetMapping("/test1")
	public TestResponse error() {

		throw new HogeException(ErrorEntry.createErrorEntry("1234", "エラーテスト"));
	}

	@GetMapping("/test2")
	public TestResponse errors() {

		List<ErrorEntry> errors = new ArrayList<>();
		errors.add(ErrorEntry.createErrorEntry("1111", "エラーテスト"));
		errors.add(ErrorEntry.createErrorEntry("2222", "エラーメッセージ"));

		throw new FugaException(errors);
	}

	@GetMapping("/test3")
	public TestResponse unkownError() {
		throw new IllegalArgumentException();
	}
}
```

## エラー処理を行うクラス

```java:GlobalExceptionHandler.java
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class GlobalExceptionHandler {

    // HogeExceptionが発生した場合は、このメソッドで捕捉
	@ExceptionHandler({ HogeException.class })
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ResponseBody
	public ErrorResponse handleException(HogeException e, WebRequest req) {
		ErrorResponse response = ErrorResponse.createErrorResponse(e);
		return response;
	}

    // FugaExceptionが発生した場合は、このメソッドで捕捉
	@ExceptionHandler({ FugaException.class })
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ResponseBody
	public ErrorResponse handleException(FugaException e, WebRequest req) {
		ErrorResponse response = ErrorResponse.createErrorResponse(e);
		return response;
	}

    // HogeException、FugaException以外の例外が発生した場合は、このメソッドで捕捉
	@ExceptionHandler({ Exception.class })
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ResponseBody
	public ErrorResponse handleException(Exception e, WebRequest req) {
		ErrorResponse response = ErrorResponse.createErrorResponse("9999","想定外のエラー");
		return response;
	}
}
```

# 実行結果
無事、例外の捕捉ができました。

## 正常
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/f97b6253-77a6-33d9-1eed-6e22e765544d.png)

## HogeExceptionの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/81754e07-8708-3f62-2194-13e829e98cd3.png)

## FugaExceptionの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/766e6fb0-3430-75ce-5bf7-93bd654e6913.png)

## それ以外の場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/132118/d77ec972-520b-bdde-ea91-a5053e2de655.png)

# おわりに
今回はControllerAdviceとExceptionHandlerで実装を行いましたが、ExceptionHandlerで、Exceptionクラスを捕捉する方法以外に、HandlerExceptionResolverインタフェースをインプリメントする方法もあるようです。

どちらがいいかはわかりませんが、ErrorとかThrowableとかを考えると、HandlerExceptionResolverインタフェースをインプリメントしたほうがいいかもしれません（機会があれば試してみます）