Spring中優雅的處理全局異常

一.前言

​ hello,everyone,週末愉快。雙休日你們都去幹嗎了?看MSI?看速9?刷劇?出去喝酒蹦迪?野炊春遊。。。all right~假期老是過得很快,週末在家刷了絕命毒師,不愧是每一季豆瓣頻分9+的神劇,全程無尿點,推薦你們觀看。html

​ 言歸正傳,玩歸玩,鬧歸鬧,不能拿bug開玩笑。平常工做編寫代碼的過程當中,隨手留下bug那是程序員再正常不過的事情了。程序出現了bug,總會有對應的日誌信息產生,後端拋出的堆棧錯誤,不可能直接拋到前端。試想,用戶搜索一件不存在的商品時,後端代碼有bug【正常業務代碼這裏仍是會去校驗一下商品是否存在的】,報了空指針異常,這是不作任何錯誤包裝,直接將空指針異常的堆棧信息返回給用戶。這下好了,領導不請你喝杯茶說不過去了。前端

​ 那麼咱們該怎麼來處理這些個拋異常的問題呢?本文就將給你們帶來spring中如何優雅定製全局異常,若是本文寫的有不對或者你們以爲有更好的方式,歡迎留言指正,salute!java

src=http___p4.itc.cn_q_70_images03_20201206_dea1b9f6b81c4eda9518c39e91ffd346.png&refer=http___p4.itc.jpeg

二.異常

既然要談一談全局異常處理,那咱們先要知道java中的異常體系。git

2.png

說明程序員

1.Throwablegithub

全部的異常都是Throwable的直接或者間接子類。Throwable有兩個直接子類,Error和Exception。spring

2.Errorjson

Error是錯誤,對於全部的編譯時期的錯誤以及系統錯誤都是經過Error拋出的。這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如Java虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,由於它們在應用程序的控制和處理能力之 外,並且絕大多數是程序運行時不容許出現的情況。對於設計合理的應用程序來講,即便確實發生了錯誤,本質上也不該該試圖去處理它所引發的異常情況。在 Java中,錯誤經過Error的子類描述。後端

3.Exception微信

它規定的異常是程序自己能夠處理的異常。異常和錯誤的區別是,異常是能夠被處理的,而錯誤是無法處理的。

4.Checked Exception【受檢異常】

可檢查的異常,這是編碼時很是經常使用的,全部checked exception都是須要在代碼中處理的。它們的發生是能夠預測的,正常的一種狀況,能夠合理的處理。例如IOException。

5.Unchecked Exception【非受檢異常】

RuntimeException及其子類都是unchecked exception。好比NPE空指針異常,除數爲0的算數異常ArithmeticException等等,這種異常是運行時發生,沒法預先捕捉處理的。Error也是unchecked exception,也是沒法預先處理的。

三.異常處理的方式

1.try-catch-finally

這種方式是單體業務方法中最多見的處理方式,對於try塊內的業務邏輯預知可能會產生異常作處理。

例如讀取文件會強制要求你處理受檢查異常 IOException

/**
 * 讀取文件內容
 * @param fileName
 * @return
 */
public String readFileContent(String fileName) {
    File file = new File(fileName);
    BufferedReader reader = null;
    StringBuffer sbf = new StringBuffer();
    try {
        reader = new BufferedReader(new FileReader(file));
        String tempStr;
        while ((tempStr = reader.readLine()) != null) {
            sbf.append(tempStr).append("\n");
        }
        reader.close();
        return sbf.toString();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
        //doSomething
    }
    return sbf.toString();
}
複製代碼

2.try-with-resource-finally

try-with-resources 是JDK 7中一個新的異常處理機制,它可以很容易(優雅)地關閉在 try-catch 語句塊中使用的資源。在第一種處理的過程當中,finally中還要去手動關閉流。使用try-with-resource-finally就能夠幫你節省這一步代碼。

/**
 * 讀取文件內容
 * @param fileName
 * @return
 */
public String readFileContent(String fileName) {
    File file = new File(fileName);
    StringBuffer sbf = new StringBuffer();
    try ( BufferedReader reader = new BufferedReader(new FileReader(file))){
        String tempStr;
        while ((tempStr = reader.readLine()) != null) {
            sbf.append(tempStr).append("\n");
        }
        reader.close();
        return sbf.toString();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //doSomething
    }
    return sbf.toString();
}
複製代碼

3.全局異常處理

