Spring MVC源码解析之组件解析:HandlerExceptionResolver(处理器异常解析器)

在这里插入图片描述

使用介绍

一般自定义异常处理策略有两种方式

  1. 使用@ExceptionHandler注解
  2. 实现HandlerExceptionResolver接口

因为@ExceptionHandler注解的方式已经足够强大,所以我们一般也很少通过实现HandlerExceptionResolver来自定义异常处理策略。

简单介绍一下@ExceptionHandler的使用,后面会结合这些例子进行源码分析

@RestController
@RequestMapping("location")
public class LocationController {

	@RequestMapping("getLocationInfo")
	public String index() {
		int sum = 10 / 0;
		return "locationInfo";
	}

	@ExceptionHandler(RuntimeException.class)
	public String processRuntimeException() {
		return "LocationController -> 发生RuntimeException";
	}

	@ExceptionHandler(Exception.class)
	public String processException() {
		return "LocationController -> 发生Exception";
	}
}

访问如下链接,返回结果为

http://localhost:8080/location/getLocationInfo
LocationController -> 发生RuntimeException

把processRuntimeException方法注释掉以后,再次访问上面的链接,结果为

LocationController -> 发生Exception

如果在每个Controller里面都写异常解析器还是很麻烦的,能不能在一个地方统一处理异常呢?当然可以,这时候就不得不用到@RestControllerAdvice或者@ControllerAdvice

写如下的全局异常解析器

@RestControllerAdvice
public class MyExceptionHandler {

	@ExceptionHandler(RuntimeException.class)
	public String processRuntimeException() {
		return "MyExceptionHandler -> 发生RuntimeException";
	}

	@ExceptionHandler(Exception.class)
	public String processException() {
		return "MyExceptionHandler -> 发生RuntimeException";
	}
}

访问上面的链接,返回结果为

LocationController -> 发生Exception

我们把LocationController类的processException方法也注释掉,此时LocationController类里面已经没有被@ExceptionHandler注解标记的方法了

访问上面的链接,返回结果为

MyExceptionHandler -> 发生RuntimeException

把MyExceptionHandler中的processRuntimeException方法注释掉
访问上面的链接,返回结果为

MyExceptionHandler -> 发生Exception

通过以上的例子,我们可以得出如下结论

  1. @RestControllerAdvice或者@ControllerAdvice类内的解析器的优先级低于@RequestMapping类的解析器的优先级
  2. 如果一个异常能被多个解析器所处理,则选择继承关系最近的解析器

假设BizException继承自NullPointException
A方法解析BizException
B方法解析NullPointException
C方法解析Exception

BizException会被A方法解析
NullPointException会被B方法解析
如果没有A方法,则BizException会被B方法解析,如果B方法也没有,则被C方法解析,不难理解哈

@RestControllerAdvice和@ControllerAdvice有什么区别呢?

名字上就可以猜出@RestControllerAdvice只是在@ControllerAdvice的基础上加了@ResponseBody注解,看一波源码也确实如此。所以@RestControllerAdvice类最终返回的是JSON,@ControllerAdvice最终返回的是视图。如果你不明白为什么加了@ResponseBody注解最终返回的内容为JSON,建议看一下返回值处理器相关的内容

源码分析

异常解析器接口定义如下

public interface HandlerExceptionResolver {

	// 将异常封装为ModelAndView后返回
	@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, 
			@Nullable Object handler, Exception ex);

}

Spring MVC默认的异常解析器存放在如下属性中

@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;

在这里插入图片描述
顺序依次为

ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver

UML图如下
在这里插入图片描述
Order接口是用来排序的哈,Spring MVC默认的解析器不是通过Order接口来控制顺序的,因为默认的解析器都继承自AbstractHandlerExceptionResolver,并且都没有重写getOrder方法

对Spring MVC比较清楚的小伙伴应该都知道DispatcherServlet属性的默认实现都定义在源码包的DispatcherServlet.properties文件中,List的顺序也是按这个来的。放一部分内容

org.springframework.web.servlet.HandlerAdapter=
    org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=
    org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionReso

接下来分析这3个默认的HandlerExceptionResolver

ExceptionHandlerExceptionResolver

ExceptionHandlerExceptionResolver用于支持@ExceptionHandler,而@ExceptionHandler应该是我们最常的,方便我们自定义异常处理策略,比通过实现HandlerExceptionResolver接口的方式简单

