资讯详情

资讯详情

SpringMVC拦截器一文讲透:原理、执行顺序与实战坑位

你的项目里的所有Controller开头是不是都在重复做同一件小事判断用户有没有登录、有没有权限、把请求参数打个日志、算一下这个接口到底耗时多少毫秒。如果答案是肯定的说明你的代码已经在你耳边喊——“给我上一个SpringMVC拦截器”。SpringMVC框架学到这个阶段其实是一个分水岭前半段你在学怎么把接口“写出来”后半段你在学怎么把接口“管起来”。这篇“SpringMVC的学习二”会完整拆解SpringMVC拦截器的原理、配置、执行顺序、实战案例和坑位读完可以直接拿到项目里去用。拦截器这个东西不接触它之前你觉得它挺神秘搞懂了以后你会发现它就是SpringMVC给开发者留的一个“钩子”让你能在请求到达Controller前后插一段逻辑。它也不是什么高深莫测的黑魔法它依赖的就是一套清晰到近乎透明的调用链。下面我从一次请求的完整生命周期讲起。1. 从“每个接口都写一遍判断”说起拦截器到底解决什么问题1.1 没有拦截器时你的代码长什么样很多习惯了边写业务边“打补丁”的团队登录校验的代码大概率是这样的GetMapping(/user/info) public Result userInfo(HttpSession session) { Object user session.getAttribute(loginUser); if (user null) { return Result.error(401, 未登录); } // 真正的业务逻辑 return Result.success(...); } GetMapping(/order/list) public Result orderList(HttpSession session) { Object user session.getAttribute(loginUser); if (user null) { return Result.error(401, 未登录); } // 真正的业务逻辑 return Result.success(...); }当你有十个接口时这还勉强能忍。当你有五十个接口时这段if (user null)就会像牛皮癣一样贴在每个方法里。更头疼的是如果校验规则变了比如不仅要判断登录还要判断用户状态是否被冻结、访问IP是否在白名单里你得把所有Controller翻一遍。这种代码的内在问题不叫“复用性差”而叫“关注点混杂”。登录校验、日志记录、权限判断、性能统计这类逻辑和“查用户信息”“查订单列表”这样的核心业务逻辑没有任何关系却硬生生挤在同一个方法里。而软件开发领域对付这种问题早就有了一套成熟的思路就是“拦截器模式/过滤器模式”。1.2 拦截器背后的设计思想横切关注点你可以把每个请求想象成一趟从浏览器出发的火车目的地是Controller里的某个方法。如果一路上每过一个站都要人工检查车票乘务员就会累死。最合理的做法是在中间设几个检查点所有火车进站时自动检查符合条件就放行不符合就当场拦下。这里的“自动检查点”在SpringMVC里就是HandlerInterceptor。它能把“登录检查”“日志记录”“耗时统计”这些和业务无关的公共逻辑抽取出来一次性注册到某些路径下让框架自动帮你调用。这个思想和后端领域常说的AOP面向切面编程是一脉相承的区别在于拦截器是SpringMVC框架基于Servlet规范提供的一种更具体的实现使用门槛更低几乎不需要动态代理的知识。拦截器的应用场景主要集中在下面这几类登录状态校验、会话有效性检查接口权限控制、角色判断访问日志记录、请求参数审计接口耗时统计、性能监控统一设置响应头、处理跨域黑名单/白名单过滤、限流控制换句话说拦截器是你在学习SpringMVC框架时最先接触也最容易直接落地的一个“治理接口”的手段。它不像AOP那样需要理解切点表达式和通知类型你只需要实现三个方法就能控制请求的进和出。2. 一次请求进入SpringMVC后是谁调用了你的拦截器这一节必须把DispatcherServlet和HandlerExecutionChain讲透否则你会一直有一种“拦截器好像被神秘力量触发”的错觉。事实上触发逻辑非常朴素。2.1 DispatcherServlet是唯一的总调度入口SpringMVC框架延续了前端控制器模式所有请求先到DispatcherServlet再由它分发到具体的Handler也就是Controller方法。DispatcherServlet内部会做下面这些事接收Request对象并做multipart封装处理。调用HandlerMapping根据URL找到能处理这个请求的Handler。HandlerMapping不光返回Handler还会返回一组“配套的拦截器”。把Handler和拦截器一起组装成一个HandlerExecutionChain对象。顺着这个执行链依次触发拦截器和Controller。这里第3步和第4步是理解拦截器的关键。Handler就好比你在演唱会门口拿到的门票拦截器则是一路上的安检口。HandlerMapping在发门票的时候就已经顺手在门票上盖了“前方需要过几道安检”的章。所以拦截器并不是在请求到达后临时去数据库里查出来的而是处理器的映射阶段就已经静态匹配好的。2.2 HandlerExecutionChainHandler 拦截器序列HandlerExecutionChain从名字也能看出来它把“执行处理器”和“一系列拦截器”包装成了链条结构。DispatcherServlet拿到这个链条后执行顺序非常固定按顺序执行所有拦截器的preHandle方法如果某个返回false请求在这里立即终止。调用HandlerController方法本身。如果Controller正常执行完倒序执行所有拦截器的postHandle方法。渲染视图如果存在ModelAndView。不管成功还是抛异常都会倒序执行所有拦截器的afterCompletion方法。我把这个过程画成文字时序你感受一下客户端请求 - DispatcherServlet - HandlerMapping 找到 Handler Interceptors - Interceptor1.preHandle - Interceptor2.preHandle - Controller 方法执行 - Interceptor2.postHandle - Interceptor1.postHandle - 视图渲染或者响应JSON - Interceptor2.afterCompletion - Interceptor1.afterCompletion - 响应回到客户端有没有注意到postHandle和afterCompletion的顺序是倒序的这其实就是栈式调用先进后出。第一个进入的拦截器最后才退出。这样可以保证资源的释放顺序和资源的申请顺序对称比如第一个拦截器打开了数据库连接最后一个关闭它整个链路才安全。2.3 拦截器的三个方法凭什么让你控制整个链路HandlerInterceptor接口定义了三个方法我建议你不要只把它们当成“三个回调”而是理解成三个时机preHandle在Controller执行之前调用。返回值决定请求是否继续。postHandle在Controller执行完成后、视图渲染之前调用。此时你可以拿到ModelAndView修改返回给前端的数据。afterCompletion在整个请求处理完成之后调用哪怕是Controller抛了异常这个方法也会执行非常适合做资源清理和日志收尾。很多初学者会把注意力全放在preHandle上这是不够的。真正让拦截器强大的是它同时覆盖了业务执行前后两个阶段并且异常发生时也能兜底。比如你想统计接口耗时就必须在preHandle里记录开始时间在afterCompletion里计算差值。前者拿不到耗时结果后者拿不到开始时间两者配合才是完整的性能监控。为了让你对这三个时机有更强的体感我用一个自定义的日志拦截器来演示public class AccessLogInterceptor implements HandlerInterceptor { private static final Logger log LoggerFactory.getLogger(AccessLogInterceptor.class); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { request.setAttribute(startTime, System.currentTimeMillis()); log.info(请求开始: {}, 来自IP: {}, request.getRequestURI(), request.getRemoteAddr()); return true; } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { log.info(请求处理完成准备渲染视图或返回响应); } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { long startTime (long) request.getAttribute(startTime); long cost System.currentTimeMillis() - startTime; log.info(请求结束: {}耗时: {}ms异常: {}, request.getRequestURI(), cost, ex null ? 无 : ex.getMessage()); } }这里用request.setAttribute(startTime, ...)把开始时间放在request域里因为preHandle和afterCompletion虽然方法不同但拿到的是同一个HttpServletRequest对象数据能天然传递。这个小trick在拦截器内部传递上下文时非常实用。3. preHandle返回false之后的那些微妙行为我刚学拦截器时最大的误区就是以为preHandle返回false之后整个请求就“什么都没有了”。实际上不是这样返回false只是把Controller短路掉但你仍然可以对Response对象做写入操作。DispatcherServlet看到preHandle返回false后会直接跳过Controller、跳过所有postHandle然后只对“已经执行过preHandle”的拦截器执行afterCompletion。这段话有点绕我用一个简单模型解释想象电梯上行一层层停靠每层都有人按下按钮。如果电梯在5楼发现异常停住了那么不会继续上行到6楼但在5楼已经亮过灯的楼层系统不会再让它们亮一次。放到拦截器里就是第2个拦截器preHandle返回false则第3个及之后的拦截器完全不会执行第2个拦截器的afterCompletion不会执行因为它的preHandle没有成功结束第1个拦截器的afterCompletion会执行因为它的preHandle已经正常返回true。也就是说afterCompletion只保证对“已经成功通过了preHandle”的拦截器触发。这个规则的合理之处在于你不能要求一个“没来得及进入”的拦截器做清理工作否则它清理的可能是别人还没有创建的资源反而出问题。3.1 一个多拦截器顺序对比表为了让你对执行顺序一目了然我用一张表展示“两个拦截器A和B”在不同情况下的调用结果场景执行顺序A和B都放行A.pre - B.pre - Controller - B.post - A.post - B.after - A.afterA放行B的pre返回falseA.pre - B.pre - A.afterA的pre返回falseA.pre - A.afterController异常A.pre - B.pre - Controller异常 - B.after - A.afterpostHandle不执行视图渲染异常A.pre - B.pre - Controller - B.post - A.post - 渲染异常 - B.after - A.after注意我这里的“A.after在前还是B.after在前”很容易记错你只要记住afterCompletion是倒序执行即可。上面表格里我默认A先注册A是链路上的第一个拦截器。3.2 返回false后不要忘记自己输出响应因为preHandle返回false后整个调用链就断了DispatcherServlet不会帮你做任何页面跳转也不会渲染默认视图。此时你能不能给用户一个友好提示完全取决于拦截器自己有没有往HttpServletResponse里写东西。最常见的登录拦截器写法是这样的Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { return true; } // 判断是不是ajax请求 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或会话已过期\}); } else { response.sendRedirect(/login); } return false; }这种写法非常经典页面访问直接重定向到登录页接口访问返回JSON。它说明拦截器不只是“放行/不放行”它和Controller一样掌握着完整的请求和响应对象可以自主决定“被挡下来之后怎么反馈给客户端”。很多刚写权限拦截器的人容易犯的错就是只返回false结果前端收到一片空白、一个302都没有。问题出在缺少对这个机制的完整理解。4. 拦截器怎么注册WebMvcConfigurer与路径表达式里的学问光有拦截器类还不够你还需要通过配置把它“挂到”DispatcherServlet的处理链上。Spring Boot集成SpringMVC后最标准的做法是实现WebMvcConfigurer接口。4.1 一段完整的注册代码Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AccessLogInterceptor()) .addPathPatterns(/**) .excludePathPatterns( /login, /register, /css/**, /js/**, /images/**, /favicon.ico, /error ); } }这里有好几个细节需要展开说第一/**和/*的区别。/*只匹配当前路径一层的URL比如/user能匹配/user/info就匹配不到。/**是匹配所有层级路径。如果你想把所有业务接口都纳入拦截必须用/**。很多初学者在这里写了个/*结果/user/info没被拦截排查半天找不到原因其实是路径匹配的问题。第二excludePathPatterns的作用是“排除”。拦截器里的排除逻辑非常重要因为登录接口本身就不需要“已登录”的校验静态资源也不需要。最容易被遗漏的排除项是/error如果你不排除Spring Boot的错误跳转路径一旦业务接口出了异常请求被转发到/error时再次经过拦截器可能导致你看到大量“接口异常”日志甚至干扰对真实异常的排查。4.2 拦截器不生效的两大罪魁祸首我帮人排查“为什么拦截器没生效”这种情况特别多绝大多数是下面两个原因第一个原因WebConfig类上的Configuration注解丢了或者这个配置类所在包不在Spring Boot的扫描路径下。Spring Boot默认只扫描启动类所在包及其子包。如果你的配置类放在了别的包恭喜你它根本没被加载拦截器自然不会生效。第二个原因有人在拦截器里注入了Service或者Mapper但创建拦截器对象时用了new而不是从Spring容器里获取Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; // 正确做法 Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) // 用容器里的bean .addPathPatterns(/**); // 错误做法registry.addInterceptor(new LoginInterceptor()) // 这样new出来的对象不在Spring容器里任何依赖注入都拿不到 } }如果拦截器里不需要任何Spring组件new没问题。但它一旦需要查询数据库或调用Redis你就要让它成为Spring bean并通过Autowired注入到配置类中。这个坑的实际表现是拦截器确实执行了但一执行就报NullPointerException因为loginService是空的。4.3 一个完整的基于注解的权限拦截器设计路径表达式只能做“粗粒度”的拦截但真实业务里经常需要“这个接口登录了就能访问那个接口必须管理员权限”。一个常见的设计思路是自定义一个RequirePermission注解配合拦截器判断每个Handler上有没有这个注解有就做权限校验没有就放行。Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); }注册时仍然拦截所有接口但在preHandle里通过方法检查真正的权限逻辑Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; // 非Controller方法比如静态资源处理器放行 } HandlerMethod handlerMethod (HandlerMethod) handler; RequirePermission permission handlerMethod.getMethodAnnotation(RequirePermission.class); if (permission null) { return true; // 没有权限注解跳过校验 } // 从request里解析当前用户校验是否拥有 permission.value() 这个权限 // 没有则写403响应返回false }这里有个隐藏知识点handler参数到底是个什么对象。当请求映射到Controller方法时handler是HandlerMethod类型你可以通过它拿到方法上的注解、类上的注解、方法参数等丰富信息。但当请求是访问静态资源时handler是ResourceHttpRequestHandler不是HandlerMethod。所以判断类型之后再强转是为了避免类型转换异常。4.4 多个拦截器的顺序怎么控制项目里很少只用一个拦截器比如“日志拦截器”和“权限拦截器”往往同时存在。多个拦截器的执行顺序默认取决于addInterceptor的调用顺序。如果你需要精确控制可以实现Ordered接口或者注册时用带有order的重载方法。registry.addInterceptor(authInterceptor).order(1); registry.addInterceptor(logInterceptor).order(2);order数值越小优先级越高越先执行preHandle也就越靠在外层。一般建议把日志、耗时统计这类“什么请求都要记录”的拦截器放在最外层把权限校验放在内层。这样即使权限校验失败日志也能先记录下来。5. 过滤器Filter与拦截器的边界看到这个问题就该知道两种技术选型几乎每个面试官在问SpringMVC拦截器时都会追加一问拦截器和过滤器有什么区别这不只是面试题在实际架构设计里选错工具会很别扭。5.1 四张图级别的区别对比我把核心差异整理成一个表格对比维度Filter过滤器HandlerInterceptor拦截器所属规范Servlet规范SpringMVC框架生效范围所有经过Servlet容器的请求只对DispatcherServlet映射的请求生效是否依赖Spring不依赖原生Servlet接口依赖Spring容器可以注入任意Bean能否拿到Controller方法信息不能只能拿到URL、Request等能handler是HandlerMethod可读方法注解执行时机在DispatcherServlet之前在HandlerMapping定位之后是否可以操作ModelAndView不能可以在postHandle里是否有业务回退钩子类似chain.doFilter后的代码有afterCompletion异常时也触发Filter的执行时机比拦截器更靠前请求在Servlet容器里会先穿过过滤器链才到达DispatcherServlet。DispatcherServlet再通过HandlerMapping匹配Controller和拦截器。用一句话概括Filter管的是“请求进不进SpringMVC”拦截器管的是“请求怎么进入Controller、出来之后怎么处理结果”。5.2 实际项目中怎么分配各自的任务基于这两种组件的特性我的习惯是Filter负责和“传输层”强相关的事情请求编码、响应压缩、CORS跨域、XSS过滤、读取并缓存请求体。Interceptor负责和“业务上下文”强相关的事情登录校验、权限判断、功能开关、审计日志、接口耗时统计。举个具体例子如果我要统计一次请求的入站开始时间放在Filter里其实更准确因为Filter是第一个拿到请求的组件但如果我要统计的是Controller方法的执行时长那就必须放在拦截器里因为Filter覆盖的范围包括静态资源和其他Servlet统计出来的就不纯了。5.3 请求体被拦截器读空的问题必须用Filter解决有一个非常经典的问题POST请求的Body是流式的只能读一次。如果你在拦截器里执行了request.getInputStream()或request.getReader()Controller里的RequestBody就会拿到空值。这是SpringMVC初学者最容易踩到的隐形大坑。要想既读请求体做日志又让Controller正常拿到参数正确的思路是使用Servlet规范里的ContentCachingRequestWrapper。这个包装类会把读过的内容缓存下来后续再次读取时从缓存返回。但因为这个包装需要在请求进入DispatcherServlet之前就生效所以它应该发生在Filter层public class RequestCachingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest) { HttpServletRequest httpReq (HttpServletRequest) request; ContentCachingRequestWrapper wrappedRequest new ContentCachingRequestWrapper(httpReq); chain.doFilter(wrappedRequest, response); } else { chain.doFilter(request, response); } } }这样Filter把包装好的request传下去后面的拦截器和Controller每次读取的都是缓存版本互不干扰。如果你需要在拦截器里打印请求体可以调用wrappedRequest.getContentAsByteArray()但注意要在afterCompletion里读因为preHandle阶段body可能还没被Controller消费完缓存是空的。这种细节只有真正踩过坑才会注意。6. 实战拆解一个完整的登录鉴权拦截器是这样迭代出来的光讲概念和API还不够我带你从头到尾把登录鉴权拦截器的代码演进过程走一遍你就能明白这玩意在实际项目中怎么落地。6.1 第一版没有任何拦截器Controller里全是重复校验这是最原始的状态前面的例子已经展示过了每个接口开头都要从session里拿用户判断是否为空。这个版本的缺点是改规则困难代码冗余严重而且容易漏写——只要有一个接口忘了加判断就出现安全漏洞。真实的线上事故往往就是这么来的新同学写了一个新接口抄了代码模板但忘了复制那段login判断。6.2 第二版正规的HandlerInterceptor 排除路径我定义一个LoginInterceptor所有需要用户登录的接口都走它而登录、注册、验证码这些接口通过排除路径放行。上面已经有代码了这里不重复。这个版本的优点是校验逻辑集中统一缺点是想对某个接口做更细粒度控制时只能靠路径区分比如“管理员接口”和“普通用户接口”必须放在不同的路径前缀下才能分别控制。6.3 第三版注解驱动想校验哪个方法就标注哪个方法我在实际项目里最喜欢的就是注解驱动。还是用前面那个RequirePermission思路登录后还必须校验角色权限。在这版设计里拦截器不再直接判死“所有接口都要登录”而是先看目标方法有没有权限注解方法上有RequirePermission(admin)必须要求当前用户是管理员。方法上有RequirePermission(user:query)要求当前用户拥有对应权限点。方法上没有任何权限注解放行。这样权限配置就完全跟着方法走迁移性非常强新接口要不要权限控制只需要看方法上面有没有注解一眼就能扫出来。而且权限校验的逻辑也沉淀在了一个拦截器里后续拓展特别方便。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod (HandlerMethod) handler; RequiresLogin login handlerMethod.getMethodAnnotation(RequiresLogin.class); if (login null) { login handlerMethod.getBeanType().getAnnotation(RequiresLogin.class); } if (login null || !login.required()) { return true; } // 校验session/login token LoginUser user (LoginUser) request.getAttribute(loginUser); if (user null) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\请先登录\}); return false; } return true; }这里多了一个小技巧先看方法注解再看类注解。如果控制器类级别标注了RequiresLogin就意味着类下所有接口默认都要登录个别方法如果不需要可以在方法上强制关闭。这就是“类默认开启 方法白名单”的组合设计比纯路径配置灵活得多。6.4 关于token登录态的一点提示现在的项目大多是前后端分离架构不再依赖session而是用JWT或自定义token。这种情况下拦截器的写法也相应变化从Header里取token解析出用户信息放入request的Attribute里方便后续业务代码直接调用。String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { response.setStatus(401); return false; } LoginUser user userService.parseToken(token); if (user null) { response.setStatus(401); return false; } request.setAttribute(loginUser, user); return true;有一个细节值得说拦截器解析完用户后不要用ThreadLocal直接存用户对象强烈建议通过request.setAttribute往下传。原因很简单SpringMVC在请求结束后并没有保证ThreadLocal被清理如果线程被复用上一个请求的用户数据可能泄露给下一个请求这是安全级别很高的问题。而且request.setAttribute是Servlet规范自带的上下文传递机制天然跟随请求生命周期不需要手动清理更稳妥。7. 我在实际项目里被拦截器绊过的5个跟头最后一个环节我想把这些年在线下项目和帮朋友排查问题中积累的“拦截器事故”分享出来。这些坑不是从文档里看来的全是真实环境里用生产事故换来的经验。7.1 坑一OPTIONS请求被权限拦截器挡住前端跨域直接失败前后端分离项目里如果前端和后端域名不同浏览器在发送真正的POST请求前会先发一个OPTIONS预检请求。如果你在拦截器里对所有/**都做登录验证这个OPTIONS请求会因为没有携带token而被401拦截。前端看到的结果是“跨域失败”但真正的问题在后端拦截器。解决办法有两个要么在排除路径里加上OPTIONS方法对应的判断要么干脆在拦截器里针对OPTIONS直接放行if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; }这样预检请求不会触发真实的业务处理放行是安全的。7.2 坑二静态资源也被拦截页面样式全部丢失Spring Boot默认把/static、/public、/resources下的文件映射到静态资源处理器。如果你在路径配置里只写了addPathPatterns(/**)没有排除静态目录拦截器也会作用到CSS、JS、图片上。这些资源根本不需要登录一旦被拦截器挡下来你的页面就会变成一个“没有灵魂的HTML骨架”。每次写拦截器配置前我都会把所有静态资源路径和健康检查路径全部列成清单放进excludePathPatterns。不要嫌麻烦这个清单能省掉大量“样式怎么丢了”的无谓排查。7.3 坑三拦截器里拿到了用户Controller里却拿不到这个问题常见于新人对model和request的作用域理解不清。你如果在拦截器里往request里set了用户对象想通过SessionAttribute或ModelAttribute去Controller里拿容易踩坑。安全可靠的做法是用RequestAttribute或者直接在Controller方法参数里显式声明再通过ServletRequestAttributes工具类获取。GetMapping(/user/info) public Result userInfo(RequestAttribute(loginUser) LoginUser user) { return Result.success(user.getUsername()); }RequestAttribute是专门用来取request.setAttribute设置的数据的语义清晰不会和session、model混在一起。7.4 坑四日志里出现两次相同请求以为是拦截器重复执行有一次我在拦截器里加了访问日志结果发现一个请求被打印了两遍。排查之后发现PATCH、PUT请求在这套老项目里会先被转发到一个处理hidden method的Filter这个Filter内部又做了一次forward。拦截器默认也会拦截forward请求所以同一个请求在转发前后各进了一次拦截器。解决办法是判断请求的DispatcherType或者用request.getAttribute(javax.servlet.forward.request_uri)来判断是否为转发请求。在Spring Boot里可以利用registry.addInterceptor(...).addPathPatterns(...)配合excludePathPatterns或者干脆把日志记录类的逻辑放到Filter层因为Filter天然支持DispatcherType的精确控制。7.5 坑五拦截器里使用Value读配置总是为null这个坑和前面提到的“用new创建拦截器”类似。如果Value或Autowired没有生效大概率是拦截器对象不是由Spring容器管理的。还有一个容易忽略的情况你在某个配置类里通过new创建了一个拦截器然后又把这个拦截器声明为Component导致容器里有两个不同的实例配置类接收的是new出来的那个依赖全部为空。我的建议是拦截器类的生命周期管理要么完全走Spring容器要么完全通过new手动注入所需参数不要混用。如果你用Component容器管理就在配置类里Autowired注入如果你喜欢纯Java写法就在配置类里手动传入所需依赖二选一思路越简单越不容易出错。8. 关于拦截器的几条个人经验做了多年开发我觉得SpringMVC拦截器是那种“一看就会一写就错”的组件。很多东西原理很简单但各种边界情况非常考验经验。如果要我给正在学这个框架的人提几条实在建议我会挑这几点。第一拦截器里写代码要时刻记得它是“并发”的。同一个拦截器实例会被多个线程同时调用所以千万不要在拦截器里用没有线程安全保护的成员变量存请求级数据。计时开始时间可以放进request attribute或ThreadLocal但用ThreadLocal时一定要在afterCompletion里清掉避免线程复用污染这是血泪教训。第二拦截器不是万能的应急工具。有些业务逻辑适合放在Service层的AOP切面里比如事务控制、更细粒度的权限切点有些适合放在独立的过滤器里比如请求体缓存、编码处理。拦截器最强的地方是它能结合URL和HandlerMethod做灵活的路径级、方法级控制但它终究是“HTTP请求层面”的组件不应承载过重的业务逻辑。第三写拦截器时永远要问自己三个问题处理不了的请求能不能返回一个明确的响应异常发生了afterCompletion会不会正常兜底多个拦截器并存时顺序是否和业务预期吻合如果你每次写拦截器都把这几个问题在脑子里过一遍项目里因为拦截器引发的事故至少能减少八成。这就是我在实践里对SpringMVC拦截器的全部沉淀。下一次学习SpringMVC时我建议你把过滤器、拦截器、控制器增强ControllerAdvice这三样东西放在一起对比学习你会发现它们虽然都是“在请求处理过程中插入逻辑”但各自服务的层次完全不同。理解了层次你才算真正把这个框架用到位了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →