文章目录
- spring mvc异常处理(源码分析)
- 概述
- 原理(源码角度)模拟debug
- 前期提要
- 分析
- 4个map
- 4个map的初始化
- 为什么需要基于mappedMethods缓存
- 总结一下
spring mvc异常处理(源码分析)
概述
spring mvc有下面三种方式实现异常处理:
分别是:
- 实现handlerExceptionResolver+@Component(上古版本)
- controller里耦合@ExceptionHandler(优先级最高)
- @ControllerAdvice+@ExceptionHandler(最常用)
1.在对应类实现spring的异常处理核心组件handlerExceptionResolver+@Component(在多个异常执行时的优先级最低,并且麻烦,最早期的异常处理)
@ExceptionHandler注解在controller方法上(优先级高于ControllerAdvice,但比较麻烦)
@ControllerAdvice+@ExceptionHandler统一异常处理(最常用)
原理(源码角度)模拟debug
从源码角度分析,spring mvc是如何进行统一异常处理的?
为了更好的说明问题,我选择用倒推,先去模拟错误发生。
前期提要
1.我定义了一个controller,有一个deleteBook方法,在这个方法中会对传来的bookId检查,非法则会抛出一个runtimeException
2.我用上述三种spring mvc异常处理方法都加在了项目中:如概述的图片所示
3.向deleteBook发送一个id非法的请求。
4.在processHandlerException方法处打上断点。
分析
可以看到这里马上去执行resolveException(这正是spring mvc的resolveException方法,若用上古处理方法,你需要去重写它。
而在执行resolveException,会依次走更抽象的顶层方法,然后来到核心方法doResolveHandlerMethodException
在doResolveHandlerMethodException
方法中:他会干下面的事情
-
1.根据HandlerMethod(要处理的方法)和exception获取异常处理的Method(先从exceptionHandlerCache查,再从advice中查)
-
2.设置异常处理方法的参数解析器和返回值解析器(一般就是之前默认的argumentResolvers和returnValueHandlers)
-
3.执行具体的异常处理方法(也就是1找到的Method)
-
4.对返回的视图模型进行处理(这个不用太纠结,因为一般不返回视图了,并且一般在@ExceptionHandler标注的方法中我们会对请求处理的,所以一般会返回new ModelAndView(),表示不对视图进行进一步处理)
重点就是如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method
先理解一下exceptionHandlerCache和exceptionHandlerAdviceCache。因为Method就从他们身上获取的。
4个map
exceptionHandlerCache,exceptionHandlerAdviceCache,mappedMethods,基于mappedMethods的缓存
直接看定义
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>();
exceptionHandlerCache存储@RequestMapping对应的ExceptionHandlerMethodResolver(就是本例中在@controller里的@ExceptionHandler)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
exceptionHandlerAdviceCache保存了@ControllerAdvice对应的ExceptionHandlerMethodResolver(在本例中就是global那个类)
还有mappedMethods,封装的异常对应的处理方法。和基于它的mappedMethods缓存
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
为什么需要这个缓存后面说
回到如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method这个问题上来
直接看详细注释:
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
//0.获取handlerMethod(deleteBook)的类(BookController)
handlerType = handlerMethod.getBeanType();
//1.从exceptionHandlerCache查(@Controller里的@ExpectionHandler)查一个resolver
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
//2.第一次一般为null,这时会根据handlerType去new一个resolver,若有handlerType对应的异常方法,这时这个resolver的mappedMethods有这个方法
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
//3.根据异常解析这个方法,先从mappedMethods的缓存中查,查不出到mappedMethods查,然后写入缓存
Method method = resolver.resolveMethod(exception);
if (method != null) {
//4.解析出方法了,在这个ServletInvocableHandlerMethod会回调具体方法
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
//5.和上面类似,只是遍历所有的exceptionHandlerAdviceCache(@ControllerAdvice),从每一个entry里找出resolver,试图解析出exception对应的方法,然后调用
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
经过测试,在本例中,
若存在三种异常处理方式
第一次执行:走234
第二次执行:走1(exceptionHandlerCache已被写入)
若把@Controller耦合@ExceptionHandler去掉,则走advice的cache(也就是global)。
若把global也去掉,则走最后那个上古版本的resolver,不会到这个getExceptionHandlerMethod方法的。直接执行resolveException了
所以优先级:@Controller耦合@ExceptionHandler > @ControllerAdvice+@ExceptionHandler > 实现handlerExceptionResolver+@Component(上古版本)
4个map的初始化
上面在探究如何根据HandlerMethod(要处理的方法)和exception获取异常处理的Method中,你会好奇这些map什么时候被初始化的
先说结论:
-
exceptionHandlerAdviceCache在ExceptionHandlerExceptionResolver被初始化的过程的afterPropertiesSet方法中赋值
-
exceptionHandlerCache则是在执行异常时,碰到了一场异常要处理了,再去初始化(也就是
第二次执行
中exceptionHandlerCache被写入)
handlerExceptionResolver作为bean注入容器:
spring mvc通过 WebMvcConfigurationSupport类(configuration类)+@Bean注解
的方式来注入handlerExceptionResolver bean
并在上述的#addDefaultHandlerExceptionResolvers
方法中,注册了3个处理器:分别处理@ExceptionHandler,@ResponseStatus标注的方法和默认异常解析器。
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
//钩子方法,用于向列表中添加用户自定义的异常处理器。
configureHandlerExceptionResolvers(exceptionResolvers);
//向列表中添加Spring MVC的默认异常处理器
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
}
//扩展或修改异常处理器列表
extendHandlerExceptionResolvers(exceptionResolvers);
//返回一个HandlerExceptionResolverComposite实例,它是handlerExceptionResolver一种实现
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers);
return composite;
}
在addDefaultHandlerExceptionResolvers
protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
ContentNegotiationManager mvcContentNegotiationManager) {
ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
//设置内容协商处理器(确定相应格式),消息转换器(java<->json),
exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
exceptionHandlerResolver.setMessageConverters(getMessageConverters());
exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
//若jackson存在,则给ResponseBodyAdvice设置一个JsonViewResponseBodyAdvice实例,用于处理jackson的jsonview
if (jackson2Present) {
exceptionHandlerResolver.setResponseBodyAdvice(
Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
if (this.applicationContext != null) {
exceptionHandlerResolver.setApplicationContext(this.applicationContext);
}
//调用afterPropertiesSet,完成初始化
exceptionHandlerResolver.afterPropertiesSet();
exceptionResolvers.add(exceptionHandlerResolver);
ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
responseStatusResolver.setMessageSource(this.applicationContext);
exceptionResolvers.add(responseStatusResolver);
exceptionResolvers.add(new DefaultHandlerExceptionResolver());
//添加3个ExceptionResolver:分别处理@ExceptionHandler,@ResponseStatus,默认异常解析器
}
在exceptionHandlerResolver.afterPropertiesSet()中:
1.找到所有@ControllerAdvice注解的类,注册为bean
2.将每一个@ControllerAdvice注解的类与ExceptionHandlerMethodResolver的对应关系 写入exceptionHandlerAdviceCache中。
3.初始化argumentResolvers和returnValueHandlers(一般就是spring默认提供的各种processor和handler)
如returnValueHandlers的HttpEntityMethodProcessor,ModelAndViewMethodReturnValueHandler等等。
如argumentResolvers的**@SessionAttribute**,@RequestAttribute。
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
//获取所有@ControllerAdvice的类
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
if (logger.isDebugEnabled()) {
int handlerSize = this.exceptionHandlerAdviceCache.size();
int adviceSize = this.responseBodyAdvice.size();
if (handlerSize == 0 && adviceSize == 0) {
logger.debug("ControllerAdvice beans: none");
}
else {
logger.debug("ControllerAdvice beans: " +
handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
}
}
}
//RequestMappingHandlerAdapter.java
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
https://www.cnblogs.com/java-chen-hao/p/11190659.html#_label0
https://blog.csdn.net/qq_26222859/article/details/51320493
https://blog.csdn.net/zzti_erlie/article/details/105746203
为什么需要基于mappedMethods缓存
因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。
由于最后只执行最近的那个method,那么我们只关心这个method就OK,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
还是不会写源码分析,小小总结一下
总结一下
-
spring mvc有下面三种方式实现异常处理:
-
实现handlerExceptionResolver+@Component(上古版本)
-
controller里耦合@ExceptionHandler(优先级最高)
-
@ControllerAdvice+@ExceptionHandler(最常用)
-
-
若存在三种异常处理时,优先级为:@Controller耦合@ExceptionHandler > @ControllerAdvice+@ExceptionHandler > 实现handlerExceptionResolver+@Component(上古版本)(执行一次就ok)
- 为什么:先查ExceptionHandler的cache,尝试解析一个方法,解析不出来再去查advice的cache。
-
spring mvc异常处理如何实现的
- 当打开webMvcConfiguration后,会注入handlerExceptionResolver(一个bean),这个handlerExceptionResolver会注入@ExceptionHandler,@ResponseStatus,默认异常解析器3个处理器,最值得关注的是ExceptionHandler处理器。
- 对于ExceptionHandler的处理器,会调用afterPropertiesSet方法完成initExceptionHandlerAdviceCache的注入
- 当异常发生后,在执行resolveException会一层一层到doResolveHandlerMethodException方法,在这里寻找该异常匹配的方法,并回调。(完成类似aop切面的效果)。
-
为什么mappedMethods需要缓存
- 因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。
- 由于最后只执行最近的那个method,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序
onHandler处理器。 - 对于ExceptionHandler的处理器,会调用afterPropertiesSet方法完成initExceptionHandlerAdviceCache的注入
- 当异常发生后,在执行resolveException会一层一层到doResolveHandlerMethodException方法,在这里寻找该异常匹配的方法,并回调。(完成类似aop切面的效果)。
-
为什么mappedMethods需要缓存
- 因为一个异常可能有多个匹配方法,这时需要按照继承优先级对它进行排序,执行最近的那个。
- 由于最后只执行最近的那个method,所以将最近的这个method写入一个map,在每次查询时直接去查这个缓存并返回就好了,避免了排序