zl程序教程

您现在的位置是:首页 >  后端

当前栏目

关于SpringMVC拦截器执行两遍的原因分析以及如何解决

SpringMVC执行 如何 解决 分析 关于 以及 原因
2023-09-11 14:19:34 时间

什么情况下HandlerInterceptor会执行两遍?

拦截器拦截了静态资源请求

先上代码:

 1 package com.atwu.miao.intercepter;
 2 
 3 import com.atwu.miao.service.UserService;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 6 
 7 import javax.servlet.http.HttpServletRequest;
 8 import javax.servlet.http.HttpServletResponse;
 9 
10 public class AuthIntercepter extends HandlerInterceptorAdapter {
11 
12     @Autowired
13     private UserService userService;
14 
15     @Override
16     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
17         String name = request.getParameter("name");
18         String password = request.getParameter("password");
19         return userService.hasUser(name,password);
20     }
21 }

断点调试发现,第一次进入断点时:

这里请求路径被拦截,是正确的。放行。。。

第二次进入断点时:

这里请求路径居然是小图标,诶!
所以问题的原因在于,拦截器拦截了不该拦截的静态资源请求。

请求的路径不存在或其他原因导致跳到/error页面

事发背景

  • 肯定是只调用了一次接口但是preHandle执行两次,这是有这个问题的前提
  • 创建demo接口的时候(实验新的检验规则),用post测试,此时接口上已经加上了@PassToken去掉鉴权(为了方便测试)。当用postman进行调用的时候,发现报权限错误,此时问题来了,不是已经有@PassToken放行了吗,怎么还会验证权限?

排查问题

  • 首先恢复案发现场,还原执行两次的那个过程
  • 查看两次拦截请求preHandle的不一样的地方,如果你够细心,基本能大致发现问题,发现第二次请求request.getServletPath()获取的方法非请求方法,而是“/error”。这method肯定不是我们需要的,那么此时就可以debug去找这个method出来的原因了。
  • 这里我就不还原debug的过程了,简单说一下method中“/error”出现的原因。

原因

  • 拦截器中preHandle第一次被执行,请求method正确,@PassToken生效,第一次拦截被放过,此时去找能够处理自己的处理器,但是没有找到,于是发出了一个error的请求(error请求的配置类是ErrorMvcAutoConfiguration,具体执行在BasicErrorController中,返回了一个404页面),这个请求被拦截器拦截了,所以输出了拦截语句但是请求路径是error,而不是浏览器发出的初始请求method。(org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error)

在controller(非restController)下:方法返回值类型为String类型或者Void类型(使用请求url作为视图名称),并且不存在对应的视图

先上代码:

 1 @Controller
 2 public class GreetingController {
 3 
 4     /**
 5      * 第一种情况:方法返回值类型为 void 类型,并且不存在RequestMapping对应的视图
 6      *
 7      * @param name
 8      * @throws IOException
 9      */
10     @RequestMapping("/greet1")
11     public void greet1(String name) throws IOException {
12         System.out.println("name = " + name);
13         RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
14         HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse();
15         PrintWriter writer = response.getWriter();
16         writer.write(name);
17         writer.flush();
18         writer.close();
19     }
20 
21 
22     /**
23      * 第二种情况:方法返回值类型为 String 类型,并且不存在返回内容对应的视图
24      *
25      * @param name
26      * @return
27      */
28     @RequestMapping("/greet2")
29     public String greet2(String name) {
30         System.out.println("name = " + name);
31         return name;
32     }
33 
34 }

对于以上两种情况,你所有的HandlerInterceptor都至少会执行两遍

原因

springmvc拦截器为什么会执行多次?

简单来讲就是controller中的void方法会导致springmvc使用你的请求url作为视图名称,然后它在渲染视图之前会检查你的视图名称,发现这视图会导致循环请求,就抛出一个ServletException,tomcat截取到这个异常后就转发到/error页面,就在这个转发的过程中导致了springmvc重新开始DispatcherServlet的整个流程,所以拦截器自然就执行了多次。

怎么解决?

拦截器拦截了静态资源请求

只需要放行这样的请求即可

请求的路径不存在或其他原因导致跳到/error页面

