使用枚舉簡單封裝一個優雅的 Spring Boot 全局異常處理!
這篇文章鴿了很久,我在這篇文章 《用好 Java 中的枚舉,真的沒有那么簡單!》 中就提到要分享。還是昨天一個讀者提醒我之后,我才發現自己沒有將這篇文章發到公眾號。說到這里,我發現自己一個很大的問題,就是有時候在文章里面說要更新什么,結果后面就忘記了,很多時候不是自己沒寫,就因為各種事情混雜導致忘記發了。以后要盡量改正這個問題!
在上一篇文章《SpringBoot 處理異常的幾種常見姿勢》中我介紹了:
- 使用 @ControllerAdvice 和 @ExceptionHandler 處理全局異常
- @ExceptionHandler 處理 Controller 級別的異常
- ResponseStatusException
通過這篇文章,可以搞懂如何在 Spring Boot 中進行異常處理。但是,光是會用了還不行,我們還要思考如何把異常處理這部分的代碼寫的稍微優雅一點。下面我會以我在工作中學到的一點實際項目中異常處理的方式,來說說我覺得稍微優雅點的異常處理解決方案。
下面僅僅是我作為一個我個人的角度來看的,如果各位讀者有更好的解決方案或者覺得本文提出的方案還有優化的余地的話,歡迎在評論區評論。
最終效果展示
下面先來展示一下完成后的效果,當我們定義的異常被系統捕捉后返回給客戶端的信息是這樣的:
效果展示
返回的信息包含了異常下面 5 部分內容:
- 唯一標示異常的 code
- HTTP 狀態碼
- 錯誤路徑
- 發生錯誤的時間戳
- 錯誤的具體信息
這樣返回異常信息,更利于我們前端根據異常信息做出相應的表現。
異常處理核心代碼
ErrorCode.java (此枚舉類中包含了異常的唯一標識、HTTP 狀態碼以及錯誤信息)
這個類的主要作用就是統一管理系統中可能出現的異常,比較清晰明了。但是,可能出現的問題是當系統過于復雜,出現的異常過多之后,這個類會比較龐大。有一種解決辦法:將多種相似的異常統一為一個,比如將用戶找不到異常和訂單信息未找到的異常都統一為“未找到該資源”這一種異常,然后前端再對相應的情況做詳細處理(我個人的一種處理方法,不敢保證是比較好的一種做法)。
- import org.springframework.http.HttpStatus;
- public enum ErrorCode {
- RESOURCE_NOT_FOUND(1001, HttpStatus.NOT_FOUND, "未找到該資源"),
- REQUEST_VALIDATION_FAILED(1002, HttpStatus.BAD_REQUEST, "請求數據格式驗證失敗");
- private final int code;
- private final HttpStatus status;
- private final String message;
- ErrorCode(int code, HttpStatus status, String message) {
- this.code = code;
- this.status = status;
- this.message = message;
- }
- public int getCode() {
- return code;
- }
- public HttpStatus getStatus() {
- return status;
- }
- public String getMessage() {
- return message;
- }
- @Override
- public String toString() {
- return "ErrorCode{" +
- "code=" + code +
- ", status=" + status +
- ", message='" + message + '\'' +
- '}';
- }
- }
ErrorReponse.java(返回給客戶端具體的異常對象)
這個類作為異常信息返回給客戶端,里面包括了當出現異常時我們想要返回給客戶端的所有信息。
- import org.springframework.util.ObjectUtils;
- import java.time.Instant;
- import java.util.HashMap;
- import java.util.Map;
- public class ErrorReponse {
- private int code;
- private int status;
- private String message;
- private String path;
- private Instant timestamp;
- private HashMap<String, Object> data = new HashMap<String, Object>();
- public ErrorReponse() {
- }
- public ErrorReponse(BaseException ex, String path) {
- this(ex.getError().getCode(), ex.getError().getStatus().value(), ex.getError().getMessage(), path, ex.getData());
- }
- public ErrorReponse(int code, int status, String message, String path, Map<String, Object> data) {
- this.code = code;
- this.status = status;
- this.message = message;
- this.path = path;
- this.timestamp = Instant.now();
- if (!ObjectUtils.isEmpty(data)) {
- this.data.putAll(data);
- }
- }
- // 省略 getter/setter 方法
- @Override
- public String toString() {
- return "ErrorReponse{" +
- "code=" + code +
- ", status=" + status +
- ", message='" + message + '\'' +
- ", path='" + path + '\'' +
- ", timestamp=" + timestamp +
- ", data=" + data +
- '}';
- }
- }
BaseException.java(繼承自 RuntimeException 的抽象類,可以看做系統中其他異常類的父類)
系統中的異常類都要繼承自這個類。
- public abstract class BaseException extends RuntimeException {
- private final ErrorCode error;
- private final HashMap<String, Object> data = new HashMap<>();
- public BaseException(ErrorCode error, Map<String, Object> data) {
- super(error.getMessage());
- this.error = error;
- if (!ObjectUtils.isEmpty(data)) {
- this.data.putAll(data);
- }
- }
- protected BaseException(ErrorCode error, Map<String, Object> data, Throwable cause) {
- super(error.getMessage(), cause);
- this.error = error;
- if (!ObjectUtils.isEmpty(data)) {
- this.data.putAll(data);
- }
- }
- public ErrorCode getError() {
- return error;
- }
- public Map<String, Object> getData() {
- return data;
- }
- }
ResourceNotFoundException.java (自定義異常)
可以看出通過繼承 BaseException 類我們自定義異常會變的非常簡單!
- import java.util.Map;
- public class ResourceNotFoundException extends BaseException {
- public ResourceNotFoundException(Map<String, Object> data) {
- super(ErrorCode.RESOURCE_NOT_FOUND, data);
- }
- }
GlobalExceptionHandler.java(全局異常捕獲)
我們定義了兩個異常捕獲方法。
這里再說明一下,實際上這個類只需要 handleAppException() 這一個方法就夠了,因為它是本系統所有異常的父類。只要是拋出了繼承 BaseException 類的異常后都會在這里被處理。
- import com.twuc.webApp.web.ExceptionController;
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.ResponseEntity;
- import org.springframework.web.bind.annotation.ControllerAdvice;
- import org.springframework.web.bind.annotation.ExceptionHandler;
- import org.springframework.web.bind.annotation.ResponseBody;
- import javax.servlet.http.HttpServletRequest;
- @ControllerAdvice(assignableTypes = {ExceptionController.class})
- @ResponseBody
- public class GlobalExceptionHandler {
- // 也可以將 BaseException 換為 RuntimeException
- // 因為 RuntimeException 是 BaseException 的父類
- @ExceptionHandler(BaseException.class)
- public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
- ErrorReponse representation = new ErrorReponse(ex, request.getRequestURI());
- return new ResponseEntity<>(representation, new HttpHeaders(), ex.getError().getStatus());
- }
- @ExceptionHandler(value = ResourceNotFoundException.class)
- public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
- ErrorReponse errorReponse = new ErrorReponse(ex, request.getRequestURI());
- return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorReponse);
- }
- }
(重要)一點擴展:
哈哈!實際上我多加了一個算是多余的異常捕獲方法handleResourceNotFoundException() 主要是為了考考大家當我們拋出了 ResourceNotFoundException異常會被下面哪一個方法捕獲呢?
答案:
會被handleResourceNotFoundException()方法捕獲。因為 @ExceptionHandler 捕獲異常的過程中,會優先找到最匹配的。
下面通過源碼簡單分析一下:
ExceptionHandlerMethodResolver.java中getMappedMethod決定了具體被哪個方法處理。
- @Nullable
- private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
- List<Class<? extends Throwable>> matches = new ArrayList<>();
- //找到可以處理的所有異常信息。mappedMethods 中存放了異常和處理異常的方法的對應關系
- for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
- if (mappedException.isAssignableFrom(exceptionType)) {
- matches.add(mappedException);
- }
- }
- // 不為空說明有方法處理異常
- if (!matches.isEmpty()) {
- // 按照匹配程度從小到大排序
- matches.sort(new ExceptionDepthComparator(exceptionType));
- // 返回處理異常的方法
- return this.mappedMethods.get(matches.get(0));
- }
- else {
- return null;
- }
- }
從源代碼看出:getMappedMethod()會首先找到可以匹配處理異常的所有方法信息,然后對其進行從小到大的排序,最后取最小的那一個匹配的方法(即匹配度最高的那個)。
寫一個拋出異常的類測試
Person.java
- public class Person {
- private Long id;
- private String name;
- // 省略 getter/setter 方法
- }
ExceptionController.java(拋出一場的類)
- @RestController
- @RequestMapping("/api")
- public class ExceptionController {
- @GetMapping("/resourceNotFound")
- public void throwException() {
- Person p=new Person(1L,"SnailClimb");
- throw new ResourceNotFoundException(ImmutableMap.of("person id:", p.getId()));
- }
- }
源碼地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-handle-exception-improved