Spring Boot 全域性異常處理
當我們在開發一個專案時,往往需要對異常進行捕獲處理,以提供友好的資訊展示給使用者。但隨著業務的增長,專案越來越複雜,需要捕獲異常的地方就會越來越多,如果每個地方都進行try catch,那程式碼將會變得非常冗餘且不好維護。
我們知道Spring Boot預設情況下會對映到 /error 進行異常處理,但是提示並不十分友好。那有沒有一種統一的處理機制?幸好從Spring 3.2以後新增了@ControllerAdvice 註解,使用AOP對Controller控制器進行增強(前置增強、後置增強、環繞增強),那麼我們就可以對控制器的方法進行呼叫前(前置增強)和呼叫後(後置增強)的處理。
Spring還提供了@ExceptionHandler異常增強註解,一般需要配合@RequestBody註解使用。程式如果在執行控制器方法前或執行時丟擲異常,會被@ExceptionHandler註解了的方法處理。如果全部異常處理返回json格式,那麼可以使用@RestControllerAdvice 代替@ControllerAdvice,這樣在方法上就可以不需要新增@ResponseBody。
自定義異常類
定義一個AgException作為全域性的自定義異常。繼承RuntimeException(執行時異常)對程式碼無可侵入性,不需要方法中強制捕獲或者丟擲。
@Getter @Slf4j public class AgException extends RuntimeException { /** * 響應狀態碼列舉 */ private StatusResultEnum statusResult; private Object[] args; /** * 構造指定異常程式碼與訊息引數的業務異常。 * * @param statusResult 異常程式碼 * @param args 訊息引數,該引數將用於格式化異常程式碼中的訊息字串 */ public AgException(StatusResultEnum statusResult, Object... args) { this(statusResult, null, args); } /** * 構造指定異常程式碼、異常原因與訊息引數的業務異常。 * * @param statusResult 異常程式碼 * @param cause 異常訊息 * @param args 訊息引數,該引數將用於格式化異常程式碼中的訊息字串 */ public AgException(StatusResultEnum statusResult, Throwable cause, Object... args) { super(statusResult.getCodeMsg(args), cause); log.error("系統異常:{} ", cause.getMessage(), cause); this.args = args; this.statusResult = statusResult; } }
新增自定義資訊列舉類
public enum StatusResultEnum {
SUCCESS("2000", "success", "請求成功"),
/**
* 可預知異常
*/
NOT_LOGIN_IN("4001", "未登入", "未登入"),
/**
* 不可預知異常,但有明確錯誤碼
*/
UN_AUTHORIZED("4002", "許可權不足", "許可權不足"),
/**
* 不可預知異常,預設錯誤
*/
INTERNAL_SERVER_ERROR("5000", "%1s", "內部伺服器錯誤,請聯絡客服人員。");
/**
* 響應返回碼
*/
@Getter
private String code;
/**
* 響應描述,面向開發者
*/
@Setter
private String codeMsg;
/**
* 響應描述,面向使用者
*/
@Setter
private String statusMsg;
StatusResultEnum(String code, String codeMsg, String statusMsg) {
this.code = code;
this.codeMsg = codeMsg;
this.statusMsg = statusMsg;
}
/**
* 根據指定的佔位符引數格式化異常訊息。
*
* @param args 佔位符引數
* @return 格式化後的異常訊息
*/
public String getCodeMsg(Object... args) {
return ErrorCodeUtils.formatMessage(codeMsg, args);
}
/**
* 根據指定的佔位符引數格式化異常訊息。
*
* @param args 佔位符引數
* @return 格式化後的異常訊息
*/
public String getStatusMsg(Object... args) {
return ErrorCodeUtils.formatMessage(statusMsg, args);
}
}
全域性異常處理類
- 對異常進行歸類:可預知異常(即自定義異常AgException);不可預知異常,但需要明確定義錯誤碼;不可預知異常,不需特殊處理錯誤碼。
- 使用Spring MVC控制器增強,捕獲全域性異常。
- 捕獲AgException業務異常,取出錯誤碼和資訊構造響應。
- 使用一個執行緒安全、並且不可更改的map儲存不可預知異常自定義的錯誤資訊。
- 捕獲AgException以外的異常(Exception),判斷map是否定義了該異常錯誤資訊,若有定義取出錯誤資訊構造響應,否則返回預設錯誤。
@RestControllerAdvice
@Slf4j
public class AgExceptionHandler {
/**
* 執行緒安全
*/
private static final ImmutableMap<Class<? extends Throwable>, StatusResultEnum> EXCEPTIONS;
static {
final ImmutableMap.Builder<Class<? extends Throwable>, StatusResultEnum> builder = ImmutableMap.builder();
builder.put(LockedAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(UnknownAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(IncorrectCredentialsException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(DisabledAccountException.class, StatusResultEnum.IDENTITY_AUTH_FAIL);
builder.put(UnauthorizedException.class, StatusResultEnum.UN_AUTHORIZED);
builder.put(MissingServletRequestParameterException.class, StatusResultEnum.REQUIRE_ARGUMENT);
// 其他未被發現的異常
builder.put(Exception.class, StatusResultEnum.INTERNAL_SERVER_ERROR);
EXCEPTIONS = builder.build();
}
@ExceptionHandler(AgException.class)
public BaseResponse handleAgException(Throwable e) {
AgException agException = (AgException) e;
return new ResultResponse(agException.getStatusResult(), agException.getArgs());
}
@ExceptionHandler(Exception.class)
public BaseResponse handleException(Exception e) {
log.error("系統異常:{} ", e.getMessage(), e);
StatusResultEnum statusResultEnum = EXCEPTIONS.get(e.getClass());
return new ResultResponse(statusResultEnum, e.getMessage());
}
}
總結
異常丟擲的順序為Dao—Service—Controller—AgExceptionHandler,SpringMVC增強的即是在Controller層進行攔截,實現全域性異常統捕獲,異常在AgExceptionHandler 統一處理後,就無需再程式碼中單獨對每個服務進行try catch,此種實現方式程式碼不僅重用性高,而