拦截器拦截/error

 1 @Configuration
 2 public class MVCConfig extends WebMvcConfigurationSupport {
 3      //自定义的拦截器
 4     @Bean
 5     public SecurityInterceptor getSecurityInterceptor() {
 6         return new SecurityInterceptor();
 7     }
 8   
 9     @Override
10     public void addInterceptors(InterceptorRegistry registry) {
11         //添加拦截器
12         InterceptorRegistration registration = registry.addInterceptor(getSecurityInterceptor());
13         //排除的路径
14         registration.excludePathPatterns("/login");
15         registration.excludePathPatterns("/logout");
16         //将这个controller放行
17         registration.excludePathPatterns("/error");
18         //拦截全部
19         registration.addPathPatterns("/**");
20     }
21 }

在controller(非restController)下:方法返回值类型为String类型或者Void类型(使用请求url作为视图名称),并且不存在对应的视图

重写RequestMappingHandlerAdapterViewNameMethodReturnValueHandler这两个类,并将其注入容器中

 1 public class CustomizedHandlerAdapter extends RequestMappingHandlerAdapter {
 2 
 3     @Override
 4     public void afterPropertiesSet() {
 5         super.afterPropertiesSet();
 6         setReturnValueHandlers(getReturnValueHandlers().stream().filter(
 7                 h -> h.getClass() != ViewNameMethodReturnValueHandler.class
 8         ).collect(Collectors.toList()));
 9     }
10 }
11 
12 public class HandlerVoidMethod extends ViewNameMethodReturnValueHandler {
13 
14     @Override
15     public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
16         /*
17          * 这里只处理了void返回值的方法,对于String返回值的方法则没有处理,原因是系统中可能还会用到springmvc的视图功能(例如jsp)
18          * 如果说是前后分离的项目,springmvc层只提供纯接口的话,那么可以将下面代码全部删除,
19          * 只写上一行  mavContainer.setRequestHandled(true);  即可
20          */
21         if (void.class == returnType.getParameterType()) {
22             mavContainer.setRequestHandled(true);//这行代码是重点,它的作用是告诉其他组件本次请求已经被程序内部处理完毕,可以直接放行了
23         } else {
24             super.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
25         }
26     }
27 }
28 
29 @Configuration
30 public class WebConfig {
31 
32     @Bean
33     public HandlerVoidMethod handlerVoidMethod() {
34         return new HandlerVoidMethod();
35     }
36 
37     @Bean
38     public CustomizedHandlerAdapter handlerAdapter(HandlerVoidMethod handlerVoidMethod) {
39         CustomizedHandlerAdapter chl = new CustomizedHandlerAdapter();
40         chl.setCustomReturnValueHandlers(Arrays.asList(handlerVoidMethod));
41         return chl;
42     }
43 }

通过以上代码,则能解决mvc拦截器执行多次的问题。

为什么?原理分析

HandlerInterceptor相关知识

对于拦截器HandlerInterceptor的机制需要有个大致的了解,这里简单讲下springmvc中的拦截器是怎么执行的,我们都知道springmvc中处理请求都是从DispatcherServletdoDispatch方法开始的,

 1 // DispatcherServlet类 doDispatch方法
 2 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
 3     ......省略部分前面的代码,直接从 1035行这里开始
 4 
 5     // HandlerExecutionChain mappedHandler = getHandler(processedRequest); 在1016行有设置mappedHandler的值,HandlerExecutionChain是一个拦截器调用链,它是链式执行的,这里是链式执行所有的拦截器里面的 preHandle 方法
 6     if (!mappedHandler.applyPreHandle(processedRequest, response)) {
 7         return;
 8     }
 9 
10     // 真正代用我们自己写的业务代码的入口
11     mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
12 
13     if (asyncManager.isConcurrentHandlingStarted()) {
14         return;
15     }
16 
17     applyDefaultViewName(processedRequest, mv);
18     // 这里是链式执行所有的拦截器里面的 postHandle 方法
19     mappedHandler.applyPostHandle(processedRequest, response, mv);
20 
21     ......省略后面的代码
22 }

多个拦截器它的执行顺序是和栈的入栈出栈顺序有点类似,我们把preHandle方法比作入栈,postHandle方法比作出栈,所以就是preHandle先执行的postHandle反而后执行。

例如我们有三个拦截器,分别为 A,B,C。它们的执行顺序如下图:

