LoginSignup
18
19

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-09

各クラスで一々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

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

@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

Webアプリケーション全体の例外処理

  • コントローラークラスが多かったりすると、コード量が増えて大変
  • →1つのクラスでアプリケーション全体の例外処理を実装するには、@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

(参考)コード全文

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";
    }
}

18
19
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
19