上面兩種方法是在方法內部處理了能夠預見的異常,那若是發生了不可預知的異常呢?有的朋友會說,我直接用catch(Exception ex)包裹處理異常。可是若是在微服務中,訂單中心調用支付中心,支付中心異常了,支付中心本身把發生的異常捕獲了,訂單中心認爲支付成功,將訂單下單成功,這就涼涼了。。。

3.jpeg

所以在支付中心必須將異常拋出,告知訂單中心,我這裏發生了異常了。訂單中心接受到了異常,終止處理。終止處理總要給前端一個錯誤碼,這個錯誤碼怎麼定義呢?try-catch嗎?那這個還只是一個下訂單的場景,若是每一個業務場景我都要單獨定一個錯誤碼,我每一個方法都定義一個try-catch塊嗎?顯然這是不可能的,且不說大量的try-catch塊會影響程序的運行效率,讓你寫着多異常處理我估計你都能煩死了。這時候咱們就須要全局異常處理了。對於特定的業務異常,定義code碼返回給全局異常處理,全局處理器解析code碼映射業務異常返回標準輸出給前端展現。

4.jpeg

四.spring中處理全局異常

4.1.@ExceptionHandler

統一處理某一類異常,從而可以減小代碼重複率和複雜度

1.未處理異常請求

@RestController
public class TestController {

    @RequestMapping("/test")
    public Object test(){
    		//拋出java.lang.ArithmeticException: / by zero 異常
        int i = 1 / 0;
        return new Date();
    }
}
複製代碼

展現

image-20210523151242414.png

2.處理異常請求

public class TestController {

    @RequestMapping("/test")
    public Object test(){
        int i = 1 / 0;
        return new Date();
    }

    @ExceptionHandler({RuntimeException.class})
    public Object catchException(Exception ex){
        return ex.getMessage();
    }
}
複製代碼

展現

image-20210523151426256.png

4.2.HandlerExceptionResolver

異常集中處理:接口形式處理異常

1.未處理異常請求

與4.1一致

2.處理異常請求

@Component
public class GlobalExceptionHandler implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",ex.getMessage());
        response.addHeader("Content-Type","application/json;charset=UTF-8");
        try {
            new ObjectMapper().writeValue(response.getWriter(),ex.getMessage());
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }

}
複製代碼

image-20210523151904317.png

4.3@ControllerAdvice與@ExceptionHandler組合

異常集中處理:註解形式

1.未處理異常請求

與4.1一致

2.處理異常請求

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public Object handle(RuntimeException ex) {
        return ex.getMessage();
    }

}
複製代碼

image-20210523152345328.png

好學的小夥伴可能想知道上面三種方式的原理,貼上一個源碼解析連接:www.cnblogs.com/lvbinbin2yu…

五.優雅異常返回

5.1.統一數據返回格式

ok,知道了異常的種類,統一捕獲異常的方式,那麼咱們如何跟前端同事約定數據返回呢?總不能後端每一個接口都告訴前端說我這個接口返回異常報文字符串,另外一個接口正常數據返回是個List結構。那我估計前端兄弟必定要對你重拳出擊了

6.jpeg

那麼定義一個統一的返回實體是很重要的,不廢話直接上代碼

//基礎先後端交互實體,定義了先後端交互過程當中,數據返回的標準格式
@Data
public class BaseResult {
    /**
     * httpCode
     */
    private Integer code;

    /**
     * 業務code
     */
    private String errorCode;

    /**
     * 業務信息
     */
    private String message;

    /**
     * 鏈路id【微服務請求調用鏈路跟蹤,不瞭解此概念的,能夠看一下個人另外一篇博客:https://juejin.cn/post/6923004276335869960】
     */
    private String traceId;

    public BaseResult() {
    }

    public BaseResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public BaseResult(Integer code, String errorCode, String message) {
        this.code = code;
        this.errorCode = errorCode;
        this.message = message;
    }

    /**
     * 通用業務請求狀態碼
     */
    public static final Integer CODE_SUCCESS = 200;
    public static final Integer CODE_SYSTEM_ERROR = 500;

    /**
     * 通用請求信息
     */
    public static final String SYSTEM_ERROR = "系統錯誤";
    public static final String MESSAGE_SUCCESS = "請求成功";
    public static final String QUERY_SUCCESS = "查詢成功";
    public static final String INSERT_SUCCESS = "新增成功";
    public static final String UPDATE_SUCCESS = "更新成功";
    public static final String DELETE_SUCCESS = "刪除成功";
    public static final String IMPORT_SUCCESS = "導入成功";
    public static final String EXPORT_SUCCESS = "導出成功";
    public static final String DOWNLOAD_SUCCESS = "下載成功";

}
複製代碼
@Data
@EqualsAndHashCode(callSuper = true)
public class Result<T> extends BaseResult {
		
