Spring專案中優雅的異常處理
前言
如今的Java Web專案多是以 MVC 模式構建的,通常我們都是將 Service 層的異常統一的丟擲,包括自定義異常和一些意外出現的異常,以便進行事務回滾,而 Service 的呼叫者 Controller 則承擔著異常處理的責任,因為他是與 Web 前端互動的最後一道防線,如果此時還不進行處理則使用者會在網頁上看到一臉懵逼的
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4
at cn.keats.TestAdd.main(TestAdd.java:20)
這樣做有以下幾點壞處:
- 使用者體驗很不友好,可能使用者會吐槽一句:這是什麼XX網站。然後不再訪問了
- 如果這個使用者是同行,他不僅看到了專案程式碼的結構,而且看到丟擲的是這麼低階的索引越界異常,會被人家看不起
- 使用者看到網站有問題,打電話給客服,客服找到產品,產品叫醒正在熟睡/打遊戲的你。你不僅睡不好遊戲打不了還得挨批評完事改程式碼
哎,真慘。因此一般我們採用的方法會是像這樣:
異常處理
一般的Controller處理
Service程式碼如下:
@Service public class DemoService { public String respException(String param){ if(StringUtils.isEmpty(param)){ throw new MyException(ExceptionEnum.PARAM_EXCEPTION); } int i = 1/0; return "你看不見我!"; } }
Controller程式碼如下:
@RestController public class DemoController { @Autowired private DemoService demoService; @PostMapping("respException") public Result respException(){ try { return Result.OK(demoService.respException(null)); } catch (MyException e){ return Result.Exception(e, null); } catch (Exception e) { return Result.Error(); } } }
如果此時傳送如下的請求:
http://localhost/respException
伺服器捕捉到自定義的異常 MyException,而返回引數異常的Json串:
{
"code": 1,
"msg": "引數異常",
"data": null
}
而當我們補上引數:
http://localhost/respException?param=zhangsan
則伺服器捕捉到 by zero 異常,會返回未知錯誤到前端頁面
{
"code": -1,
"msg": "未知錯誤",
"data": null
}
這樣就會在一定程度上規避一些問題,例如引數錯誤就可以讓使用者去修改其引數,當然這一般需要前端同學配合做頁面的引數校驗,必傳引數都有的時候再向伺服器傳送請求,一方面減輕伺服器壓力,一方面將問題前置節省雙方的時間。但是這樣寫有一個壞處就是所有的Controller方法中關於異常的部分都是一樣的,程式碼非常冗餘。且不利於維護,而且一些不太熟悉異常機制的同學可能會像踢皮球一樣將異常抓了拋,拋完又抓回來,鬧著玩呢。。。(筆者就曾經接手過一個跑路同學的程式碼這樣處理異常,那簡直是跟異常捉迷藏呢!可恨)我們在Service有全域性事務處理,在系統中可以有全域性的日誌處理,這些都是基於Spring 的一大殺器:AOP(面向切面程式設計) 實現的,AOP是什麼呢?
AOP
AOP是Spring框架面向切面的程式設計思想,AOP採用一種稱為“橫切”的技術,將涉及多業務流程的通用功能抽取並單獨封裝,形成獨立的切面,在合適的時機將這些切面橫向切入到業務流程指定的位置中。如果說我們常用的OOP思想是從上到下執行業務流程的話,AOP就相當於在我們執行業務的時候橫切一刀,如下圖所示:
而Advice(通知)是AOP思想中重要的一個術語,分為前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)和環繞通知(Around)五種。具體通知所表示的意義我這裡不多贅述,網上關於Spring核心原理的講解都會提及。而我們熟知的 Service 事務處理其實就是基於AOP AfterThrowing 通知實現的事務回滾。我們自定義的日誌處理也可以根據不同的需求定製不同的通知入口。那既然如此,我們為何不自定義一個全域性異常處理的切面去簡化我們的程式碼呢?別急,且繼續向下看。
優雅的處理異常
Spring 在 3.2 版本已經為我們提供了該功能: @ControllerAdvice 註解。此註解會捕捉Controller層丟擲的異常,並根據 @ExceptionHandler 註解配置的方法進行異常處理。下面是一個示例工程,主要程式碼如下:
Result類:
此 Result 採用泛型的方式,便於在 Swagger 中配置方法的出參。使用靜態工廠方法是的物件的初始化更加見名只意。對於不存在共享變數問題的 Error 物件,採用雙重校驗鎖懶漢單例模式來節省伺服器資源(當然最好還是整個專案執行中一直沒有初始化它讓人更加舒服。)
package cn.keats.util;
import cn.keats.exception.MyException;
import lombok.Data;
/**
* 功能:統一返回結果,直接呼叫對應的工廠方法
*
* @author Keats
* @date 2019/11/29 18:20
*/
@Data
public class Result<T> {
private Integer code;
private String msg;
private T data;
/**
* 功能:響應成功
*
* @param data 響應的資料
* @return woke.cloud.property.transformat.Result
* @author Keats
* @date 2019/11/30 8:54
*/
public static <T> Result<T> OK(T data){
return new Result<>(0, "響應成功", data);
}
private static Result errorResult;
/**
* 功能:返回錯誤,此錯誤不可定製,全域性唯一。一般是程式碼出了問題,需要修改程式碼
*
* @param
* @return Result
* @author Keats
* @date 2019/11/30 8:55
*/
public static Result Error(){
if(errorResult == null){
synchronized (Result.class){
if(errorResult == null){
synchronized (Result.class){
errorResult = new Result<>(-1, "未知錯誤", null);
}
}
}
}
return errorResult;
}
/**
* 功能:返回異常,直接甩自定義異常類進來
*
* @param e 自定義異常類
* @param data 資料,如果沒有填入 null 即可
* @return woke.cloud.property.transformat.Result<T>
* @author Keats
* @date 2019/11/30 8:55
*/
public static <T> Result<T> Exception(MyException e, T data){
return new Result<>(e.getCode(), e.getMsg(), data);
}
/**
* 功能:為了方便使用,使用靜態工廠方法建立物件。如需新的構造方式,請新增對應的靜態工廠方法
*
* @author Keats
* @date 2019/11/30 8:56
*/
private Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
自定義異常類:
package cn.keats.exception;
import lombok.Getter;
/**
* 功能:系統自定義異常類。繼承自RuntimeException,方便Spring進行事務回滾
*
* @author Keats
* @date 2019/11/29 18:50
*/
@Getter
public class MyException extends RuntimeException{
private Integer code;
private String msg;
public MyException(ExceptionEnum eEnum) {
this.code = eEnum.getCode();
this.msg = eEnum.getMsg();
}
}
異常程式碼列舉類:
package cn.keats.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 功能:異常列舉
*
* @author Keats
* @date 2019/11/29 18:49
*/
@Getter
@AllArgsConstructor
public enum ExceptionEnum {
PARAM_EXCEPTION(1,"引數異常"),
USER_NOT_LOGIN(2,"使用者未登入"),
FILE_NOT_FOUND(3,"檔案不存在,請重新選擇");
private Integer code;
private String msg;
}
異常切面:
其中 @RestControllerAdvice 是spring 4.3 新增的新註解,是 @ControllerAdvice 和 @ResponseBody 的簡寫方式,類似與 @RestController 與 @Controller 的關係
package cn.keats.advice;
import cn.keats.exception.MyException;
import cn.keats.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 功能:全域性異常處理器,Controller異常直接丟擲
*
* @return
* @author Keats
* @date 2019/11/30 10:28
*/
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
/**
* 功能:其餘非預先規避的異常返回錯誤
*
* @param e
* @return woke.cloud.property.transformat.Result
* @author Keats
* @date 2019/11/30 10:08
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result ResponseException(Exception e) {
log.error("未知錯誤,錯誤資訊:", e);
return Result.Error();
}
/**
* 功能:捕捉到 MyException 返回對應的訊息
*
* @param e
* @return woke.cloud.property.transformat.Result
* @author Keats
* @date 2019/11/30 10:07
*/
@ExceptionHandler(value = MyException.class)
@ResponseBody
public Result myException(MyException e) {
log.info("返回自定義異常:異常程式碼:" + e.getCode() + "異常資訊:" + e.getMsg());
return Result.Exception(e, null);
}
}
此時的 Controller 方法可以這樣寫:
package cn.keats.controller;
import cn.keats.service.DemoService;
import cn.keats.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
@PostMapping("respException")
public Result respException(String param) throws Exception {
return Result.OK(demoService.respException(param));
}
@PostMapping("respError")
public Result respError() throws Exception {
return Result.OK(demoService.respException(null));
}
}
省略的大部分的異常處理程式碼,使得我們只需要關注業務,一方面提高了程式碼質量,可閱讀性,另一方面也提高了我們的開發速度。美哉!
啟動專案,進行測試沒有問題。
我是 Keats,一個熱愛技術的程式設計師,鑑於技術有限,如果本文有什麼紕漏或者兄臺還有其他更好的建議/實現方式,歡迎留言評論,謝謝您