mvc中handler,aop,controllerAdvice执行顺序和异常情况下如何保证ThreadLocal的安全性

spring mvc

正常访问

interceptor的preHandle是比aop切面现执行的。

首先,从名字来讲,preHandle就是在处理之前,处理就是行执行这个方法。

而aop是针对方法的切面,而且最最关键的是preHandle的返回值能够决定方法是否执行,我能控制方法的执行,控制aop的执行实习太容易了。

此外,preHandle是无法获取方法执行的参数值的,而aop是可以获取参数值的,这个就说明,preHandle肯定是在aop之前就执行了。

controller方法执行完成后首先执行的是aop后置方法

这个也很好理解,aop是对普通方法的代理,也可以这么说,preHandle返回参数放行的就是整个aop包裹的方法。

正常访问时,post和after都会执行

post先执行的,原因看下面就行,post是在视图呈现之后,after是在之后。

post方法:在 HandlerAdapter 实际调用处理程序之后,但在 DispatcherServlet 呈现视图之前调用。

after方法:请求处理完成后的回调,即渲染视图后。将在处理程序执行的任何结果上调用,从而允许适当的资源清理。注意:仅当此拦截器的preHandle方法成功完成返回true时才会调用!

简单的分析一下官方的文档,官方容许我们在这里做资源的清理,比如TheadLocal,但是这个方法仅仅在成功和返回true的时候才会执行,也就是说,如果我们仅仅在这里清理资源肯定是要出问题的,通过这个我们可以猜出它的代码,就是一个一个执行,利用前一个的返回结果,判断下一个是否执行,这个和我自己写的一个权限框架的实现估计是一样的。

此外,我还发现,异步方法也是有拦截器的AsyncHandlerInterceptor,所以这个线程局部变量的清理还是很麻烦的,要是能做一个框架,适配spring的机制处理就好了。

现在我感觉这个接口还是很不错的,因为自己以前用它做权限管理的时候特别不方便,后来就该用aop了,前段时间,想着自己利用aop封装一个,但是奈何aop实现不了我想要的功能,所以我们只好使用这个接口,没想到效果还是非常好的!

preHandle拒绝

有两种情况,一种是返回false,一种是抛出异常,我猜两个的效果是一样的,但是我更想知道,这里面的异常能够被拦截住吗?

好,说干就干。

测试了一下,和预想的一样,链路瞬间被切断了。

我试一试抛出异常会怎么样。先来一个可以捕获的异常,其实我觉得异常就应该把最大的throwable捕获了,放过异常可是很麻烦的。

