作者:京東零售 秦浩然
一、什麼是異常
Java 語言按照錯誤嚴重性,從 throwale 根類衍生出 Error 和 Exception 兩大派系。
Error(錯誤):
程式在執行過程中所遇到的硬體或作業系統的錯誤。錯誤對程式而言是致命的,將導致程式無法執行。常見的錯誤有記憶體溢位,jvm 虛擬機器自身的非正常執行,calss 檔案沒有主方法。程式本生是不能處理錯誤的,只能依靠外界干預。Error 是系統內部的錯誤,由 jvm 丟擲,交給系統來處理。
Exception(異常):
程式正常執行中,可以預料的意外情況。比如資料庫連線中斷,空指標,陣列下標越界。異常出現可以導致程式非正常終止,也可以預先檢測,被捕獲處理掉,使程式繼續執行。Exception(異常)按照性質,又分為編譯異常(受檢異常)和執行時異常(非受檢異常)。
◦ 編譯異常:
又叫可檢查異常,通常時由語法錯和環境因素(外部資源)造成的異常。比如輸入輸出異常 IOException,資料庫操作 SQLException。其特點是,Java 語言強制要求捕獲和處理所有非執行時異常。透過行為規範,強化程式的健壯性和安全性。
◦ 執行時異常:
又叫不檢查異常 RuntimeException,這些異常一般是由程式邏輯錯誤引起的,即語義錯。比如算術異常,空指標異常 NullPointerException,下標越界 IndexOutOfBoundsException。執行時異常應該在程式測試期間被暴露出來,由程式設計師去除錯,而避免捕獲。
二、處理異常方式
程式碼中,我們最常見到的處理異常的方式就是:try-catch
try {
// 業務邏輯
} catch (Exception e) {
// 捕獲到異常的邏輯
}
或者是再進一步區分下異常型別:
try {
// 業務邏輯
} catch (IOException ie) {
// 捕獲到IO異常的邏輯
} catch (Exception e) {
// 捕獲到其他異常的邏輯
}
三、如何丟擲異常
我們通常可以用丟擲異常的方式來控制程式碼流程,然後在閘道器處統一catch異常來返回錯誤code。這在一定程度上可以簡化程式碼流程控制,如下所示:
@Override
public UserVO queryUser(Long id) {
UserDO userDO = userMapper.queryUserById(id);
if (Objects.isNull(userDO)) {
throw new RuntimeException("使用者不存在"); //使用者不存在丟擲異常
}
return userDO.toVo();
}
上面這種丟擲異常的方式,雖然簡化了程式碼流程,但是在存在多種錯誤場景時,沒有辦法細分具體的錯誤型別。如:使用者不存在的錯誤、使用者沒有許可權的錯誤;
聰明如你,一定想到了自定義異常,如下:
@Override
public UserVO queryUser(Long id) {
UserDO userDO = userMapper.queryUserById(id);
if (Objects.isNull(userDO)) {
throw new UserNotFoundException(); //使用者不存在丟擲對應異常
}
if(!checkLicence(userDO)) {
throw new BadLicenceException(); //使用者無許可權丟擲對應異常
}
return userDO.toVo();
}
確實,自定義異常可以解決錯誤場景細分的問題。進一步的,我們可以對系統流程不同階段、不同業務型別分別自定義異常,但這需要自定義大量的異常;
四、如何優雅的丟擲異常
上面的方式,可以區分出錯誤場景了,但是還存在一些缺點。如:可讀性差、需要定義大量的自定義異常;
那我們下面就去最佳化上面的問題;
用斷言增加程式碼的可讀性;
@Override
public UserVO queryUser(Long id) {
UserDO userDO = userMapper.queryUserById(id);
Assert.notNull(userDO, "使用者不存在"); //用斷言進行引數的非空校驗
return userDO.toVo();
}
斷言雖然程式碼簡潔、可讀性好,但是缺乏像上述自定義異常一樣可以明確區分錯誤場景,這就引出我們的究極方案:自定義斷言;
自定義斷言;
我們用自定義斷言的方式,綜合上面自定義異常和斷言的優點,在斷言失敗後,丟擲我們制定好的異常。程式碼如下:
• 自定義異常基本類
@Getter
@Setter
public class BaseException extends RuntimeException {
// 響應碼
private IResponseEnum responseEnum;
// 引數資訊
private Object[] objs;
public BaseException(String message, IResponseEnum responseEnum, Object[] objs) {
super(message);
this.responseEnum = responseEnum;
this.objs = objs;
}
public BaseException(String message, Throwable cause, IResponseEnum responseEnum, Object[] objs) {
super(message, cause);
this.responseEnum = responseEnum;
this.objs = objs;
}
}
• 自定義斷言介面
public interface MyAssert {
/**
* 建立自定義異常
*
* @param objs 引數資訊
* @return 自定義異常
*/
BaseException newException(Object... objs);
/**
* 建立自定義異常
*
* @param msg 描述資訊
* @param objs 引數資訊
* @return 自定義異常
*/
BaseException newException(String msg, Object... objs);
/**
* 建立自定義異常
*
* @param t 接收驗證異常
* @param msg 描述資訊
* @param objs 引數資訊
* @return 自定義異常
*/
BaseException newException(Throwable t, String msg, Object... objs);
/**
* 校驗非空
*
* @param obj 被驗證物件
*/
default void assertNotNull(Object obj, Object... objs) {
if (obj == null) {
throw newException(objs);
}
}
/**
* 校驗非空
*
* @param obj 被驗證物件
*/
default void assertNotNull(Object obj, String msg, Object... objs) {
if (obj == null) {
throw newException(msg, objs);
}
}
}
上述程式碼我們可以看出基本設計,就是在我們自定義斷言失敗後丟擲我們自定義異常。
下面是具體的實現案例:
• 自定義業務異常類,繼承自異常基本類
public class BusinessException extends BaseException {
public BusinessException(IResponseEnum responseEnum, Object[] args, String msg) {
super(msg, responseEnum, args);
}
public BusinessException(IResponseEnum responseEnum, Object[] args, String msg, Throwable t) {
super(msg, t, responseEnum, args);
}
}
• 響應code列舉介面定義
public interface IResponseEnum {
/**
* 返回code碼
*
* @return code碼
*/
String getCode();
/**
* 返回描述資訊
*
* @return 描述資訊
*/
String getMsg();
}
• 自定義業務異常類斷言定義,實現自定義斷言失敗後對應的自定義異常的定義;
public interface BusinessExceptionAssert extends IResponseEnum, MyAssert {
@Override
default BaseException newException(Object... args) {
return new BusinessException(this, args, this.getMsg()); //斷言失敗後,丟擲自定義異常
}
@Override
default BaseException newException(String msg, Object... args) {
return new BusinessException(this, args, msg); //斷言失敗後,丟擲自定義異常
}
@Override
default BaseException newException(Throwable t, String msg, Object... args) {
return new BusinessException(this, args, msg, t); //斷言失敗後,丟擲自定義異常
}
}
• 用列舉的方式,代替BadLicenceException、UserNotFoundException自定義異常。
public enum ResponseEnum implements IResponseEnum, BusinessExceptionAssert {
BAD_LICENCE("0001", "無權訪問"),
USER_NOT_FOUND("1001", "使用者不存在"),
;
private final String code, msg;
ResponseEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
@Override
public String getCode() {
return code;
}
@Override
public String getMsg() {
return msg;
}
}
使用例項
自定義斷言失敗丟擲自定義異常
@Override
public UserVO queryUser(Long id) {
UserDO userDO = userMapper.queryUserById(id);
ResponseEnum.USER_NOT_FOUND.assertNotNull(userDO); //自定義斷言失敗丟擲自定義異常
return userDO.toVo();
}
閘道器處統一catch異常,識別異常場景
public static void main(String[] args) {
UserService userService = new UserServiceImpl(new UserMapperImpl());
UserController userController = new UserController(userService);
try {
UserVO vo = userController.queryUser(2L); //執行業務邏輯
} catch (BusinessException e) {
System.out.println(e.getResponseEnum().getCode()); //出現異常,錯誤code:1001
System.out.println(e.getMessage()); //出現異常,錯誤msg:使用者不存在
}
}
五、如何優雅的處理異常
閘道器處統一處理異常,這屬於常規操作,這裡不再贅述,簡單舉例如下:
@ControllerAdvice
public class BusinessExceptionHandler {
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public Response handBusinessException(BaseException e) {
return new Response(e.getResponseEnum().getCode(), e.getResponseEnum().getMsg()); //統一處理異常
}
}
綜上,我們採用自定義斷言的方式,結合了斷言的可讀性高的優勢和自定義異常區分錯誤場景的優勢。並且,有新增的錯誤場景,我們只需要在錯誤碼列舉中新增對應列舉即可。