		//業務數據返回放置
    private T data;

    public Result() {
    }

    public Result(Integer code, String message, T data) {
        super(code, message);
        this.data = data;
    }

    public Result(Integer code, String errorCode, String message, T data) {
        super(code, errorCode, message);
        this.data = data;
    }

    public boolean success() {
        return CODE_SUCCESS.equals(getCode());
    }

    public boolean systemFail() {
        return CODE_SYSTEM_ERROR.equals(getCode());
    }

    public static Result<Object> ok() {
        return new Result<>(CODE_SUCCESS, "", null);
    }

    public static Result<Object> ok(String message) {
        return new Result<>(CODE_SUCCESS, message, null);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(CODE_SUCCESS, MESSAGE_SUCCESS, data);
    }

    public static <T> Result<T> success(T data, String message) {
        return new Result<>(CODE_SUCCESS, message, data);
    }

    public static Result<Object> error(String message) {
        return Result.error(CODE_SYSTEM_ERROR, null, message, null);
    }

    public static Result<Object> error(String errorCode, String message) {
        return Result.error(CODE_SYSTEM_ERROR, errorCode, message, null);
    }

    public static Result<Object> error(Integer code, String errorCode, String message, Object data) {
        return new Result<>(code, errorCode, message, data);
    }

}
複製代碼
//若是是列表頁,那必然要返回給前端數據總條數,否則前端很差計算你一共有幾頁
@Data
@EqualsAndHashCode(callSuper = true)
public class PageResult<T> extends BaseResult {

    private Long total;

    private List<T> data;

    public PageResult() {
    }

    public static <T> PageResult<T> ok(Page<T> result) {
        PageResult<T> pageResult = new PageResult<>();
        pageResult.setCode(CODE_SUCCESS);
        pageResult.setMessage(QUERY_SUCCESS);
        pageResult.setTotal(result.getTotal());
        pageResult.setData(result.getRecords());
        return pageResult;
    }
}
複製代碼

5.2.異常處理

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public Object handle(RuntimeException ex) {
        return Result.error(errorCode.toString(), errorMessage.toString());
    }

}
複製代碼

如今咱們來看一下,仍是用上面的的demo代碼,看一下異常返回是什麼。

image-20210523153906814.png

5.3.異常標準化處理

​ 看到這裏小夥伴可能還以爲這個異常處理仍是沒什麼東西,不仍是吧代碼裏面的異常給拋了出來,前端是會直接展現message裏面的信息用做用戶操做結束的提示的。你報錯了,返回了一個/by zero。用戶鬼知道他的操做發生了什麼。因此這裏咱們還須要針對不一樣的異常,須要有不一樣的業務異常提示映射機制。

​ 全局業務異常處理用映射規則,咱們用什麼比較好呢?跟個人異常可以匹配,返回的是我定製的業務提示?

src=http___b-ssl.duitang.com_uploads_item_201803_06_20180306233859_tCTyz.thumb.700_0.jpeg&refer=http___b-ssl.duitang.jpeg

國際化功能啊!!!

關於國際化功能,小夥伴若是有不瞭解的,能夠參考這篇文章:blog.csdn.net/u012234419/…

我在國際化配置文件中定義code碼,業務異常拋出對應的code碼,全局異常中來映射不就行了?

ok,上代碼【這裏爲了演示方便,僅提供中文版的國際化code對應】

5.3.1.定義messages.properties

寫入內容

id.is.null=用戶id不可爲空
複製代碼

5.3.2.定義國際化配置類

@Component
public class SpringMessageSourceErrorMessageSource {

    @Autowired
    private MessageSource messageSource;

    @Override
    public String getMessage(String code, Object... params) {
        return messageSource.getMessage(code, params, LocaleContextHolder.getLocale());
    }

    @Override
    public String getMessage(String code, String defaultMessage, Object... params) {
        return messageSource.getMessage(code, params, defaultMessage, LocaleContextHolder.getLocale());
    }
}
複製代碼

5.3.3.統一業務異常

全部本系統內的業務異常繼承此異常,全局異常可經過捕獲該異常來處理業務異常

