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("参数校验未通过");
            }
        }
    
    }
    
posted @ 2020-04-26 15:04  茶兮。  阅读(8198)  评论(2编辑  收藏  举报