各クラスで一々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を入力
- 主キー違反となってエラーが起き、"ログイン画面に戻る"ボタンが表示されました!^^
#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エラーが起き、"ログイン画面に戻る"ボタンが表示されました!^^
#@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というアノテーションを付けたメソッドを作成
- ユーザー登録画面がエラーを起こしやすいので、ユーザー登録画面のコントローラークラスに実装
-
引数に例外クラスを指定することで、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が発生しました"
- と表示できました!^^
#Webアプリケーション全体の例外処理
- コントローラークラスが多かったりすると、コード量が増えて大変
- →1つのクラスでアプリケーション全体の例外処理を実装するには、@ControllerAdviceアノテーションを付けたクラスを用意すればOK
- @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クラスのエラー処理が実行されました^^
#(参考)コード全文
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";
}
}