springboot多個@ControllerAdvice全局異常處理


背景

在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處理,因為ExceptionCustomException的父類。
  • 若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方法優雅。

    1. 使用basePackageClasses屬性,將想要優先加載的包寫在前面;
    2. 使用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("參數校驗未通過");
            }
        }
    
    }
    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM