
1. 从“请求”到“响应”为什么需要拦截器在任何一个基于SpringMVC构建的Web应用中从用户发起一个HTTP请求到控制器Controller处理完毕并返回响应这中间并非一条毫无阻碍的直线。想象一下你管理着一栋大楼你的Web应用每个房间Controller提供不同的服务。访客HTTP请求可以随意进入任何房间吗显然不行。你需要一个前台Interceptor在访客进入房间前检查他的身份登录验证、记录他的到访时间日志记录、甚至根据他的VIP等级决定是否提供快速通道权限校验。这个“前台”的角色就是SpringMVC拦截器。拦截器Interceptor是SpringMVC框架提供的一种强大机制它允许你在请求处理的生命周期中的特定点如控制器方法执行前、执行后、视图渲染后插入自定义逻辑。它与我们常听到的过滤器Filter有相似之处但工作层次和粒度不同。过滤器是Servlet规范的一部分作用于更底层能拦截一切请求包括静态资源而拦截器是SpringMVC框架的一部分它只拦截进入SpringMVC处理流程的请求即那些映射到Controller或RestController的请求。这意味着拦截器可以天然地获取Spring的上下文如Autowired注入的Bean对控制器方法、处理器适配器等有更深的理解。在实际项目中拦截器几乎是标配。我遇到过不少团队项目初期为了赶进度把登录校验、日志打印等逻辑直接写在每个Controller方法里结果就是代码重复、难以维护后期想统一加个操作日志或者接口耗时统计改动点遍布全项目苦不堪言。拦截器正是为了解决这类横切关注点Cross-Cutting Concerns而生的利器。通过它我们可以将非业务逻辑如认证、授权、日志、性能监控、通用参数处理从业务代码中剥离出来实现关注点分离让Controller只专注于处理核心业务。2. 拦截器的核心骨架HandlerInterceptor接口深度解析SpringMVC拦截器的能力源于org.springframework.web.servlet.HandlerInterceptor接口。理解这个接口的三个核心方法是灵活运用拦截器的关键。这三个方法分别在请求处理流程的不同时机被调用构成了拦截器的完整生命周期。2.1preHandle请求的“守门员”preHandle方法在处理器即我们的Controller方法执行之前被调用。这是拦截器最常用、也是最强大的方法常用于进行前置检查和预处理。public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } // ... 其他方法 }参数解析:request,response: 标准的HttpServletRequest和HttpServletResponse对象你可以从中获取请求参数、头信息也可以直接向响应中写入数据。handler: 即将被执行的处理器对象通常是HandlerMethod类型它封装了目标Controller方法的信息如Method对象、Bean实例。你可以通过它进行更精细的控制例如判断方法上是否有某个注解。返回值意义: 这是一个布尔值它决定了请求是否继续向下执行。return true: 放行请求会继续传递给下一个拦截器或最终的处理器。return false: 中断请求处理流程在此终止。后续的拦截器和处理器都不会再执行。通常你需要在此方法内自己完成响应例如写回一个JSON错误信息否则客户端会一直等待。典型应用场景:身份认证与授权: 检查Session或Token判断用户是否登录是否有权限访问当前接口。日志记录: 记录请求进入时间、请求路径、用户IP等信息用于后续的审计或调试。防重复提交: 通过校验请求中的Token防止表单重复提交。全局参数预处理: 从请求中提取一些通用参数如当前用户ID、设备信息并放入请求属性中供后续Controller使用。注意在preHandle中如果返回false务必记得通过response对象给出明确的响应否则浏览器或客户端会一直处于挂起状态体验极差。一个常见的做法是在认证失败时直接response.sendError(HttpStatus.UNAUTHORIZED.value(), “未授权”)或者输出一个标准的JSON错误体。2.2postHandle处理后的“质检员”postHandle方法在处理器执行之后、但在视图渲染之前被调用。此时处理器方法已经执行完毕模型数据ModelAndView也已经准备就绪但最终的HTML或JSON还未生成。public interface HandlerInterceptor { // ... default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable ModelAndView modelAndView) throws Exception { } // ... }参数解析: 多了一个ModelAndView参数。对于返回JSON的RestController这个参数可能为null对于返回视图的Controller你可以通过它来修改即将渲染的视图名称或向模型中添加额外的全局属性。执行时机: 注意只有当preHandle方法返回true时postHandle才会被执行。典型应用场景:统一修改模型数据: 向所有页面的模型中添加一些公共数据比如网站名称、当前年份、用户菜单等。记录处理结果: 结合preHandle中记录的请求开始时间可以在这里计算接口耗时并记录到日志中。对响应进行初步加工: 虽然不能修改已经写入HttpServletResponse输出流的内容但可以对ModelAndView进行调整。2.3afterCompletion收尾的“清洁工”afterCompletion方法在整个请求完成之后即视图渲染完毕或响应已返回给客户端之后被调用。它主要用于进行资源清理和最终的通知。public interface HandlerInterceptor { // ... default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable Exception ex) throws Exception { } }参数解析: 多了一个Exception ex参数。如果处理器执行过程中抛出了异常且没有被ControllerAdvice全局异常处理器捕获那么这个异常对象会传递到这里。如果流程正常完成则ex为null。执行条件: 该方法同样需要对应的preHandle返回true才会执行。它是请求生命周期的最后一道关卡。典型应用场景:资源清理: 关闭在preHandle中打开的数据库连接、文件流等资源虽然更推荐用try-with-resources或框架管理。性能监控收尾: 完成在preHandle中开始的性能统计将最终耗时上报到监控系统。异常日志记录: 虽然业务异常最好由全局异常处理器处理但这里可以作为最后一道防线记录那些未被捕获的、意料之外的系统异常。三个方法的执行顺序与嵌套关系 想象一个请求穿过多个拦截器链Interceptor1-Interceptor2-Controller。Interceptor1.preHandle()-Interceptor2.preHandle()-Controller方法执行Interceptor2.postHandle()-Interceptor1.postHandle()Interceptor2.afterCompletion()-Interceptor1.afterCompletion()注意postHandle和afterCompletion的执行顺序与preHandle相反类似于栈的“先进后出”原则。这对于理解多个拦截器之间的协作非常重要。3. 从零到一如何定义并注册一个拦截器理解了原理我们来动手实现一个最常用的拦截器基于JWTJSON Web Token的认证拦截器。这个例子涵盖了拦截器定义、配置以及一些实用技巧。3.1 第一步实现自定义拦截器类我们创建一个AuthInterceptor类实现HandlerInterceptor接口。这里我们假设Token放在HTTP请求头的Authorization字段中格式为Bearer token。import com.yourproject.util.JwtUtil; // 假设的JWT工具类 import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; Component // 将其声明为Spring管理的Bean方便注入其他依赖 public class AuthInterceptor implements HandlerInterceptor { // 可以注入需要的服务如JwtUtil、用户服务等 // Autowired // private UserService userService; private final JwtUtil jwtUtil; public AuthInterceptor(JwtUtil jwtUtil) { this.jwtUtil jwtUtil; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 检查是否是无需认证的路径如登录、注册、公开API String requestURI request.getRequestURI(); if (isWhiteList(requestURI)) { return true; // 白名单直接放行 } // 2. 从请求头获取Token String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { sendUnauthorizedResponse(response, 缺少或无效的Authorization头); return false; } String token authHeader.substring(7); // 去掉Bearer 前缀 // 3. 验证Token String userId; try { userId jwtUtil.validateTokenAndGetUserId(token); } catch (Exception e) { // 捕获JWT过期、签名错误等异常 sendUnauthorizedResponse(response, Token无效或已过期: e.getMessage()); return false; } // 4. Token验证通过将用户信息存入请求属性供Controller使用 request.setAttribute(CURRENT_USER_ID, userId); // 5. 可选刷新Token逻辑如采用双Token机制可在此检查并刷新 // ... return true; // 放行 } private boolean isWhiteList(String uri) { // 这里可以配置一个白名单列表通常从配置文件或数据库读取 return uri.startsWith(/api/auth/login) || uri.startsWith(/api/public/) || uri.equals(/error); // Spring Boot的默认错误路径通常也放行 } private void sendUnauthorizedResponse(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); // 构造一个标准的错误JSON响应体 String jsonResponse String.format({\code\: 401, \message\: \%s\}, message); response.getWriter().write(jsonResponse); response.getWriter().flush(); } // postHandle和afterCompletion根据需求实现本例中可能不需要 }关键点说明Component注解让Spring管理这个拦截器实例这样我们才能在配置类中Autowired它并且它内部也可以使用Autowired注入其他Bean如JwtUtil。白名单判断这是一个非常重要的实践。一定要在拦截器最开始判断当前请求路径是否在白名单内避免对登录、注册等接口进行无谓的Token验证。友好的错误响应在验证失败返回false前务必通过response对象返回一个结构清晰、HTTP状态码正确的错误信息。直接返回false而不写响应会导致客户端超时。信息传递通过request.setAttribute()将验证后的用户ID存入请求作用域后续的Controller可以通过RequestAttribute(“CURRENT_USER_ID”)注解来获取实现了拦截器与Controller之间的数据传递。3.2 第二步配置拦截器并指定拦截路径定义了拦截器之后需要告诉SpringMVC在什么时候、对哪些路径使用它。这需要通过实现WebMvcConfigurer接口的配置类来完成。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) // 拦截所有以/api/开头的请求 .excludePathPatterns( // 排除不需要拦截的路径 /api/auth/login, /api/auth/register, /api/public/**, /swagger-resources/**, // 通常需要排除Swagger/knife4j的文档资源 /v2/api-docs, /swagger-ui.html, /webjars/**, /doc.html // knife4j的文档页面 ); // 可以继续添加更多拦截器 // registry.addInterceptor(new LogInterceptor()).addPathPatterns(/**); } }配置详解addInterceptor: 注册我们定义的拦截器Bean。addPathPatterns: 指定要拦截的URL模式。支持Ant风格路径模式如/api/**,/user/*。excludePathPatterns: 指定要排除的URL模式。这里要特别注意除了业务白名单像Swagger、Knife4j的API文档页面和静态资源路径也通常需要排除否则会导致文档页面无法访问。踩坑实录曾经有同事配置了全局拦截/**的拦截器但忘了排除静态资源路径如/static/**,/css/**,/js/**和健康检查端点如/actuator/health导致前端页面样式全无监控系统告警不断。所以配置拦截路径时一定要“瞻前顾后”。3.3 第三步在Controller中使用拦截器传递的数据在AuthInterceptor的preHandle方法中我们将用户ID设置到了请求属性中。在Controller中我们可以轻松地获取它。import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/api/user) public class UserController { GetMapping(/profile) public ApiResponse getUserProfile(RequestAttribute(CURRENT_USER_ID) String userId) { // 直接使用拦截器注入的userId来查询用户信息 UserProfile profile userService.getProfileById(userId); return ApiResponse.success(profile); } }使用RequestAttribute注解可以优雅地获取请求作用域中的属性避免了在Controller中再次解析Token的重复代码。4. 进阶实战多拦截器编排与常见问题排查单个拦截器往往不能满足复杂的需求。实际项目中我们可能需要多个拦截器各司其职例如日志拦截器 - 认证拦截器 - 权限拦截器。这就涉及到拦截器的执行顺序问题。4.1 多拦截器的执行顺序控制在WebMvcConfigurer.addInterceptors方法中注册拦截器的顺序决定了它们preHandle方法的执行顺序。Override public void addInterceptors(InterceptorRegistry registry) { // 第一个注册日志拦截器最先执行preHandle registry.addInterceptor(logInterceptor).addPathPatterns(/**); // 第二个注册认证拦截器 registry.addInterceptor(authInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/**); // 第三个注册权限拦截器最后执行preHandle registry.addInterceptor(permissionInterceptor).addPathPatterns(/api/admin/**); }执行流程将是LogInterceptor.preHandle()-AuthInterceptor.preHandle()-PermissionInterceptor.preHandle()控制器方法执行PermissionInterceptor.postHandle()-AuthInterceptor.postHandle()-LogInterceptor.postHandle()PermissionInterceptor.afterCompletion()-AuthInterceptor.afterCompletion()-LogInterceptor.afterCompletion()顺序设计原则日志/监控类拦截器应放在最前面以便记录最原始的请求信息和最完整的耗时。认证类拦截器应在日志之后在权限校验之前。因为必须先知道“你是谁”才能判断“你能做什么”。细粒度权限校验拦截器可以放在最后只针对特定路径如/api/admin/**生效。4.2 拦截器 vs. 过滤器如何选择这是面试和实际设计中经常遇到的问题。它们很相似但定位不同。特性SpringMVC 拦截器 (Interceptor)Servlet 过滤器 (Filter)规范/容器Spring框架的一部分依赖于Spring MVC。Java EE (Servlet) 规范的一部分由Servlet容器如Tomcat管理。作用范围只拦截进入Spring MVC处理流程的请求即DispatcherServlet处理的请求。拦截所有请求包括静态资源js, css, 图片、JSP、以及进入Spring MVC的请求。依赖注入支持。本身是Spring Bean可以使用Autowired等注入其他Bean。不支持。不是Spring Bean需要通过其他方式如SpringContextUtil获取Bean不优雅。获取处理器信息可以。preHandle的handler参数能拿到HandlerMethod从而获取控制器方法、注解等信息。不可以。只能拿到Servlet层面的ServletRequest和ServletResponse。执行时机在DispatcherServlet内部处理器执行前后、视图渲染后。在DispatcherServlet之前pre和之后post。使用场景与Spring MVC业务强相关的逻辑登录校验、权限检查、日志记录针对API、全局数据模型处理。更底层、更通用的逻辑字符编码设置、CORS跨域处理、全局压缩、XSS/SQL注入防御在参数进入Spring前过滤。选择建议如果你的逻辑需要用到Spring的上下文如调用Service层方法、或者需要根据控制器方法的注解做判断用拦截器。如果你的逻辑非常通用且需要在Spring MVC框架之外生效比如对静态资源请求也做日志记录或者处理HTTP请求/响应的底层属性如编码用过滤器。在现代Spring Boot项目中对于跨域CORS更推荐使用CrossOrigin注解或WebMvcConfigurer.addCorsMappings配置而不是过滤器。4.3 常见“坑点”与排查指南即使理解了原理在实际使用拦截器时还是会遇到一些意想不到的问题。问题一拦截器对RestController无效postHandle的ModelAndView为null。原因RestController默认返回的是JSON数据其处理流程是通过HttpMessageConverter直接写入HttpServletResponse的输出流不经过视图解析器因此不会创建ModelAndView对象。postHandle方法虽然会被调用但modelAndView参数是null。解决方案如果需要在Controller方法执行后做一些事情比如记录响应日志不要依赖postHandle的ModelAndView。可以考虑在preHandle中记录请求开始时间和信息在afterCompletion中记录结束时间和最终状态通过response.getStatus()获取状态码。使用Spring AOP的AfterReturning环绕RestController的方法这样可以获取到方法的返回值。使用ResponseBodyAdvice接口它专门用于在ResponseBody或RestController方法返回值被写入响应体之前进行干预。问题二拦截器内抛出的异常无法被全局异常处理器ControllerAdvice捕获。原因全局异常处理器ControllerAdvice ExceptionHandler处理的是处理器Controller执行过程中抛出的异常。而拦截器的preHandle方法执行在处理器之前它内部抛出的异常会直接抛给Servlet容器绕过Spring MVC的异常处理机制。解决方案在拦截器内部消化异常这是最常用的方式。在preHandle中使用try-catch捕获所有异常然后构造错误响应并返回false。使用过滤器Filter并在外面包裹一层try-catch然后将异常转发request.getRequestDispatcher(“/error”).forward(request, response)到某个统一的错误处理端点。但这种方式比较重。实现HandlerExceptionResolver接口来自定义异常解析但它粒度较粗不推荐仅为此目的使用。问题三静态资源如图片、CSS也被拦截器拦截了。原因拦截器配置的路径模式如/**匹配了静态资源路径而Spring Boot默认的静态资源处理器可能优先级配置不当或者拦截器注册时没有正确排除静态资源路径。解决方案精确配置拦截路径不要轻易使用/**。使用/api/**、/admin/**等业务路径前缀。使用excludePathPatterns排除明确排除静态资源路径如/static/**,/resources/**,/public/**,/*.ico等。检查Spring Boot静态资源配置确保没有通过WebMvcConfigurer.addResourceHandlers自定义配置覆盖了默认的静态资源处理链。问题四拦截器中无法通过Autowired注入Bean报NullPointerException。原因如果你通过new关键字手动创建了拦截器实例如registry.addInterceptor(new AuthInterceptor())那么这个拦截器对象不在Spring容器管理之下其内部的Autowired注解自然不会生效。解决方案务必让Spring来管理你的拦截器。在拦截器类上添加Component注解。在配置类中通过Autowired注入该拦截器Bean再注册。Configuration public class WebConfig implements WebMvcConfigurer { Autowired // 确保这里能注入 private AuthInterceptor authInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor); // 使用注入的Bean } }拦截器是SpringMVC框架中一个经典且强大的组件它体现了AOP思想在Web层的巧妙应用。掌握它不仅能让你写出更清晰、更易维护的代码更能让你深入理解一个HTTP请求在SpringMVC内部的完整旅程。从简单的登录校验到复杂的审计日志、接口限流拦截器都是你工具箱中不可或缺的利器。在实际使用中多思考其执行顺序、生命周期以及与过滤器、AOP的职责边界就能避免大多数坑让它真正为你的项目服务。