如何優雅的處理異常 | 京東雲技術團隊

作者:京東零售  秦浩然

一、什麼是異常

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());    //統一處理異常
    }
}

綜上,我們採用自定義斷言的方式,結合了斷言的可讀性高的優勢和自定義異常區分錯誤場景的優勢。並且,有新增的錯誤場景,我們只需要在錯誤碼枚舉中新增對應枚舉即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章