在SpringBoot的開發中,為了提高程式執行的魯棒性,我們經常需要對各種程式異常進行處理,但是如果在每個出異常的地方進行單獨處理的話,這會引入大量業務不相關的例外處理程式碼,增加了程式的耦合,同時未來想改變異常的處理邏輯,也變得比較困難。這篇文章帶大家瞭解一下如何優雅的進行全域性例外處理。
為了實現全域性攔截,這裡使用到了Spring中提供的兩個註解,@RestControllerAdvice
和@ExceptionHandler
,結合使用可以攔截程式中產生的異常,並且根據不同的異常型別分別處理。下面我會先介紹如何利用這兩個註解,優雅的完成全域性異常的處理,接著解釋這背後的原理。
在下面的例子中,我們繼承了ResponseEntityExceptionHandler
並使用@RestControllerAdvice
註解了這個類,接著結合@ExceptionHandler
針對不同的異常型別,來定義不同的例外處理方法。這裡可以看到我處理的異常是自定義異常,後續我會展開介紹。
ResponseEntityExceptionHandler中包裝了各種SpringMVC在處理請求時可能丟擲的異常的處理,處理結果都是封裝成一個ResponseEntity物件。ResponseEntityExceptionHandler是一個抽象類,通常我們需要定義一個用來處理異常的使用
@RestControllerAdvice
註解標註的例外處理類來繼承自ResponseEntityExceptionHandler。ResponseEntityExceptionHandler中為每個異常的處理都單獨定義了一個方法,如果預設的處理不能滿足你的需求,則可以重寫對某個異常的處理。
@Log4j2
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
/**
* 定義要捕獲的異常 可以多個 @ExceptionHandler({}) *
* @param request request
* @param e exception
* @param response response
* @return 響應結果
*/
@ExceptionHandler(AuroraRuntimeException.class)
public GenericResponse customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {
AuroraRuntimeException exception = (AuroraRuntimeException) e;
if (exception.getCode() == ResponseCode.USER_INPUT_ERROR) {
response.setStatus(HttpStatus.BAD_REQUEST.value());
} else if (exception.getCode() == ResponseCode.FORBIDDEN) {
response.setStatus(HttpStatus.FORBIDDEN.value());
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
return new GenericResponse(exception.getCode(), null, exception.getMessage());
}
@ExceptionHandler(NotLoginException.class)
public GenericResponse tokenExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) {
log.error("token exception", e);
response.setStatus(HttpStatus.FORBIDDEN.value());
return new GenericResponse(ResponseCode.AUTHENTICATION_NEEDED);
}
}
這裡定義了常見的幾種異常碼,主要用在丟擲自定義異常時,對不同的情形進行區分。
@Getter
public enum ResponseCode {
SUCCESS(0, "Success"),
INTERNAL_ERROR(1, "伺服器內部錯誤"),
USER_INPUT_ERROR(2, "使用者輸入錯誤"),
AUTHENTICATION_NEEDED(3, "Token過期或無效"),
FORBIDDEN(4, "禁止存取"),
TOO_FREQUENT_VISIT(5, "存取太頻繁,請休息一會兒");
private final int code;
private final String message;
private final Response.Status status;
ResponseCode(int code, String message, Response.Status status) {
this.code = code;
this.message = message;
this.status = status;
}
ResponseCode(int code, String message) {
this(code, message, Response.Status.INTERNAL_SERVER_ERROR);
}
}
這裡我定義了一個AuroraRuntimeException
的異常,就是在上面的例外處理函數中,用到的異常。每個異常範例會有一個對應的異常碼,也就是前面剛定義好的。
@Getter
public class AuroraRuntimeException extends RuntimeException {
private final ResponseCode code;
public AuroraRuntimeException() {
super(String.format("%s", ResponseCode.INTERNAL_ERROR.getMessage()));
this.code = ResponseCode.INTERNAL_ERROR;
}
public AuroraRuntimeException(Throwable e) {
super(e);
this.code = ResponseCode.INTERNAL_ERROR;
}
public AuroraRuntimeException(String msg) {
this(ResponseCode.INTERNAL_ERROR, msg);
}
public AuroraRuntimeException(ResponseCode code) {
super(String.format("%s", code.getMessage()));
this.code = code;
}
public AuroraRuntimeException(ResponseCode code, String msg) {
super(msg);
this.code = code;
}
}
為了保證各個介面的返回統一,這裡專門定義了一個返回型別。
@Getter
@Setter
public class GenericResponse<T> {
private int code;
private T data;
private String message;
public GenericResponse() {};
public GenericResponse(int code, T data) {
this.code = code;
this.data = data;
}
public GenericResponse(int code, T data, String message) {
this(code, data);
this.message = message;
}
public GenericResponse(ResponseCode responseCode) {
this.code = responseCode.getCode();
this.data = null;
this.message = responseCode.getMessage();
}
public GenericResponse(ResponseCode responseCode, T data) {
this(responseCode);
this.data = data;
}
public GenericResponse(ResponseCode responseCode, T data, String message) {
this(responseCode, data);
this.message = message;
}
}
下面的例子中,我們想獲取到使用者的資訊,如果使用者的資訊不存在,可以直接丟擲一個異常,這個異常會被我們上面定義的全域性例外處理方法所捕獲,然後根據不同的異常編碼,完成不同的處理和返回。
public User getUserInfo(Long userId) {
// some logic
User user = daoFactory.getExtendedUserMapper().selectByPrimaryKey(userId);
if (user == null) {
throw new AuroraRuntimeException(ResponseCode.USER_INPUT_ERROR, "使用者id不存在");
}
// some logic
....
}
以上就完成了整個全域性異常的處理過程,接下來重點說說為什麼@RestControllerAdvice
和@ExceptionHandler
結合使用可以攔截程式中產生的異常?
下面會提到
@ControllerAdvice
註解,簡單地說,@RestControllerAdvice與@ControllerAdvice的區別就和@RestController與@Controller的區別類似,@RestControllerAdvice註解包含了@ControllerAdvice註解和@ResponseBody註解。
接下來我們深入Spring原始碼,看看是怎麼實現的,首先DispatcherServlet物件在建立時會初始化一系列的物件,這裡重點關注函數initHandlerExceptionResolvers(context);
.
public class DispatcherServlet extends FrameworkServlet {
// ......
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
// 重點關注
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
// ......
}
在initHandlerExceptionResolvers(context)方法中,會取得所有實現了HandlerExceptionResolver介面的bean並儲存起來,其中就有一個型別為ExceptionHandlerExceptionResolver的bean,這個bean在應用啟動過程中會獲取所有被@ControllerAdvice註解標註的bean物件做進一步處理,關鍵程式碼在這裡:
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
// ......
private void initExceptionHandlerAdviceCache() {
// ......
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
// 找到所有ExceptionHandler標註的方法並儲存成一個ExceptionHandlerMethodResolver型別的物件快取起來
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
if (logger.isInfoEnabled()) {
logger.info("Detected @ExceptionHandler methods in " + adviceBean);
}
}
// ......
}
}
// ......
}
當Controller丟擲異常時,DispatcherServlet通過ExceptionHandlerExceptionResolver來解析異常,而ExceptionHandlerExceptionResolver又通過ExceptionHandlerMethodResolver 來解析異常, ExceptionHandlerMethodResolver 最終解析異常找到適用的@ExceptionHandler標註的方法是這裡:
public class ExceptionHandlerMethodResolver {
// ......
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
// 找到所有適用於Controller丟擲異常的處理方法,例如Controller丟擲的異常
// 是AuroraRuntimeException(繼承自RuntimeException),那麼@ExceptionHandler(AuroraRuntimeException.class)和
// @ExceptionHandler(Exception.class)標註的方法都適用此異常
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
/* 這裡通過排序找到最適用的方法,排序的規則依據丟擲異常相對於宣告異常的深度,例如
Controller丟擲的異常是是AuroraRuntimeException(繼承自RuntimeException),那麼AuroraRuntimeException
相對於@ExceptionHandler(AuroraRuntimeException.class)宣告的AuroraRuntimeException.class其深度是0,
相對於@ExceptionHandler(Exception.class)宣告的Exception.class其深度是2,所以
@ExceptionHandler(BizException.class)標註的方法會排在前面 */
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
// ......
}
整個@RestControllerAdvice
處理的流程就是這樣,結合@ExceptionHandler
就完成了對不同異常的靈活處理。
關注公眾號【碼老思】,第一時間獲取最通俗易懂的原創技術乾貨。