使用枚舉簡(jiǎn)單封裝一個(gè)優(yōu)雅的 Spring Boot 全局異常處理!
這篇文章鴿了很久,我在這篇文章 《用好 Java 中的枚舉,真的沒(méi)有那么簡(jiǎn)單!》 中就提到要分享。還是昨天一個(gè)讀者提醒我之后,我才發(fā)現(xiàn)自己沒(méi)有將這篇文章發(fā)到公眾號(hào)。說(shuō)到這里,我發(fā)現(xiàn)自己一個(gè)很大的問(wèn)題,就是有時(shí)候在文章里面說(shuō)要更新什么,結(jié)果后面就忘記了,很多時(shí)候不是自己沒(méi)寫(xiě),就因?yàn)楦鞣N事情混雜導(dǎo)致忘記發(fā)了。以后要盡量改正這個(gè)問(wèn)題!
在上一篇文章《SpringBoot 處理異常的幾種常見(jiàn)姿勢(shì)》中我介紹了:
- 使用 @ControllerAdvice 和 @ExceptionHandler 處理全局異常
- @ExceptionHandler 處理 Controller 級(jí)別的異常
- ResponseStatusException
通過(guò)這篇文章,可以搞懂如何在 Spring Boot 中進(jìn)行異常處理。但是,光是會(huì)用了還不行,我們還要思考如何把異常處理這部分的代碼寫(xiě)的稍微優(yōu)雅一點(diǎn)。下面我會(huì)以我在工作中學(xué)到的一點(diǎn)實(shí)際項(xiàng)目中異常處理的方式,來(lái)說(shuō)說(shuō)我覺(jué)得稍微優(yōu)雅點(diǎn)的異常處理解決方案。
下面僅僅是我作為一個(gè)我個(gè)人的角度來(lái)看的,如果各位讀者有更好的解決方案或者覺(jué)得本文提出的方案還有優(yōu)化的余地的話(huà),歡迎在評(píng)論區(qū)評(píng)論。
最終效果展示
下面先來(lái)展示一下完成后的效果,當(dāng)我們定義的異常被系統(tǒng)捕捉后返回給客戶(hù)端的信息是這樣的:
效果展示
返回的信息包含了異常下面 5 部分內(nèi)容:
- 唯一標(biāo)示異常的 code
- HTTP 狀態(tài)碼
- 錯(cuò)誤路徑
- 發(fā)生錯(cuò)誤的時(shí)間戳
- 錯(cuò)誤的具體信息
這樣返回異常信息,更利于我們前端根據(jù)異常信息做出相應(yīng)的表現(xiàn)。
異常處理核心代碼
ErrorCode.java (此枚舉類(lèi)中包含了異常的唯一標(biāo)識(shí)、HTTP 狀態(tài)碼以及錯(cuò)誤信息)
這個(gè)類(lèi)的主要作用就是統(tǒng)一管理系統(tǒng)中可能出現(xiàn)的異常,比較清晰明了。但是,可能出現(xiàn)的問(wèn)題是當(dāng)系統(tǒng)過(guò)于復(fù)雜,出現(xiàn)的異常過(guò)多之后,這個(gè)類(lèi)會(huì)比較龐大。有一種解決辦法:將多種相似的異常統(tǒng)一為一個(gè),比如將用戶(hù)找不到異常和訂單信息未找到的異常都統(tǒng)一為“未找到該資源”這一種異常,然后前端再對(duì)相應(yīng)的情況做詳細(xì)處理(我個(gè)人的一種處理方法,不敢保證是比較好的一種做法)。
- import org.springframework.http.HttpStatus;
- public enum ErrorCode {
- RESOURCE_NOT_FOUND(1001, HttpStatus.NOT_FOUND, "未找到該資源"),
- REQUEST_VALIDATION_FAILED(1002, HttpStatus.BAD_REQUEST, "請(qǐng)求數(shù)據(jù)格式驗(yàn)證失敗");
- 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(返回給客戶(hù)端具體的異常對(duì)象)
這個(gè)類(lèi)作為異常信息返回給客戶(hù)端,里面包括了當(dāng)出現(xiàn)異常時(shí)我們想要返回給客戶(hù)端的所有信息。
- 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 的抽象類(lèi),可以看做系統(tǒng)中其他異常類(lèi)的父類(lèi))
系統(tǒng)中的異常類(lèi)都要繼承自這個(gè)類(lèi)。
- 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 (自定義異常)
可以看出通過(guò)繼承 BaseException 類(lèi)我們自定義異常會(huì)變的非常簡(jiǎn)單!
- import java.util.Map;
- public class ResourceNotFoundException extends BaseException {
- public ResourceNotFoundException(Map<String, Object> data) {
- super(ErrorCode.RESOURCE_NOT_FOUND, data);
- }
- }
GlobalExceptionHandler.java(全局異常捕獲)
我們定義了兩個(gè)異常捕獲方法。
這里再說(shuō)明一下,實(shí)際上這個(gè)類(lèi)只需要 handleAppException() 這一個(gè)方法就夠了,因?yàn)樗潜鞠到y(tǒng)所有異常的父類(lèi)。只要是拋出了繼承 BaseException 類(lèi)的異常后都會(huì)在這里被處理。
- 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
- // 因?yàn)?nbsp;RuntimeException 是 BaseException 的父類(lèi)
- @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);
- }
- }
(重要)一點(diǎn)擴(kuò)展:
哈哈!實(shí)際上我多加了一個(gè)算是多余的異常捕獲方法handleResourceNotFoundException() 主要是為了考考大家當(dāng)我們拋出了 ResourceNotFoundException異常會(huì)被下面哪一個(gè)方法捕獲呢?
答案:
會(huì)被handleResourceNotFoundException()方法捕獲。因?yàn)?@ExceptionHandler 捕獲異常的過(guò)程中,會(huì)優(yōu)先找到最匹配的。
下面通過(guò)源碼簡(jiǎn)單分析一下:
ExceptionHandlerMethodResolver.java中g(shù)etMappedMethod決定了具體被哪個(gè)方法處理。
- @Nullable
- private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
- List<Class<? extends Throwable>> matches = new ArrayList<>();
- //找到可以處理的所有異常信息。mappedMethods 中存放了異常和處理異常的方法的對(duì)應(yīng)關(guān)系
- for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
- if (mappedException.isAssignableFrom(exceptionType)) {
- matches.add(mappedException);
- }
- }
- // 不為空說(shuō)明有方法處理異常
- if (!matches.isEmpty()) {
- // 按照匹配程度從小到大排序
- matches.sort(new ExceptionDepthComparator(exceptionType));
- // 返回處理異常的方法
- return this.mappedMethods.get(matches.get(0));
- }
- else {
- return null;
- }
- }
從源代碼看出:getMappedMethod()會(huì)首先找到可以匹配處理異常的所有方法信息,然后對(duì)其進(jìn)行從小到大的排序,最后取最小的那一個(gè)匹配的方法(即匹配度最高的那個(gè))。
寫(xiě)一個(gè)拋出異常的類(lèi)測(cè)試
Person.java
- public class Person {
- private Long id;
- private String name;
- // 省略 getter/setter 方法
- }
ExceptionController.java(拋出一場(chǎng)的類(lèi))
- @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