【Java・SpringBoot】Springで例外処理を共通化

各クラスで一々try～catchを実装するのは大変なので、Springで**例外処理を共通化**したい！
そこで、**共通エラー/それぞれのHTTPエラーに対応するエラーページ**、**コントローラークラス毎の例外処理/Webアプリケーション全体の例外処理方法**について学びます。

#Springでの例外処理
* @AfterThrowingアスペクトを使用
* コントローラークラス毎に例外ハンドリングを実装
* Webアプリケーション全体で共通の例外ハンドリングを実装

##エラーページの種類
* **共通のエラーページ**
    * **WhiteLabelページの代わりに、共通エラーページが表示**される
* **HTTPエラーに対応するエラーページ**
    * 404エラーや500エラーなどによって、画面に表示するメッセージを変えることができる

#共通のエラーページ
* **`src/main/resources/templates`配下に、error.htmlを用意すればOK！**

###エラー内容の表示
* Modelから以下のキーを指定すると、**Springが自動でキーに値を入れてくれて**、エラー内容の詳細を取得できる。
* **${status}**：HTTPのエラーコードを格納
* **${error}**：HTTPのエラー概要が表示
* **${message}**：エラーメッセージを表示

```error.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <title>Error</title>
</head>
<body>
    <!-- エラー内容を表示 -->
    <h1 th:text="${status} + ' ' + ${error}"></h1>
    <p th:text="${message}"></p>
    <p>ログイン画面に戻ってください</p>
    <form method="post" th:action="@{/logout}">
        <button class="btn btn-link" type="submit">ログイン画面に戻る</button>
    </form>
</body>
</html>
```

##SpringBootを起動してユーザー登録画面を確認！
* http://localhost:8080/signup
* ユーザー登録画面で、ユーザーIDにdata.sqlで初期登録されているユーザーIDを入力
* 主キー違反となってエラーが起き、"ログイン画面に戻る"ボタンが表示されました！^^

![スクリーンショット 2020-12-06 21.11.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/094fcdea-fbfd-e9dd-aa6d-42f5584fbb32.png)

#HTTPエラーに対応するエラーページ
* **`src/main/resources/templates/error`配下に、各HTTPエラー専用のhtmlを作成**すればOK！
* 例えば404エラーなら、404.html

```404.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <title>404Error</title>
</head>
<body>
    <h1 th:text="${status} + ' ' + ${error}"></h1>
    <p>404エラーの専用ページです</p>
    <p>ログイン画面に戻ってください</p>
    <form method="post" th:action="@{/logout}">
        <button class="btn btn-link" type="submit">ログイン画面に戻る</button>
    </form>
</body>
</html>
```

##SpringBootを起動して、存在しないページにリクエスト！
* http://localhost:8080/a
* 404エラーが起き、"ログイン画面に戻る"ボタンが表示されました！^^

![スクリーンショット 2020-12-06 21.16.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/14246ef4-e03b-af9d-08cd-1f8899acd7f4.png)


#@AfterThrowingアスペクトでの例外処理
* アスペクト処理で、DataAccessException発生時のログ出力を実装

```ErrorAspect.java
package com.example.demo.login.aspect;

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

@Aspect
@Component
public class ErrorAspect {

    //全てのクラスを対象
    @AfterThrowing(value="execution(* *..*..*(..))"
            + " && (bean(*Controller) || bean(*Service) || bean(*Repository))"
            , throwing="ex")
    public void throwingNull(DataAccessException ex) {

        //例外処理の内容（ログ出力）
        System.out.println("===========================================");
        System.out.println("DataAccessExceptionが発生しました。 : " + ex);
        System.out.println("===========================================");

    }

}
```

##SpringBootを起動、ユーザー登録で主キー違反エラーを起こす
* http://localhost:8080/signup
* ユーザー登録画面で、主キー違反エラーを起こすと、
* コンソール画面に以下のようにエラーログを出力できます^^

