背景
在springboot多模塊中, common模塊有全局異常處理, A模塊引用了common模塊, 且A模塊中有自己的全局異常處理, 在有些服務中是A中的全局異常處理生效, 有些服務中是common模塊中的全局異常處理生效. 非常疑惑, 了解后寫下此篇.
簡單描述
先加載的@ControllerAdvice
類里如果存在@ExceptionHandler(xxException.class)
是需要捕獲的異常或其父類,則將使用先加載的類中的異常處理方式。如果沒有,則看后面的@ControllerAdvice
類里是否有。
可以使用@Order
來決定加載優先級,網上也有說法可以使用@Primary
,暫未自測,個人覺得應該也是可行的。
舉例:
A類和B類都有@ControllerAdvice
注解,要捕獲的異常為自定義異常CustomException
。
場景一:A中有@ExceptionHandler(CustomException.class)
,B中沒有,但B中有@ExceptionHandler(Exception.class)
,
- 若B加載順序優先於A,則
CustomException
異常會被B處理,因為Exception
是CustomException
的父類。 - 若A加載順序優先於B,則
CustomException
異常會被A處理。
場景二:A中有@ExceptionHandler(CustomException.class)
,B中沒有,且B中沒有任何@ExceptionHandler()
是CustomException
的父類
- 不管AB是何優先級加載,均會被A處理。
部分源碼理解及分析
處理
入口是ExceptionHandlerExceptionResolver.doResolveHandlerMethodException
,這里面主要看ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
方法,源碼如下:
- getExceptionHandlerMethod:
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
if (handlerMethod != null) {
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}
}
// 第一遍exceptionHandlerCache不會有值,會走到這個遍歷里來
// exceptionHandlerAdviceCache, 結構為:LinkedHashMap<ControllerAdviceBean,ExceptionHandlerMethodResolver>(),注意是有序的
for (Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
// 重點是這個方法,找到處理異常的方法返回,由上面入口執行異常處理
// 注意是調用的resolver.resolveMethod(),也就是resolver中的屬性都能獲取到
// 有個屬性是后面要用到的,存儲了@ControllerAdvice注解類的所有@ExceptionHandler方法:private final Map<Class<? extends Throwable>, Method> mappedMethods = new ConcurrentHashMap<Class<? extends Throwable>, Method>(16);
Method method = resolver.resolveMethod(exception);
// 這里只要method不為空就會返回,可以和前面例子中的順序問題結合理解
if (method != null) {
return new ServletInvocableHandlerMethod(entry.getKey().resolveBean(), method);
}
}
}
return null;
}
- resolver.resolveMethod:
public Method resolveMethod(Exception exception) {
// 這個方法進去看
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = resolveMethodByExceptionType(cause.getClass());
}
}
return method;
}
- resolveMethodByExceptionType:
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
// 這個方法進去看
method = getMappedMethod(exceptionType);
this.exceptionLookupCache.put(exceptionType, (method != null ? method : NO_METHOD_FOUND));
}
return (method != NO_METHOD_FOUND ? method : null);
}
- getMappedMethod:
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<Class<? extends Throwable>>();
// 遍歷this.mappedMethods.keySet(),是上文中提到的存儲了@ControllerAdvice注解類的所有@ExceptionHandler方法
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
// class1.isAssignableFrom(class2):一個類Class1和另一個類Class2是否相同 或 Class1是否是Class2的超類或接口
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 如果存在則不會返空,也就是第一遍遍歷如果有能處理異常的方法就會返回,不管是相同的異常類處理方法還是對其父類的處理方法
if (!matches.isEmpty()) {
// 排序, 如果當前@ControllerAdvice注解類中既存在相同異常類處理又存在父類異常處理,則會將相同異常類處理排在前面。這個排序沒有深究,多個父類排序規則不清楚。
Collections.sort(matches, new ExceptionDepthComparator(exceptionType));
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
- 總結:如果有多個
@ControllerAdvice
注解類,當第一個加載的注解類里有對需要捕獲異常的相同類/父類有方法處理,就會使用第一個處理方法。可使用@Order
控制加載順序。
載入
上面處理流程是遍歷exceptionHandlerAdviceCache
,故來看這個數據來源。
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Looking for exception mappings: " + getApplicationContext());
}
// 找到有@ControllerAdvice注解的類並排序,可以看到這里決定了上面的處理順序,但加載排序未仔細看
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
if (logger.isInfoEnabled()) {
logger.info("Detected @ExceptionHandler methods in " + adviceBean);
}
}
if (ResponseBodyAdvice.class.isAssignableFrom(adviceBean.getBeanType())) {
this.responseBodyAdvice.add(adviceBean);
if (logger.isInfoEnabled()) {
logger.info("Detected ResponseBodyAdvice implementation in " + adviceBean);
}
}
}
}
其他
-
個人理解哪個生效就是一個加載順序的問題。啟動類如果有
@ComponentScan
注解,那么還有其他方法,但沒有上述使用@Order
方法優雅。- 使用
basePackageClasses
屬性,將想要優先加載的包寫在前面; - 使用
excludeFilters
屬性排除不想要加載的類,該屬性使用方式多樣,可自行搜索查看。(例:@ComponentScan(excludeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = xx.class))
)
- 使用
-
關於全局異常處理的運用
全局異常處理代碼:
@RestController @ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Result handle(Exception e) { log.error("全局異常處理:", e); return new Result<>(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服務異常", null); } }
邏輯代碼:
@Service public class xxxServiceImpl { private void xxxFunction(int xxId) { // 省略邏輯代碼, CustomException為自定義異常, 繼承Exception throw new CustomException("參數校驗未通過"); } } }