//最高父類業務異常
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {

    private static final long serialVersionUID = 430933593095358673L;

    private String errorMessage;

    private String errorCode;

    /**
     * 構造新實例。
     */
    public ServiceException() {
        super();
    }
    
    /**
     * 用給定的異常信息構造新實例。
     * @param errorMessage 異常信息。
     */
    public ServiceException(String errorMessage) {
        super((String)null);
        this.errorMessage = errorMessage;
    }

    //省略部分代碼

}
複製代碼
//子類參數校驗業務異常
@EqualsAndHashCode(callSuper = true)
public class ValidationException extends ServiceException {

    @Getter
    private Object[] params;

    public ValidationException(String message) {
        super(message);
    }

    public ValidationException(String message, Object[] params) {
        super(message);
        this.params = params;
    }

    public ValidationException(String code, String message, Object[] params) {
        super(code, message);
        this.params = params;
    }

    public static ValidationException of(String code, Object[] params) {
        return new ValidationException(code, null, params);
    }

}
複製代碼

5.3.4.定義子類異常校驗工具類

public class ValidationUtil {

    public static void isTrue(boolean expect, String code, Object... params) {
        if (!expect) {
            throw ValidationException.of(code, params);
        }
    }

    public static void isFalse(boolean expect, String code, Object... params) {
        isTrue(!expect, code, params);
    }
    
    //省略部分代碼

}
複製代碼

5.3.5.定義全局異常處理

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @Autowired
    private SpringMessageSourceErrorMessageSource messageSource;

    @ExceptionHandler(ConstraintViolationException.class)
    public Object handle(ConstraintViolationException ex) {
        StringBuilder errorCode = new StringBuilder();
        StringBuilder errorMessage = new StringBuilder();
        ex.getConstraintViolations()
                .stream()
                .forEach(error -> {
                    if (StrUtil.isNotBlank(errorCode.toString())) {
                        errorCode.append(",");
                    }
                    errorCode.append(error.getMessageTemplate());
                    if (StrUtil.isNotBlank(errorMessage.toString())) {
                        errorMessage.append(",");
                    }
                    errorMessage.append(error.getMessage());
                });
        return Result.error(errorCode.toString(), errorMessage.toString());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object handle(MethodArgumentNotValidException ex) {
        StringBuilder errorCode = new StringBuilder();
        StringBuilder errorMessage = new StringBuilder();
        buildBindingResult(errorCode, errorMessage, ex.getBindingResult());
        return Result.error(errorCode.toString(), errorMessage.toString());
    }

    @ExceptionHandler(ValidationException.class)
    public Object handle(ValidationException ex) {
        String errorMessage = messageSource.getMessage(ex.getErrorCode(), ex.getMessage(), ex.getParams());
        return Result.error(ex.getErrorCode(), errorMessage);
    }

    @ExceptionHandler(ServiceException.class)
    public Object handle(ServiceException ex) {
        return Result.error(ex.getErrorCode(), ex.getMessage());
    }

    @ExceptionHandler(Throwable.class)
    public Object handle(Throwable ex) {
        log.error("全局異常", ex);
        return Result.error(BaseResult.SYSTEM_ERROR);
    }

    /**
     * 獲取國際化數據
     * @param messageTemplate 消息模板
     * @return
     */
    private String getFromMessageTemplate(String messageTemplate) {
        if(StrUtil.isBlank(messageTemplate)){
            return null;
        }
        if (messageTemplate.length() < 2) {
            return null;
        }
        return messageTemplate.substring(1, messageTemplate.length() - 1);
    }

    /**
     * 構建並綁定返回結果
     * @param errorCode 錯誤code
     * @param errorMessage 國際化錯誤信息
     * @param bindingResult 須要處理的錯誤信息
     */
    private void buildBindingResult(StringBuilder errorCode, StringBuilder errorMessage, BindingResult bindingResult) {
        List<ObjectError> errors = bindingResult.getAllErrors();
        errors
                .stream()
                .forEach(error -> {
                    if (error.contains(ConstraintViolation.class)) {
                        ConstraintViolation constraintViolation = error.unwrap(ConstraintViolation.class);
                        if (errorCode.length() > 0) {
                            errorCode.append(",");
                        }
                        errorCode.append(getFromMessageTemplate(constraintViolation.getMessageTemplate()));
                    }
                    if (errorMessage.length() > 0) {
                        errorMessage.append(",");
                    }
                    String errorInfo = messageSource.getMessage(getFromMessageTemplate(error.getDefaultMessage()), null, (Object) null);
                    errorMessage.append(errorInfo);
                });
    }
}
複製代碼

5.4.演示

經過以上配置後,demo項目結構以下

