前言
如今的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,一個熱愛技術的程序員,鑒於技術有限,如果本文有什么紕漏或者兄台還有其他更好的建議/實現方式,歡迎留言評論,謝謝您!