拦截器执行的相关源码:

 1     boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
 2         HandlerInterceptor[] interceptors = getInterceptors();//获取所有的拦截器
 3         if (!ObjectUtils.isEmpty(interceptors)) {
 4             for (int i = 0; i < interceptors.length; i++) {// 这里是从第一个开始
 5                 HandlerInterceptor interceptor = interceptors[i];
 6                 if (!interceptor.preHandle(request, response, this.handler)) {
 7                     triggerAfterCompletion(request, response, null);
 8                     return false;
 9                 }
10                 this.interceptorIndex = i;
11             }
12         }
13         return true;
14     }
15 
16     void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
17         HandlerInterceptor[] interceptors = getInterceptors();//获取所有的拦截器
18         if (!ObjectUtils.isEmpty(interceptors)) {
19             for (int i = interceptors.length - 1; i >= 0; i--) {// 这里是从最后一个开始
20                 HandlerInterceptor interceptor = interceptors[i];
21                 interceptor.postHandle(request, response, this.handler, mv);
22             }
23         }
24     }

上面简要的讲了下拦截器从哪里开始以及它的执行顺序相关的东西,经过了拦截器的前置拦截之后,springmvc通过反射执行了我们的具体业务方法,那在执行具体的业务方法时有两个很重要的问题,一是如何处理我们业务方法的参数(千奇百怪的);二是如何处理我们业务方法的返回值(也是多种多样的)。在springmvc中通过HandlerMethodArgumentResolver来处理方法参数,通过HandlerMethodReturnValueHandler来处理方法返回值。在本文中,方法的入参与本文所讨论的问题关系不大,因此这里就不展开叙述HandlerMethodArgumentResolver相关的东西了。重点说下与问题相关的HandlerMethodReturnValueHandler类。

HandlerMethodReturnValueHandler

对于controller方法的返回值的处理,springmvc框架中内置了20多种默认的返回值处理器,在RequestMappingHandlerAdapter#getDefaultReturnValueHandlers方法中可以看到它设置的一些默认的HandlerMethodReturnValueHandler

 1     private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
 2         List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
 3 
 4         // Single-purpose return value types
 5         // 返回值类型是ModelAndView或其子类
 6         handlers.add(new ModelAndViewMethodReturnValueHandler());
 7         // 返回值类型是Model或其子类
 8         handlers.add(new ModelMethodProcessor());
 9         // 返回值类型是View或其子类
10         handlers.add(new ViewMethodReturnValueHandler());
11 
12         // ResponseBody注解
13         handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
14         handlers.add(new StreamingResponseBodyReturnValueHandler());
15 
16         // 用来处理返回值类型是HttpEntity的方法
17         handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
18         handlers.add(new HttpHeadersReturnValueHandler());
19         handlers.add(new CallableMethodReturnValueHandler());
20         handlers.add(new DeferredResultMethodReturnValueHandler());
21         handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
22 
23         // Annotation-based return value types
24         // 返回值有@ModelAttribute注解
25         handlers.add(new ModelAttributeMethodProcessor(false));
26         handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
27 
28         // Multi-purpose return value types
29         // 返回值是void或String, 将返回的字符串作为view视图的名字
30         handlers.add(new ViewNameMethodReturnValueHandler());
31         // 返回值类型是Map
32         handlers.add(new MapMethodProcessor());
33 
34         // Custom return value types,自定义返回值处理
35         if (getCustomReturnValueHandlers() != null) {
36             handlers.addAll(getCustomReturnValueHandlers());
37         }
38 
39         // Catch-all
40         if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
41             handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
42         }
43         else {
44             handlers.add(new ModelAttributeMethodProcessor(true));
45         }
46         return handlers;
47     }

在本文的问题(方法返回值类型为String类型或者Void类型)中,controller的返回值为void和String两种都有,刚好对应ViewNameMethodReturnValueHandler这个处理器

 1 public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
 2 
 3     @Override
 4     public boolean supportsReturnType(MethodParameter returnType) {
 5         Class<?> paramType = returnType.getParameterType();
 6         // 返回值类型匹配,void和String
 7         return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
 8     }
 9 