```
===========================================
DataAccessExceptionが発生しました。 : org.springframework.dao.DuplicateKeyException: PreparedStatementCallback; SQL [INSERT INTO m_user(user_id, password, user_name, birthday, age, marriage, role) VALUES(?, ?, ?, ?, ?, ?, ?)ユニークインデックス、またはプライマリキー違反: "PRIMARY_KEY_8 ON PUBLIC.M_USER(USER_ID) VALUES ('yamada@xxx.co.jp', 1)"
Unique index or primary key violation: "PRIMARY_KEY_8 ON PUBLIC.M_USER(USER_ID) VALUES ('yamada@xxx.co.jp', 1)"; SQL statement:
INSERT INTO m_user(user_id, password, user_name, birthday, age, marriage, role) VALUES(?, ?, ?, ?, ?, ?, ?) [23505-197]; nested exception is org.h2.jdbc.JdbcSQLException: ユニークインデックス、またはプライマリキー違反: "PRIMARY_KEY_8 ON PUBLIC.M_USER(USER_ID) VALUES ('yamada@xxx.co.jp', 1)"
Unique index or primary key violation: "PRIMARY_KEY_8 ON PUBLIC.M_USER(USER_ID) VALUES ('yamada@xxx.co.jp', 1)"; SQL statement:
INSERT INTO m_user(user_id, password, user_name, birthday, age, marriage, role) VALUES(?, ?, ?, ?, ?, ?, ?) [23505-197]
===========================================
```

#コントローラークラス毎の例外処理
* コントローラークラス内に、**@ExceptionHandlerというアノテーションを付けたメソッドを作成**
    * ユーザー登録画面がエラーを起こしやすいので、ユーザー登録画面のコントローラークラスに実装

###@ExceptionHandler
* **引数に例外クラスを指定**することで、Exception毎の例外処理を実装することができる
    * `@ExceptionHandler(Exception.class)`
    * メソッドは複数用意できる
    * 以下では共通エラーページに遷移し、エラーメッセージをModelクラスに登録
   

```SignupController.java

//中略(全文は下記参考)

@Controller
public class SignupController {

    @Autowired
    private UserService userService;

//中略(全文は下記参考)
    /**
     * DataAccessException発生時の処理メソッド.
     */
    @ExceptionHandler(DataAccessException.class)
    public String dataAccessExceptionHandler(DataAccessException e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー（DB）：ExceptionHandler");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "SignupControllerでDataAccessExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }

    /**
     * Exception発生時の処理メソッド.
     */
    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー：ExceptionHandler");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "SignupControllerでExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }
}
```


##SpringBootを起動、ユーザー登録で主キー違反エラーを起こす
* http://localhost:8080/signup
* ユーザー登録画面で、主キー違反エラーを起こすと、"SignupControllerでDataAccessExceptionが発生しました"
* と表示できました！^^

![スクリーンショット 2020-12-06 21.34.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/d64bf10a-3d0c-7323-b077-83fa760ebd8a.png)

#Webアプリケーション全体の例外処理
* コントローラークラスが多かったりすると、コード量が増えて大変
* →１つのクラスでアプリケーション全体の例外処理を実装するには、**@ControllerAdviceアノテーションを付けたクラスを用意**すればOK

###@ControllerAdvice
* @ControllerAdviceクラスを用意し、アプリケーション全体で発生した例外処理を実装
* クラスの中身は@ExceptionHandlerを付けたメソッドを用意するだけ

```GlobalControllAdvice.java
//中略(全文は下記参考)

@ControllerAdvice
@Component
public class GlobalControllAdvice {

    @ExceptionHandler(DataAccessException.class)
    public String dataAccessExceptionHandler(DataAccessException e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー（DB）：GlobalControllAdvice");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "DataAccessExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }

    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー：GlobalControllAdvice");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "Exceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }
}

```

