#34 例外処理の共通化

# #34 例外処理の共通化
Springでは例外処理を共通化できる仕組みがいくつかあります。例外処理を共通化することで開発者は本質的な処理の実装に集中することができます。

# 前提条件
この記事はSpringの最低限の知識が必要になります。
また、なるべく分かりやすく書くつもりですが、この記事の目的は自分の勉強のアウトプットであるため所々説明は省略します。

# 構築環境
1. 各バージョン
Spring Boot ver 2.7.5
mybatis-spring-boot-starter ver 2.2.2
Model Mapper ver 3.1.0
jquery ver 3.6.1
bootstrap ver 5.2.2
webjars-locator ver 0.46
thymeleaf-layout-dialect ver 3.0.0

2. 依存関係
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/58b53ccd-7cf2-2d82-4214-9f0c08fced2e.png)

# 成果物

# 今回行うこと
今回は以下の流れに沿って進めていきます。
1. @AfterThrowingアスペクトでの例外処理
2. コントロラークラス毎の例外処理
3. Webアプリケーション全体の例外処理

## 1. @AfterThrowingアスペクトでの例外処理
@AfterThrowingアスペクトを使用して、エラー発生時にログを出力します。
``DataAccessException``が発生した場合、「DataAccessExceptionが発生しました」とログを出力します。

まずは、src/main/java/com/example/aspectの直下に``ErrorAspect.java``を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/b050f11b-21f3-1f94-0290-c3356788b485.png)

記述する内容は以下になります。
AOPを実装するためには@Aspect, @Componentアノテーションをクラスに付けます。
```java:ErrorAspect.java
package com.example.aspect;

import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Slf4j
public class ErrorAspect {

	@AfterThrowing(value="execution(* *..*..*(..)) &&"
			+ "(bean(*Controller) || bean(*Service) || bean(*Repository))",
			throwing ="ex")
	public void throwingNull(DataAccessException ex) {
		
		// 例外処理の内容（ログ出力）
		log.error("DataAccessExceptionが発生しました");
	}
}
```
<br>

@AfterThrowingアノテーションを付けることで例外発生時(異常終了時)のAOPを実装することができます。
throwing属性には、例外クラスのメソッド引数を指定します(``throwing="ex"``)。
value属性にはAOPを実行するPointCutを指定しています。
- ``execution(* *..*..*(..))``：アプリ内の全メソッド対象
- ``(bean(*Controller) || bean(*Service) || bean(*Repository))``：DIコンテナーに登録されているクラス(@Controllerや@Serviceなどのアノテーションが付けられているクラス)でクラス名がController, Service, Repositoryで終わるクラス

上記の2つの条件を両方満たすメソッドが例外発生した場合ログを出力します。
```java
	@AfterThrowing(value="execution(* *..*..*(..)) &&"
			+ "(bean(*Controller) || bean(*Service) || bean(*Repository))",
			throwing ="ex")
```
<br>

一意制約違反で例外を発生させた場合、以下のようなエラーがログとして出力されます。
```console
2022-12-24 11:21:17.751 ERROR 18108 --- [nio-8080-exec-4] com.example.aspect.ErrorAspect           : DataAccessExceptionが発生しました
```

## 2. コントロラークラス毎の例外処理
ここではコントローラーで予期せぬ例外が発生した場合の例外処理について学んで行きます。
```java:UserSignupController.java
package com.example.controller;

import java.util.Locale;
import java.util.Map;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.application.service.UserApplicationService;
import com.example.domain.user.model.MUser;
import com.example.domain.user.service.UserService;
import com.example.form.GroupOrder;
import com.example.form.SignupForm;

import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("/user")
@Slf4j
public class SignupController {
	
	@Autowired
	private UserApplicationService userApplicationService;
	
	@Autowired
	private UserService userService;
	
	@Autowired
	private ModelMapper modelMapper;
	
	// ユーザー登録画面を表示
	@GetMapping("/signup")
    // 省略
	}
	
	// ユーザー登録処理
	@PostMapping("/signup")
    // 省略
	}
	
	/* データベース関連の例外処理 */
	@ExceptionHandler(DataAccessException.class)
	public String dataAccessExceptionHandler(DataAccessException e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error");
		
		// メッセージをModelに登録
		model.addAttribute("message", "signupControllerで例外は発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
	}
	
	/* その他の例外処理 */
	@ExceptionHandler(Exception.class)
	public String exeptionHandler(Exception e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error", "");
		
		// メッセージをModelに登録
		model.addAttribute("message", "SignupControllerで例外が発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
	}
}
```
<br>

@ExceptionHandlerアノテーションを付けたメソッドを用意すると、例外処理を実装できます。
アノテーションの引数に例外クラスを指定することで、例外毎の処理を実装できます。また、@ExceptionHandlerを付けたメソッドは複数用意することができます。

```java
	/* データベース関連の例外処理 */
	@ExceptionHandler(DataAccessException.class)
	public String dataAccessExceptionHandler(DataAccessException e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error", "");
		
		// メッセージをModelに登録
		model.addAttribute("message", "signupControllerで例外は発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
	}
}

	/* その他の例外処理 */
	@ExceptionHandler(Exception.class)
	public String exeptionHandler(Exception e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error", "");
		
		// メッセージをModelに登録
		model.addAttribute("message", "SignupControllerで例外が発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
		
	}
}
```
<br>

実際に一意制約違反で例外を発生させると以下のような画面が表示されます。
- ${status}：500 INTEERNAL_SERVER_ERROR
- ${error}：""
- ${message}：SignupControllerで例外が発生しました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/07767417-1eda-e282-361e-726b15bbc108.png)

ただし、UserSignupController.javaに関係ない部分で発生してしまった例外は下記のように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/ea08cc40-a7fb-6a9b-cece-d27ae19e3ff5.png)

## 3. Webアプリケーション全体の例外処理
先ほどはコントローラー毎に例外処理を作成しました。画面毎に例外処理を用意するば各画面に適切な処理を表示することができますが、例外処理の実装を忘れてしまう可能性もあるため、Webアプリケーション全体の例外処理を記述していきます。

@ControllerAdviceをクラスに付けることで全てのコントローラーで共有するメソッドを用意することができます。
ただし、@ExceptionHandle, @InitBinder, @ModelAttributeのいずれかのアノテーションがついたメソッドのみ、コントローラー間で共有することができます。
```java:GlobalControllerAdvice.java
package com.example.aspect;

import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalControllAdvice {

	/* データベース関連の例外処理 */
	@ExceptionHandler(DataAccessException.class)
	public String dataAccessExceptionHandler(DataAccessException e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error", "");
		
		// メッセージをModelに登録
		model.addAttribute("message", "DataAccessExceptionで例外は発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
	}
	
	/* その他の例外処理 */
	@ExceptionHandler(Exception.class)
	public String exeptionHandler(Exception e, Model model) {
		
		// 空文字をセット
		model.addAttribute("error", "");
		
		// メッセージをModelに登録
		model.addAttribute("message", "SignupControllerで例外が発生しました");
		
		// HTTPのエラーコード(500)をModelに登録
		model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);
		
		return "error";
		
	}
} 
```

先ほど``2. コントロラークラス毎の例外処理``で記述したただし、UserSignupController.javaに関係ない部分で発生してしまった例外は下記のように表示されていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/ea08cc40-a7fb-6a9b-cece-d27ae19e3ff5.png)

GlobalControllAdvice.javaを実装後は以下のように変わりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884987/797a4e51-ed88-a6c6-0e51-559d570006e4.png)

# 最後に
以上で例外処理を終了します。