从AbstractHandlerMethodExceptionResolver#shouldApplyTo可以看到

@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
	if (handler == null) {
		// handler为空,交给父类去判断
		// 默认该逻辑返回true
		return super.shouldApplyTo(request, null);
	}
	else if (handler instanceof HandlerMethod) {
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		handler = handlerMethod.getBean();
		// 交给父类判断
		return super.shouldApplyTo(request, handler);
	}
	else {
		// 不支持
		return false;
	}
}

只有当handler为空或者handler的类型为HandlerMethod时(@RequestMapping返回的类型为HandlerMethod)才会执行后面的异常解析逻辑。所以你通过实现Controller接口或者实现HttpRequestHandler接口定义的Handler,这个注解是不起作用的

@ExceptionHandler的处理过程主要和下面2个类有关系ExceptionHandlerExceptionResolver,ExceptionHandlerMethodResolver

用几个成员变量说一下处理过程,就不贴过多的代码了

ExceptionHandlerExceptionResolver

// 省略了继承和实现关系
public class ExceptionHandlerExceptionResolver {

	@Nullable
	private HandlerMethodArgumentResolverComposite argumentResolvers;

	@Nullable
	private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

	private List<HttpMessageConverter<?>> messageConverters;


	// 被@RequestMapping标记的类 -> ExceptionHandlerMethodResolver
	private final Map<Class<?>, ExceptionHandlerMethodResolver> 
	exceptionHandlerCache = new ConcurrentHashMap<>(64);

	// 被@ControllerAdvice注解标记的类 -> ExceptionHandlerMethodResolver
	private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>
	exceptionHandlerAdviceCache = new LinkedHashMap<>();
}

可以看到ExceptionHandlerExceptionResolver类定义了自己的参数处理器,返回值处理器,消息转换器。所以你可以通过这些组件反向知道@ExceptionHandler方法支持的参数类型

例如从如下方法可以知道,支持的参数类型为@SessionAttribute,@RequestAttribute等
如果你写个@RequestParam是肯定不会注入进来的

protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
	List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

	// Annotation-based argument resolution
	resolvers.add(new SessionAttributeMethodArgumentResolver());
	resolvers.add(new RequestAttributeMethodArgumentResolver());

	// Type-based argument resolution
	resolvers.add(new ServletRequestMethodArgumentResolver());
	resolvers.add(new ServletResponseMethodArgumentResolver());
	resolvers.add(new RedirectAttributesMethodArgumentResolver());
	resolvers.add(new ModelMethodProcessor());

	// Custom arguments
	if (getCustomArgumentResolvers() != null) {
		resolvers.addAll(getCustomArgumentResolvers());
	}

	return resolvers;
}

最重要的4个map来了,ExceptionHandlerExceptionResolver的工作过程主要就是操作这4个map

// 省略了继承和实现关系
public class ExceptionHandlerExceptionResolver {

	// 被@RequestMapping标记的类 -> ExceptionHandlerMethodResolver
	private final Map<Class<?>, ExceptionHandlerMethodResolver>
	exceptionHandlerCache = new ConcurrentHashMap<>(64);

	// 被@ControllerAdvice注解标记的类 -> ExceptionHandlerMethodResolver
	private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver>
	exceptionHandlerAdviceCache = new LinkedHashMap<>();
			
}

exceptionHandlerCache保存了@RequestMapping对应的ExceptionHandlerMethodResolver,是在执行异常解析的过程中被赋值的

exceptionHandlerAdviceCache保存了@ControllerAdvice对应的
ExceptionHandlerMethodResolver,是在ExceptionHandlerExceptionResolver被初始化的过程中赋值的

而ExceptionHandlerMethodResolver你可以认为只是封装了一下Exception及其对应的Method

以最开始的例子演示,ExceptionHandlerExceptionResolver初始化后
在这里插入图片描述
此时exceptionHandlerCache是没有值的
访问如下链接后

http://localhost:8080/location/getLocationInfo

exceptionHandlerCache中的值如下,LocationController及其对应的ExceptionHandlerMethodResolver被放了进来
在这里插入图片描述
追一下以下方法的执行
ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
ExceptionHandlerExceptionResolver#getExceptionHandlerMethod

可以得出我们测试的结论
@RestControllerAdvice或者@ControllerAdvice类内的解析器的优先级低于@RequestMapping类的解析器的优先级