和想的差不多,就是在原来的基础上,捕获了一下,不过我发现pre-handle竟然执行了两次,这不得debug看看?我发现问题了,因为我设置的是拦截全部/**路径,而且别说执行两次了,pre-handle实际上执行了好多次呢,这样下去系统能受得了?那不行,肯定不能这么干。我说的可以不是抛出异常,而且路径配置的问题。

然后我说利用浏览器看看走了哪些路径,可想到人家没有跳转,那线索可不能这么中断了。但是是从日志里面看,是一个/error的路径,说明是内部转发的,那不得无限嵌套啊,我赶紧排除它试一试。

唉,连返回页面都出来了,不是tomcat而是spring的了,但是我还是看到它有两个pre-handler,唉奇了怪了。我再改一改,排除/error和/error/**都是这样,但这可是大问题啊,白白执行两次。

好家伙,还真是两次,那怎么办呢,我先自己看源码,实现不行,就百度。

记录一下,第一次是在doDispatch里面获取到的,这个时候Exception dispatchException = null,捕获异常之后给它附上了一个值。然后processDispatchResult处理了一下,就给返回我的prehandler里面了,但是第二次不抛出了,我还以为是dispatchException的问题,看来还得看源码里面发生了什么。

HandlerExceptionResolverComposite抓住了我的异常,因为我就是想看看,两次到底有什么不一样,此外我还注意到清除了一个request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE)

好家伙,里面没有这个熟悉,那看来和他也没有关系了,同时我也早就想到了把过滤器弄的和我的路径一样,但是这样更不解决不了问题,你让我的框架怎么办,一个一个配置吗?那不行!

我发现的就是,第一遍处理完成后request里面的属性变多了,而且还把我的异常属性放到里面了。看来基本原理就是利用这些属性了,但是我更关注的是,怎么解决问题?

而且第二次没有找到HandlerExceptionResolver去处理我的异常,所以就没有处理,直接把我的异常抛出了,那现在问题又进一步被锁定了!怎么找这个处理器?

 for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
     exMv = resolver.resolveException(request, response, handler, ex);
     if (exMv != null) {
         break;
     }
 }
复制代码

我知道第二个能处理我的异常,我一进去,好家伙,又是一次循环,而且很类似

 for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
     ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
     if (mav != null) {
         return mav;
     }
 }
复制代码

再到里面,这个八九不离十就是核心判断方法了。

 shouldApplyTo(request, handler)
复制代码

后面走的太多了,我就调重点说了,我看到它把我的所有异常都给保存起来,放到了一个数组里面。

然后就选择了一个处理返回值的handler,差不多很生成刚刚返回的网页内容了,然后第二次处理的时候,就找不到对应的handler了,我只能说是浅浅的原因,但是问题还是没有解决。

我先试一试指定详细的处理url吧。好家伙,就执行了一次,说明有救,说明就是url的问题,

/test/normal 我把url 打印出来了,/test/抛出了可以接受的异常你没看错,真的是这样的,无语了。

我百度一下,还真有大佬知道,我直接给跪了。它说只有返回值为string或者void才会这样,那不就好了嘛,谁会这样返回啊。

首先给解决办法

 public class CustomizedHandlerAdapter extends RequestMappingHandlerAdapter {
 ​
     @Override
     public void afterPropertiesSet() {
         super.afterPropertiesSet();
         setReturnValueHandlers(getReturnValueHandlers().stream().filter(
                 h -> h.getClass() != ViewNameMethodReturnValueHandler.class
         ).collect(Collectors.toList()));
     }
 }
 ​
 public class HandlerVoidMethod extends ViewNameMethodReturnValueHandler {
 ​
     @Override
     public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
         /*
          * 这里只处理了void返回值的方法,对于String返回值的方法则没有处理,原因是系统中可能还会用到springmvc的视图功能(例如jsp)
          * 如果说是前后分离的项目,springmvc层只提供纯接口的话,那么可以将下面代码全部删除,
          * 只写上一行  mavContainer.setRequestHandled(true);  即可
          */
         if (void.class == returnType.getParameterType()) {
             mavContainer.setRequestHandled(true);//这行代码是重点,它的作用是告诉其他组件本次请求已经被程序内部处理完毕,可以直接放行了
         } else {
             super.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
         }
     }
 }
 ​
 @Configuration
 public class WebConfig {
 ​
     @Bean
     public HandlerVoidMethod handlerVoidMethod() {
         return new HandlerVoidMethod();
     }
 ​
     @Bean
     public CustomizedHandlerAdapter handlerAdapter(HandlerVoidMethod handlerVoidMethod) {
         CustomizedHandlerAdapter chl = new CustomizedHandlerAdapter();
         chl.setCustomReturnValueHandlers(Arrays.asList(handlerVoidMethod));
         return chl;
     }
 }
复制代码

看来问题的原因和我分析的一样,就是请求的转发,而且是tomcat的锅,但是后面的这些,我只能说,我是废物。但是我尝试了一下,还是执行了两遍,而且上面两个还真不能加Compant注解。那我试一试其他类型的吧。有点失望,还是两次,这可麻烦了啊。

但是我找到我写的框架,发现唉,这个方法竟然可以避免这种问题唉,我再换string,等一下这好像不是解决,这是不抛出异常了哈,那为什么我的权限框架是正常的,不行了,晕了。

 if (!(handler instanceof HandlerMethod)) {
     return true;
 }//好像是来自satoken的一段代码,我当时aop无路的时候看了它的,然后把自己的代码改了,牛逼牛逼
复制代码

不对,异常不但被抛出了,而且还被拦截住了!太棒了,太棒了!!!而且最可喜的是什么,post和after也执行了,也就是说,你只要捕获了全部异常,在pre-handle里面的的异常是完全没有问题的,后面的资源清理工作也绰绰有余。