10     @Override
11     public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
12             ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
13         // 返回值处理这里,只处理了String类型的返回值,将返回值的结果作为视图名称设置到ModelAndViewContainer对象中
14         // 对于void类型的返回值,这里并没有处理,
15         if (returnValue instanceof CharSequence) {
16             String viewName = returnValue.toString();
17             mavContainer.setViewName(viewName);
18             if (isRedirectViewName(viewName)) {
19                 mavContainer.setRedirectModelScenario(true);
20             }
21         }
22         else if (returnValue != null) {
23             // should not happen
24             throw new UnsupportedOperationException("Unexpected return type: " +
25                     returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
26         }
27     }
28 }

这里有个小细节,handleReturnValue方法中returnValue参数前面是加了一个@Nullable注解的,意味这这个参数的值可能是null,当你的controller为void方法时,returnValue就会为null,那handleReturnValue这个方法就不会对mavContainer这个对象做任何处理。

ModelAndView对象的流转过程

mavContainer这个参数是从RequestMappingHandlerAdapter中的invokeHandlerMethod方法中创建并一路传进来的,整个调用链如下图:

最终在DispatcherServlet的doDispatch方法中得到上图中最后返回的ModelAndView对象

 1 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
 2                 ......省略部分前面的代码
 3                 // Actually invoke the handler.
 4                 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
 5 
 6                 ......省略部代码
 7                     
 8                 // 直接看这里,设置默认视图
 9                 applyDefaultViewName(processedRequest, mv);
10     
11                 ......省略部分前面的代码
12 }
13 
14 private void applyDefaultViewName(HttpServletRequest request, @Nullable ModelAndView mv) throws Exception {
15     // 这里判断是否设置了视图,通过前面的分析,我们可以知道,由于我们的controller是void类型的,所以是没有设置视图的
16     if (mv != null && !mv.hasView()) {
17         // 获取默认视图名称
18         String defaultViewName = getDefaultViewName(request);
19         if (defaultViewName != null) {
20             // 设置默认视图名称
21             mv.setViewName(defaultViewName);
22         }
23     }
24 }

获取默认视图名称的方法:getDefaultViewName,由于这个方法里面调用栈比较深,这里直接给出它调用的最里面的那个方法:

org.springframework.web.util.UrlPathHelper#getPathWithinApplication

 1     public String getPathWithinApplication(HttpServletRequest request) {
 2         String contextPath = getContextPath(request);
 3         String requestUri = getRequestUri(request);
 4         String path = getRemainingPath(requestUri, contextPath, true);
 5         if (path != null) {
 6             // Normal case: URI contains context path.
 7             return (StringUtils.hasText(path) ? path : "/");
 8         }
 9         else {
10             return requestUri;
11         }
12     }
最后这个方法其实就是获取了controller的requestMapping,然后返回出去。再结合前面的getDefaultViewName方法可知,这个默认视图名称就是requestMapping的值。在渲染视图之前springmvc还做了个判断,就是看你的视图名称是不是本次请的uri中的一部分或者和uri一样,如果是的话,就会抛出一个异常,说你是一个循环视图路径

tomcat对错误页面的处理

在tomcat的StandardHostValve类中,它获取到了上面springmvc抛出的ServletException异常,它写了个很明了的注释,寻找一个应用级别的错误页面,如果存在的话则渲染它(就是重定向到错误页面)

通过debug直到执行到下面的代码,通过下图中第二行后面的debug信息也可以看到错误页面的路径为/error

通过RequestDispatcher这个类名能猜到应该是请求分发器,看看它是如何创建RequestDispatcher对象的
 1     public RequestDispatcher getRequestDispatcher(final String path) {
 2         ......省略部代码
 3       
 4         try {     
 5             ......省略部代码
 6 
 7             // Construct a RequestDispatcher to process this request
 8             // 创建一个新的请求调度器来处理该请求
 9             return new ApplicationDispatcher(wrapper, uri, wrapperPath, pathInfo,
10                     queryString, mapping, null);
11         } finally {          
12             mappingData.recycle();
13         }
14     }

这里可以很明显的看到开始了请求转发,这对于springmvc来讲就已经开始了一个新的请求,它会再次进入到DispatcherServletdoDispatch方法中,整个springmvc的流程会再重新走一遍,所以拦截器自然也会再执行一次,只不过这次在拦截器中看到的url已经变成/error了,而不是之前的requestMapping里面的值。

参考:

https://www.jianshu.com/p/fcbaedb3ec50

https://blog.csdn.net/weixin_44762610/article/details/107414652

https://blog.csdn.net/weixin_43948758/article/details/122718935

https://www.jb51.net/article/223052.htm