##ユーザー詳細画面に直接遷移
* http://localhost:8080/userDetail
* ユーザー詳細画面のURLにユーザーIDを含めなかったので、GlobalControllAdviceクラスのエラー処理が実行されました^^

![スクリーンショット 2020-12-06 21.40.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/282755/230888e6-0a79-728a-bd79-75d6e0458d36.png)





#(参考)コード全文

```SignupController.java
package com.example.demo.login.controller;

import java.util.LinkedHashMap;
import java.util.Map;

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 com.example.demo.login.domain.model.GroupOrder;
import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;

@Controller
public class SignupController {

    @Autowired
    private UserService userService;

    //ラジオボタン用変数
    private Map<String, String> radioMarriage;

    /**
     * ラジオボタンの初期化メソッド.
     */
    private Map<String, String> initRadioMarrige() {

        Map<String, String> radio = new LinkedHashMap<>();

        // 既婚、未婚をMapに格納
        radio.put("既婚", "true");
        radio.put("未婚", "false");

        return radio;
    }

    /**
     * ユーザー登録画面のGETメソッド用処理.
     */
    @GetMapping("/signup")
    public String getSignUp(@ModelAttribute SignupForm form, Model model) {

        // ラジオボタンの初期化メソッド呼び出し
        radioMarriage = initRadioMarrige();

        // ラジオボタン用のMapをModelに登録
        model.addAttribute("radioMarriage", radioMarriage);

        // signup.htmlに画面遷移
        return "login/signup";
    }

    /**
     * ユーザー登録画面のPOSTメソッド用処理.
     */
    @PostMapping("/signup")
    public String postSignUp(@ModelAttribute @Validated(GroupOrder.class) SignupForm form,
            BindingResult bindingResult,
            Model model) {

        // 入力チェックに引っかかった場合、ユーザー登録画面に戻る
        if (bindingResult.hasErrors()) {

            // GETリクエスト用のメソッドを呼び出して、ユーザー登録画面に戻ります
            return getSignUp(form, model);

        }

        // formの中身をコンソールに出して確認します
        System.out.println(form);

        // insert用変数
        User user = new User();

        user.setUserId(form.getUserId()); //ユーザーID
        user.setPassword(form.getPassword()); //パスワード
        user.setUserName(form.getUserName()); //ユーザー名
        user.setBirthday(form.getBirthday()); //誕生日
        user.setAge(form.getAge()); //年齢
        user.setMarriage(form.isMarriage()); //結婚ステータス
        user.setRole("ROLE_GENERAL"); //ロール（一般）

        // ユーザー登録処理
        boolean result = userService.insert(user);

        // ユーザー登録結果の判定
        if (result == true) {
            System.out.println("insert成功");
        } else {
            System.out.println("insert失敗");
        }

        // login.htmlにリダイレクト
        return "redirect:/login";
    }

    /**
     * DataAccessException発生時の処理メソッド.
     */
    @ExceptionHandler(DataAccessException.class)
    public String dataAccessExceptionHandler(DataAccessException e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー（DB）：ExceptionHandler");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "SignupControllerでDataAccessExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }

    /**
     * Exception発生時の処理メソッド.
     */
    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー：ExceptionHandler");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "SignupControllerでExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }
}
```



```GlobalControllAdvice.java
package com.example.demo.login.controller;

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

@ControllerAdvice
@Component
public class GlobalControllAdvice {

    @ExceptionHandler(DataAccessException.class)
    public String dataAccessExceptionHandler(DataAccessException e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー（DB）：GlobalControllAdvice");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "DataAccessExceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }

    @ExceptionHandler(Exception.class)
    public String exceptionHandler(Exception e, Model model) {

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("error", "内部サーバーエラー：GlobalControllAdvice");

        // 例外クラスのメッセージをModelに登録
        model.addAttribute("message", "Exceptionが発生しました");

        // HTTPのエラーコード（500）をModelに登録
        model.addAttribute("status", HttpStatus.INTERNAL_SERVER_ERROR);

        return "error";
    }
}

```