关于SpringMVC拦截器执行两遍的原因分析以及如何解决
什么情况下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作为视图名称),并且不存在对应的视图
重写RequestMappingHandlerAdapter和ViewNameMethodReturnValueHandler这两个类,并将其注入容器中
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中处理请求都是从DispatcherServlet的doDispatch方法开始的,
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 }
tomcat对错误页面的处理
在tomcat的StandardHostValve类中,它获取到了上面springmvc抛出的ServletException异常,它写了个很明了的注释,寻找一个应用级别的错误页面,如果存在的话则渲染它(就是重定向到错误页面)
通过debug直到执行到下面的代码,通过下图中第二行后面的debug信息也可以看到错误页面的路径为/error
参考:
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
相关文章
- springmvc+mybatis+maven项目框架搭建
- 【SpringMVC笔记】第一课-框架执行过程
- [SpringMVC] - 解决Jackson中文乱码 : springmvc-servlet.xml
- Springmvc入门案例(1)
- spirng: srping mvc配置(访问路径配置)搭建SpringMVC——最小化配置
- SpringMVC整合quartz,实现定时任务
- 【SpringMVC笔记01】SpringMVC介绍及搭建开发环境
- 【SpringMVC笔记11】SpringMVC实现文件上传和下载
- 【SpringMVC笔记03】SpringMVC返回响应数据的几种方式
- 对Spring 及SpringMVC的理解
- Spring+SpringMVC+Mybatis(开发必备技能)04、mybatis自动生成mapper_dao_model(包含工具与视频讲解) 纯绿色版本、配套使用视频,100%运行成功
- springmvc项目中InitializingBean执行2次
- springmvc 之 Controller
- 学习SpringMVC——说说视图解析器
- SpringMVC Ajax返回的请求json的方式来解决在中国字符串乱码问题
- [springMVC学习]11、自定义拦截器
- SpringMVC接收哪些类型参数参数
- Springmvc接收json数据的4种方式
- SpringMVC 执行流程解析
- SpringMVC: Controller及 RestFul风格