自定義全局異常處理器(Java)

正常業務系統中,當前後端分離時,系統即使有未知異常,也要保證接口能返回錯誤提示,也需要根據業務規則制定相應的異常狀態碼和異常提示。所以需要一個全局異常處理器。相關代碼:GitHub

異常

下面是 Java 異常繼承圖:

                     ┌───────────┐
                     │  Object   │
                     └───────────┘
                           ▲
                           │
                     ┌───────────┐
                     │ Throwable │
                     └───────────┘
                           ▲
                 ┌─────────┴─────────┐
                 │                   │
           ┌───────────┐       ┌───────────┐
           │   Error   │       │ Exception │
           └───────────┘       └───────────┘
                 ▲                   ▲
         ┌───────┘              ┌────┴──────────┐
         │                      │               │
┌─────────────────┐    ┌─────────────────┐┌───────────┐
│OutOfMemoryError │... │RuntimeException ││IOException│...
└─────────────────┘    └─────────────────┘└───────────┘
                                ▲
                    ┌───────────┴─────────────┐
                    │                         │
         ┌─────────────────────┐ ┌─────────────────────────┐
         │NullPointerException │ │IllegalArgumentException │...
         └─────────────────────┘ └─────────────────────────┘

根據編譯時是否需要捕獲,異常可以分爲兩類:1、寫代碼時,編譯器規定必須捕獲的異常,不捕獲將報錯;2、(拋出後)不必須捕獲的異常,編譯器對此類異常不做處理。

  • 必須捕獲的異常:Exception 以及 Exception 除去 RuntimeException 的子類。

  • 不必須捕獲的異常:Error 以及 Error 的子類;RuntimeException 以及 RuntimeException 的子類。

必須捕獲的異常:

    @GetMapping("/testThrowIOException")
    public ApiResponse<Void> testThrowIOException() {

        testThrowIOException(); // 將報錯
        return ApiResponse.success();
    }

    private void throwIOException() throws IOException {
        System.out.println("testThrowIOException");
        throw new IOException();
    }

不必須捕獲的異常:

    @GetMapping("/testThrowRuntimeException")
    public ApiResponse<Void> testThrowRuntimeException() {

        throwRuntimeException(); // 不報錯
        return ApiResponse.success();
    }

    private void throwRuntimeException() { // 無需 throws
        System.out.println("testThrowRuntimeException");
        throw new ArrayIndexOutOfBoundsException();
    }

不過在運行時,任何異常都可以進行捕獲處理,避免接口沒有返回值的情況。

拋異常

常見異常處理方式有兩種,1、捕獲後處理,2、拋出。拋出也分爲捕獲後拋出和直接拋出。

當本身沒有異常,卻使用 throws 拋出異常時,此時相當於沒有拋異常(將攔截不到異常)。

    @GetMapping("/testThrowIOException2")
    public ApiResponse<Void> testThrowIOException2() throws IOException {

        throwIOException2();
        return ApiResponse.success();
    }

    private void throwIOException2() throws IOException {
        System.out.println("testThrowIOException");
    }

打印異常

打印異常可以使用 Logback 打印,其相關方法的使用: log.error(e.getMessage(), e); 相當於下面這兩條語句:

System.out.println(e.getMessage()); // 打印異常信息
e.printStackTrace(); // 打印異常調用棧

減少 NullPointException 的方式是設置默認值。

Error 錯誤

測試 StackOverflowError,設置虛擬機棧的大小爲 256K,IDEA(VM options): -Xss256k

class JavaVMStackSOF {
    public int stackLength = 1;

    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

測試 OutOfMemoryError,設置 Java 堆的大小爲 128M,IDEA(VM options):-Xms128M -Xmx128M

class HeapOOM {
    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

全局異常處理器

自定義異常

自定義異常從 RuntimeException 派生,構造方法使用 super(message);super(message, cause);。添加狀態碼和參數屬性。

public abstract class BaseException extends RuntimeException {
    private int code; // 狀態碼
    private String message;
    private Object[] args; // 參數
    private IResponseEnum responseEnum;

    public BaseException(IResponseEnum iResponseEnum, Object[] args, String message) {
        super(message);
        this.code = iResponseEnum.getCode();
        this.message = message;
        this.responseEnum = iResponseEnum;
        this.args = args;
    }

    public BaseException(IResponseEnum iResponseEnum, Object[] args, String message, Throwable cause) {
        super(message, cause);
        this.code = iResponseEnum.getCode();
        this.message = message;
        this.responseEnum = iResponseEnum;
        this.args = args;
    }

    public int getCode() {
        return this.code;
    }

    public String getMessage() {
        return this.message;
    }

    public Object[] getArgs() {
        return this.args;
    }

    public IResponseEnum getResponseEnum() {
        return this.responseEnum;
    }
}

當前服務的業務異常不用每個單獨作爲一個異常類,可通過 message 和 code 來做一個區分。

public class LoanException extends BusinessException {