总体实现也不难,从exceptionHandlerCache中能找到解析器就返回执行,找不到就从exceptionHandlerAdviceCache中找,这不是就实现了优先级了吗?

接着来看剩下的2个Map

public class ExceptionHandlerMethodResolver {


	// 异常 -> 对应的处理方法
	private final Map<Class<? extends Throwable>, Method>
	mappedMethods = new HashMap<>(16);

	// 异常 -> 对应的处理方法
	// 这个是基于mappedMethods又做了一次缓存
	// 为什么要再做一次缓存呢?
	// 是因为根据异常类型获取处理方法的时候,一个异常可能有多个处理方法,即一个异常会从mappedMethods中查出多个处理方法
	// 最后返回的是继承关系最近的异常对应的处理方法,所以在查找的时候又做了一次缓存,避免每次查mappedMethods然后取最优值
	// 从exceptionLookupCache中就可以直接查到最优的处理方法
	private final Map<Class<? extends Throwable>, Method>
	exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
	
}

@ControllerAdvice的mappedMethods是在ExceptionHandlerExceptionResolver初始化的过程中赋值的
在这里插入图片描述

@RequestMapping的mappedMethods是在执行异常解析的过程中被赋值的
在这里插入图片描述
而exceptionLookupCache是在异常解析过程中,通过Exception查找Method的过程中基于mappedMethods做的缓存

为什么在查找过程中要再做一次缓存呢?

是因为根据异常类型获取处理方法的时候,一个异常可能有多个处理方法,即一个异常会从mappedMethods中查出多个处理方法,最后返回的是继承关系最近的异常对应的处理方法,所以在查找的时候又做了一次缓存,避免每次查mappedMethods然后取最优值。
从exceptionLookupCache中就可以直接查到最优的处理方法

以LocationController为例,查找一次异常后,exceptionLookupCache的值如下
在这里插入图片描述
这样当再次发生ArithmeticException异常时就能从exceptionLookupCache找到对应的处理方法

ResponseStatusExceptionResolver

ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver的实现都不是很难,就不进行过多的分析了

ResponseStatusExceptionResolver主要用来处理如下异常

  1. 抛出的异常类型继承自ResponseStatusException
  2. 抛出的异常类型被@ResponseStatus标记

以一个例子来演示这个处理器的功能

@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class UnauthorizedException extends RuntimeException {
}
@RestController
@RequestMapping("shoppingCar")
public class ShoppingCarController {

	@RequestMapping("getCarInfo")
	public String index() {
		throw new UnauthorizedException();
	}
}

访问

http://localhost:8080/shoppingCar/getCarInfo

显示如下
在这里插入图片描述

DefaultHandlerExceptionResolver

用来处理一些常见的Http异常,如

400:请求无效
405:请求方法不支持
500:内部服务器错误

执行入口

DispatcherServlet#doService
DispatcherServlet#doDispatch
DispatcherServlet#processDispatchResult

# DispatcherServlet#processDispatchResult的部分方法
// 处理过程发生了异常
if (exception != null) {
	if (exception instanceof ModelAndViewDefiningException) {
		logger.debug("ModelAndViewDefiningException encountered", exception);
		// 直接使用异常中封装的ModelAndView作为最终的ModelAndView结果
		mv = ((ModelAndViewDefiningException) exception).getModelAndView();
	}
	else {
		// 其他异常类型,先获取解析器
		Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
		// 通过异常解析器将异常解析为一个错误视图
		mv = processHandlerException(request, response, handler, exception);
		errorView = (mv != null);
	}
}

如果整个处理过程发生异常,依次调用DispatcherServlet的成员变量handlerExceptionResolvers的resolveException方法,找到第一个不为null的ModelAndView,然后返回

@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;

欢迎关注

在这里插入图片描述

参考博客

好文
[0]https://juejin.im/post/5c4e6b6df265da61511519ae
[1]https://www.cnblogs.com/fangjian0423/p/springmvc-exception-analysis.html
[2]https://blog.csdn.net/andy_zhang2007/article/details/100154082
[3]https://www.jianshu.com/p/333ed5ee958d
[4]https://www.cnblogs.com/weiyinfu/p/6861074.html
好文
[5]https://www.cnblogs.com/lvbinbin2yujie/p/10574812.html

猜你喜欢

转载自blog.csdn.net/zzti_erlie/article/details/105746203