正常業務系統中,當前后端分離時,系統即使有未知異常,也要保證接口能返回錯誤提示,也需要根據業務規則制定相應的異常狀態碼和異常提示。所以需要一個全局異常處理器。相關代碼: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;命令行:java -Xss256k JavaVMStackSOF
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;
}
}
}
stack length:1693
Exception in thread "main" java.lang.StackOverflowError
at wang.depp.exception.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at wang.depp.exception.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
...
測試 OutOfMemoryError,設置 Java 堆的大小為 128M,IDEA(VM options):-Xms10M -Xmx10M;命令行:java -Xms10M -Xmx10M wang.depp.exception.HeapOOM
(如果類中包含 package 路徑,需 cd 到 java
目錄后運行此命令)
package wang.depp.exception;
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3720)
at java.base/java.util.Arrays.copyOf(Arrays.java:3689)
...
全局異常處理器
自定義異常
自定義異常從 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 也能被捕獲。