    public static LoanException INTERNAL_ERROR = new LoanException(ResponseEnum.SERVER_ERROR);
    public static LoanException REJECT = new LoanException(ResponseEnum.REJECT);
    public static LoanException BAND_FAIL = new LoanException(ResponseEnum.BAND_FAIL);
    public static LoanException FORBIDDEN = new LoanException(ResponseEnum.FORBIDDEN);
    public static LoanException DB_OPTIMISTIC_LOCK = new LoanException(ResponseEnum.DB_OPTIMISTIC_LOCK);

    public LoanException(IResponseEnum responseEnum) {
        super(responseEnum, null, responseEnum.getMessage());
    }

    public LoanException(IResponseEnum responseEnum, String message) {
        super(responseEnum, null, message);
    }

}
    @GetMapping("/testLoanException")
    private ApiResponse<Void> testLoanException() {
        throw LoanException.REJECT;
    }

爲不同的業務錯誤場景設置相關枚舉類型(狀態碼、錯誤提示)。爲枚舉添加可斷言判斷拋出異常功能。

public interface Assert {
    BaseException newException(Object... var1);

    BaseException newException(Throwable var1, Object... var2);

    default void assertNotNull(Object obj) {
        if (obj == null) {
            throw this.newException((Object[])null);
        }
    }

    default void assertNotNull(Object obj, Object... args) {
        if (obj == null) {
            throw this.newException(args);
        }
    }

    default void assertTrue(boolean flag) {
        if (!flag) {
            throw this.newException((Object[])null);
        }
    }

    default void assertTrue(boolean flag, Object... args) {
        if (!flag) {
            throw this.newException((Object[])null);
        }
    }
}
public interface BusinessExceptionAssert extends IResponseEnum, Assert {
    default BaseException newException(Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg);
    }

    default BaseException newException(Throwable t, Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);
        return new BusinessException(this, args, msg, t);
    }
}
@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {

    SUCCESS(111000,"success"),
    PARAM_VALID_ERROR(111001,"param check error."),
    SERVER_ERROR(111002,"server error."),
    LOGIN_ERROR(111003,"login error"),
    UNAUTHORIZED(111004, "unauthorized"),
    SERVICE_ERROR(111005,"service error."),
    FORBIDDEN(114003, "forbidden"),
    TIMEOUT(114000, "timeout"),
    REJECT(114001, "reject"),
    EMAIL_CONFLICT(114002, "email conflict"),
    EMAIL_VERIFY_FAIL(114004, "email verify fail"),
    DB_OPTIMISTIC_LOCK(114008, "update fail"),// 數據庫樂觀鎖
    EMAIL_SEND_FAIL(114011, "email send fail"),
    DATA_NOT_FOUND(114012, "data not found"),
    LOGIN_TOKEN_VERIFY_FAIL(114014, "login token verify fail"),
    ;

    /**
     * 返回碼
     */
    private int code;
    /**
     * 返回消息
     */
    private String message;

}
    @GetMapping("/test")
    public ApiResponse<String> test(String value) {
        ResponseEnum.SERVICE_ERROR.assertNotNull(value);
        return ApiResponse.success("true");
    }

全局異常管理器

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 生產環境
     */
    private final static String ENV_PROD = "production";

    /**
     * 當前環境
     */
    @Value("${env}")
    private String profile;

    /**
     * 業務異常
     *
     * @param e 異常
     * @return 異常結果
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public ApiResponse<String> handleBusinessException(BaseException e) {
        log.error(e.getMessage(), e);
        log.error("BusinessException");
        return ApiResponse.fail(e.getCode(), e.getMessage());
    }

    /**
     * 非錯誤編碼類系統異常
     *
     * @param e 異常
     * @return 異常結果
     */
    @ExceptionHandler(value = SystemException.class)
    @ResponseBody
    public ApiResponse<String> handleBaseException(SystemException e) {
        return getServerErrorApiResponse(e);
    }

    /**
     * Controller 上一層相關異常
     *
     * @param e 異常
     * @return 異常結果
     */
    @ExceptionHandler({NoHandlerFoundException.class,
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            // BindException.class,
            // MethodArgumentNotValidException.class
            HttpMediaTypeNotAcceptableException.class,
            ServletRequestBindingException.class,
            ConversionNotSupportedException.class,
            MissingServletRequestPartException.class,
            AsyncRequestTimeoutException.class
    })
    @ResponseBody
    public ApiResponse<String> handleServletException(Exception e) {
        return getServerErrorApiResponse(e);
    }

    /**
     * 未定義異常。相當於全局異常捕獲處理器。
     *
     * @param e 異常
     * @return 異常結果
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public ApiResponse<String> handleException(Exception e) {
        return getServerErrorApiResponse(e);
    }

    private ApiResponse<String> getServerErrorApiResponse(Exception e) {
        int code = ResponseEnum.SERVER_ERROR.getCode();
        String productShowMessage = ResponseEnum.SERVER_ERROR.getMessage();
        if (ENV_PROD.equals(profile)) {
            return ApiResponse.fail(code, productShowMessage);
        }
        return ApiResponse.fail(code, e.getMessage());
    }
}

使用 @ControllerAdvice + @ExceptionHandler 實現對指定異常的捕獲。此時運行時異常和 Error 也能被捕獲。

延伸閱讀

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