但是你发现了吗?如果我们的异常被捕获到了,after方法是会执行的哈,而且postHandle里面有modelAndView参数可以让我们操控一把返回值,等一下,这竟然是在异常处理后返回的,哈哈,那这可就牛逼了,我可以处理这些包装异常的结果了,而且after还有抛出的异常,不错,虽然我感觉没有用。但是这两个方法也可能遇到false的风险呢,也就是说!!

如果我在pre使用了threadLocal,我要在返回false之前处理干净里面的变量,同时,我也要在post和after也处理threadLocal,毕竟多处理又没有事情。不过我没有返回false的情况,都是抛出异常,那就说明,如果你都捕获到了,我的清理工作是成功的。所以,请务必在异常处理中捕获全部的异常!!!

我有的框架,比如参数校验框架的实现原理是aop,aop那不用太担心,我在finall方法清理threadLocal不就行了,我现在就担心前面抛出告知用户的异常,和快速失败模式的异常,其实也不用我去猜,我感觉是成功不了的。但是首先我得把相应都换成类,不然可能掉进字符串的坑,因为的ResponseBodyAdvice,唉,等等我忘记标注解了,哎,我的错,罚自己再测试一遍!

哎呀,这个是在aop结束之后,post和after之前执行,我试一试给它抛出一个异常,如果这个还能执行,那我就在这个里面清除aop的threadLocal。哎呀,可惜了,因为没有返回结果,所以,没有执行成功,倒也合理哈

基本执行流程梳理

我先简单梳理一下基本执行逻辑

pre如果返回false的话,都不会执行,如果抛出异常了,但是被捕获了,post和after都会执行

  • pre-handler

    • aop
    • ResponseBodyAdvice
  • post

  • after

mvc参数转换异常

其中全局异常处理器,我感觉就是什么时候抛出异常了,什么时候立马执行,我给尝试。当然,我们从现在开始就不考虑捕获不到的了,因为没有什么意义。

有点意外,在controller放出的异常竟然捕获不到?

好吧,实际证明了两点,after抛出的异常是无法处理的,所以你要特别的谨慎,确保after不会出现错误,但是,问题也不是很大,在post和after的异常是不会影响返回结果的!

但是controller还是让我有点懵逼,虽然说我controller处理一个异常是小问题,等一下,那这样也说明一点,不要在controller做复杂的时候,异常抛出之后,谁也救不了你。此外,虽然没有什么用,我试一试参数解析出错会这么样吧。

好家伙,我不传递参数的话,异常肯定是能捕获到的,但是aop执行不了了,当然这都是正常的,麻烦的是,after竟然执行了两次,这个我也不深究原理了,毕竟就做一些清理工作,等下看看satoken是怎么清理的吧。所以我的参数校验aop也使用了,如果你参数传递有问题,我肯定是不执行aop的。

所以,参数的确定肯定是在pre-handler之后,aop之前的。

总结

  1. 全局异常处理器必不可少,而且必须要能捕获Throwable,而且它可以捕获除了after和controller以外的所有异常,而且controller也是,只要这个异常不是你主动抛出来的,就是你直接throw出来,不是通过统一返回体处理的,就没有问题!
  2. 关于新事物ResponseBodyAdvice,如果我们在前面抛出异常,是执行不到的。但是在controller抛出的异常,不管什么方式,全局异常处理器处理了不了,但是ResponseBodyAdvice会执行,debug能看到body参数是null。
  3. 但是!!!最最有用的来了,你知道为什么捕获不了吗?你明明记得自己的项目可以捕获的,这个原因就是你在aop里面捕获了异常,但是没有抛出!!!所以,我的建议是不要捕获,你直接finally处理就可以了。但是遗憾的是,全局异常处理器捕获后,ResponseBodyAdvice执行不了。
  4. 那我如果想实现日志框架的话,在哪里能保证这个返回的结果的,你像我现在,如果正常执行,我在aop可以拿到返回值,但可惜一旦产生异常,aop返回的结果就是空的。所以我打算直接debug,寻找自己的出路了!

千万要设置可以捕获全部异常的全局异常处理器,千万不要在aop中把捕获到的错误打印堆栈但是不抛出。

HandlerInterceptor的垃圾在post和after都清理一遍

aop的垃圾在finally要清理一遍,但是可能会抛出异常清理不掉,所以你要在aop执行前清理一遍

猜你喜欢

转载自juejin.im/post/7061789527395811365