├── demo.iml
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── examp
        │           ├── DemoApplication.java
        │           ├── config
        │           │   └── SpringMessageSourceErrorMessageSource.java
        │           ├── controller
        │           │   └── TestController.java
        │           ├── exception
        │           │   ├── ServiceException.java
        │           │   └── ValidationException.java
        │           ├── handler
        │           │   └── GlobalExceptionHandler.java
        │           ├── model
        │           │   ├── BaseResult.java
        │           │   ├── Result.java
        │           │   └── User.java
        │           └── util
        │               └── ValidationUtil.java
        └── resources
            ├── application.yml
            ├── logback-spring.xml
            └── messages.properties
複製代碼

演示一下異常處理的效果

1.messages.properties配置文件中添加

id.is.null=用戶id不可爲空
id.is.can.not.be.one=用戶id不能夠等於1
userName.is.blank=用戶名不可爲空
複製代碼

2.新建用戶類

@Data
public class User {
		
		//定義用戶id不可爲空,不然報錯
    @NotNull(message = "{id.is.null}")
    private Long id;

    @NotBlank(message = "{userName.is.blank}")
    private String userName;
}
複製代碼

3.測試方法

@RestController
public class TestController {

    @PostMapping
   	//1.參數校驗命中
    public Object add(@RequestBody @Valid User user) throws Exception{
    		//2.工具類校驗命中
        ValidationUtil.isFalse(Objects.equals(user.getId(),1L),"id.is.can.not.be.one");
        //3.業務異常校驗命中
        if(Objects.equals(user.getId(),2L)){
            throw new ServiceException("用戶id不可爲2");
        }
        //4.非業務異常命中
        if(Objects.equals(user.getId(),3L)){
            throw new Exception("用戶id不可爲3");
        }
        //5.正確邏輯執行
        System.out.println(user.toString());
        return Result.ok(BaseResult.INSERT_SUCCESS);
    }

}
複製代碼

5.4.1.參數校驗

post中body參數
{
   
}

命中校驗規則:1

控制檯輸出:
{
    "code": 500,
    "errorCode": "id.is.null,userName.is.blank",
    "message": "用戶id不可爲空,用戶名不可爲空",
    "traceId": null,
    "data": null
}
複製代碼

5.4.2.工具類校驗

post中body參數
{
    "id":1,
    "userName":"柏炎"
}

命中校驗規則:2

控制檯輸出:
{
    "code": 500,
    "errorCode": "id.is.can.not.be.one",
    "message": "用戶id不能夠等於1",
    "traceId": null,
    "data": null
}
複製代碼

5.4.3.業務異常校驗

post中body參數
{
    "id":2,
    "userName":"柏炎"
}

命中校驗規則:3

控制檯輸出:
{
    "code": 500,
    "errorCode": null,
    "message": "用戶id不可爲2",
    "traceId": null,
    "data": null
}
複製代碼

5.4.4.非業務異常

post中body參數
{
    "id":3,
    "userName":"柏炎"
}

命中校驗規則:4

控制檯輸出:
{
    "code": 500,
    "errorCode": null,
    "message": "系統錯誤",
    "traceId": null,
    "data": null
}
複製代碼

5.4.5.正常執行

post中body參數
{
    "id":4,
    "userName":"柏炎"
}

命中校驗規則:無

控制檯輸出:
{
    "code": 200,
    "errorCode": null,
    "message": "新增成功",
    "traceId": null,
    "data": null
}
複製代碼

5.5.全局異常處理流程圖

image-20210523164540066.png

六.總結

本文詳細介紹如何在spring優雅的使用全局異常的過程,現作如下總結及建議:

1.方法入參若是爲body形式,使用spring校驗規則進行參數預檢查

2.減小if/else的邏輯異常拋出,使用邏輯校驗工具類

3.內外部受檢查的業務異常捕獲返回包裝後的信息拋出給前端

4.沒法預測的異常在兜底的@ExceptionHandler(Throwable.class)最高異常捕獲類中處理,嚴禁將未作包裝的代碼異常直接返回給前端

5.不作拋出的異常在本身捕獲的地方作必要的日誌打印,便於問題定位與跟蹤

七.源碼獲取

本文核心內容已經收錄至博主的github中,感興趣的小夥伴能夠自取:

github.com/louyanfeng2…

八.參考

blog.csdn.net/writebook20…

blog.csdn.net/michaelgo/a…

九.聯繫我

若是你以爲文章寫得不錯,可以點贊評論+關注三連,麼麼噠~

釘釘:louyanfeng25

微信:baiyan_lou

相關文章
相關標籤/搜索