
1. 项目概述为什么我们需要拦截器做Web开发尤其是用SpringMVC框架的朋友肯定都遇到过这样的场景用户登录状态的校验、接口访问权限的控制、请求日志的记录、或者是对某些敏感操作的审计。这些功能几乎每个项目都绕不开。如果把这些逻辑都写在每一个Controller方法里那代码会变得臃肿不堪维护起来简直是灾难。想象一下几十个接口每个接口开头都有一模一样的if (session.getAttribute(user) null) { return redirect:/login; }这画面太美不敢看。这时候SpringMVC拦截器Interceptor就该登场了。它就像是守在你Controller门前的“保安”和“秘书”。在请求真正到达你的业务处理方法之前拦截器可以先行介入进行一些预处理PreHandle在业务处理完毕之后、视图渲染之前它还能进行后处理PostHandle甚至在请求完全结束之后它还能进行收尾工作AfterCompletion。这种AOP面向切面编程思想的应用让我们能够将横切关注点如日志、安全、事务从核心业务逻辑中剥离出来实现代码的解耦和复用。简单来说拦截器帮你干了三件事在进门时查票权限校验在出门时记录日志审计在客人走后打扫资源清理。它和Servlet规范中的Filter过滤器功能上有重叠但属于SpringMVC框架体系内更高级、更易用的组件能直接获取到Spring的上下文和丰富的参数。接下来我们就深入拆解这个“门神”是如何工作的以及怎么把它用好、用对。2. 拦截器核心设计与工作原理拆解2.1 拦截器与过滤器的本质区别很多刚接触的朋友容易把Interceptor和Servlet Filter搞混虽然它们都是用来拦截请求的但定位和能力强弱完全不同。理解这个区别是正确选型的第一步。过滤器Filter是Servlet规范的一部分它的工作层级更靠前。一个HTTP请求过来会先经过Tomcat等Web容器然后立即进入过滤器链。过滤器可以对请求和响应进行最原始的加工比如修改字符编码、压缩响应内容、实现CORS跨域等。但过滤器有个很大的局限它不知道Spring的存在。在过滤器里你拿不到Spring的ApplicationContext也无法直接使用Autowired注入Spring管理的Bean需要一些额外技巧。它工作在更底层功能强大但比较“原始”。拦截器Interceptor则是SpringMVC框架的一部分。只有当请求通过了过滤器链进入了Spring的DispatcherServlet之后才会轮到拦截器上场。此时Spring的IoC容器已经完成了初始化因此拦截器本身就是一个Spring Bean你可以轻松地注入其他Service、Repository来进行复杂的业务逻辑判断。更重要的是拦截器的方法参数中直接包含了HandlerMethod对象你能知道当前请求将要执行的是哪个Controller的哪个方法甚至可以获取方法上的注解信息从而实现基于方法的精细拦截。用一个生活化的比喻Filter是小区大门保安对所有进出车辆进行最基础的检查是不是本小区车牌而Interceptor是你家楼下的单元门禁它认识楼里的每一户知道你要去谁家并能根据户主Controller方法设置的规则比如“拒绝推销”来决定是否放行。2.2 拦截器核心接口与执行链解析SpringMVC拦截器的核心是HandlerInterceptor接口。它定义了三个方法构成了一个请求处理的完整生命周期public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable ModelAndView modelAndView) throws Exception { } default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable Exception ex) throws Exception { } }1.preHandle预处理方法调用时机在Controller方法执行之前被调用。核心作用进行权限校验、登录检查、参数预处理等。这里是决定请求是否继续向下传递的关键节点。返回值boolean类型。返回true则执行链继续调用下一个拦截器或最终的Controller方法返回false则中断流程后续的拦截器和Controller都不会执行通常需要在此方法内直接通过response写回结果如跳转登录页。参数中的handler通常是HandlerMethod实例封装了目标Controller方法的信息是进行注解扫描、权限判断的利器。2.postHandle后处理方法调用时机在Controller方法执行之后但在视图渲染之前被调用。核心作用对Controller返回的ModelAndView进行加工。例如向所有页面的模型数据中添加一些全局变量如当前用户信息、站点配置。注意点如果Controller方法内部通过response直接输出数据并完成了请求比如ResponseBody注解的方法或者preHandle返回了false则postHandle方法不会被执行。3.afterCompletion完成回调方法调用时机在整个请求处理完毕之后即视图渲染结束之后被调用。核心作用进行资源清理、性能监控、异常日志记录等。这是“打扫战场”的阶段。关键参数ex如果处理过程中抛出了异常这个参数就是捕获到的异常对象如果流程正常结束则为null。无论是否发生异常该方法都会被调用这使得它非常适合做统一的异常日志记录和监控收尾。这三个方法构成了一个清晰的职责链。SpringMVC会将多个拦截器组成一个链式结构HandlerExecutionChain。当一个请求到来时preHandle方法按拦截器的配置顺序正序执行postHandle在Controller方法后按配置顺序逆序执行afterCompletion则在请求最终完成后同样按配置顺序逆序执行。理解这个“正序-逆序-逆序”的调用顺序对于设计有依赖关系的多个拦截器至关重要。3. 从零到一自定义拦截器实战理论讲得再多不如动手写一个。我们来实现一个最经典的场景登录状态校验拦截器。3.1 第一步实现HandlerInterceptor接口我们创建一个LoginInterceptor类并实现HandlerInterceptor接口。这里我们主要使用preHandle方法。Component // 声明为Spring组件方便被自动扫描和管理 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 判断handler是否为HandlerMethod避免对静态资源等请求进行拦截 if (!(handler instanceof HandlerMethod)) { return true; // 静态资源直接放行 } // 2. 检查请求中是否包含登录令牌这里以session为例实际项目多用JWT HttpSession session request.getSession(false); // false表示如果不存在则不创建新session if (session null || session.getAttribute(currentUser) null) { // 3. 未登录根据请求类型返回不同结果 String requestType request.getHeader(X-Requested-With); boolean isAjax XMLHttpRequest.equalsIgnoreCase(requestType); if (isAjax) { // AJAX请求返回JSON格式的错误信息 response.setContentType(application/json;charsetutf-8); response.setStatus(HttpStatus.UNAUTHORIZED.value()); // 401状态码 PrintWriter writer response.getWriter(); writer.write({\code\: 401, \msg\: \用户未登录或登录已过期\}); writer.flush(); } else { // 普通页面请求重定向到登录页 // 注意要记录当前请求的URL登录后可以跳转回来提升用户体验 String redirectUrl request.getRequestURI(); String queryString request.getQueryString(); if (StringUtils.hasText(queryString)) { redirectUrl ? queryString; } // 将目标URL存入session或cookie这里简单演示存入session request.getSession().setAttribute(redirectUrl, redirectUrl); response.sendRedirect(/login?redirect URLEncoder.encode(redirectUrl, UTF-8)); } return false; // 中断请求链 } // 4. 已登录将用户信息放入请求属性方便后续Controller使用 User currentUser (User) session.getAttribute(currentUser); request.setAttribute(currentUser, currentUser); return true; // 放行 } Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 可以在这里向所有页面的Model中添加一些全局数据比如用户信息如果preHandle已经放入request这里可以省略 if (modelAndView ! null request.getAttribute(currentUser) ! null) { modelAndView.addObject(currentUser, request.getAttribute(currentUser)); } } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 记录请求完成日志包含耗时、用户、路径等信息 if (ex ! null) { // 发生异常记录错误日志这里简单打印实际应使用日志框架 User user (User) request.getAttribute(currentUser); String username user ! null ? user.getUsername() : anonymous; System.err.printf([ERROR] 用户[%s]访问[%s]时发生异常%s%n, username, request.getRequestURI(), ex.getMessage()); // 注意此处仅作演示生产环境应使用如SLF4JLogback等日志框架并记录更详细的堆栈信息 } // 可以进行一些资源清理工作比如关闭ThreadLocal中存储的资源 } }实操要点解析handler类型判断这是新手极易忽略的一点。并非所有进入DispatcherServlet的请求都对应一个Controller方法。对于静态资源如图片、CSS、JS的映射handler可能是ResourceHttpRequestHandler。如果不做判断直接强转为HandlerMethod并尝试获取注解会抛出ClassCastException。因此先判断handler instanceof HandlerMethod是防御性编程的好习惯。区分AJAX与普通请求这是提升用户体验的关键。对于前端异步请求返回JSON错误信息和401状态码更友好对于同步页面请求则重定向到登录页。通过检查请求头X-Requested-With是一个常用方法。登录后跳转直接重定向到登录页会让用户丢失之前的操作路径。一个好的实践是记录下原始请求的URLrequest.getRequestURI()和request.getQueryString()并在登录成功后跳转回去。这里演示了存入session的一种简单方式。用户信息传递在preHandle中校验通过后我们将用户对象存入request.setAttribute。这样在后续的Controller方法中可以通过RequestAttribute注解或直接从HttpServletRequest中获取避免了重复从Session中读取。3.2 第二步配置拦截器并指定拦截路径实现接口只是第一步要让拦截器生效必须将其注册到SpringMVC的配置中。在Spring Boot环境下通常通过一个配置类来实现。Configuration public class WebMvcConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) // 拦截所有路径 .excludePathPatterns( // 排除不需要拦截的路径 /login, // 登录页面本身 /doLogin, // 登录提交接口 /logout, // 登出接口 /register, // 注册页面 /css/**, // 静态资源 /js/**, /images/**, /error // Spring Boot默认的错误页面路径 ) .order(1); // 设置拦截器执行顺序数字越小优先级越高 // 可以继续添加更多拦截器 // registry.addInterceptor(new AuditInterceptor()).addPathPatterns(/admin/**).order(2); } }配置详解与避坑指南addPathPatterns用于指定要拦截的路径模式。支持Ant风格如/user/*拦截一层路径/user/**拦截所有子路径和正则表达式。/**表示拦截所有请求。excludePathPatterns这是配置的关键也是最容易出问题的地方。你必须仔细列出所有不需要登录就能访问的公共资源否则会导致连登录页面都进不去的死循环。常见的排除项包括登录/注册相关的页面和接口、静态资源路径、公开的API接口、健康检查端点如/actuator/health等。order方法当有多个拦截器时通过order值决定它们preHandle的执行顺序。值越小优先级越高越先执行。但请注意postHandle和afterCompletion的执行顺序与preHandle相反。通常功能越基础、范围越广的拦截器如日志拦截器order值越小越先执行preHandle。4. 高级应用与深度优化4.1 基于注解的精细化拦截上面的例子是“一刀切”的拦截所有配置路径下的请求都需要登录。但在实际项目中权限控制往往更精细。比如某些接口需要管理员角色某些接口需要特定的权限码。这时结合自定义注解和拦截器是更优雅的方案。第一步定义权限注解Target(ElementType.METHOD) // 注解用在方法上 Retention(RetentionPolicy.RUNTIME) // 运行时保留 public interface RequirePermission { String value(); // 权限标识符如 user:add, order:query }第二步在Controller方法上使用注解RestController RequestMapping(/user) public class UserController { GetMapping(/list) RequirePermission(user:query) public ApiResult listUsers() { // ... 查询用户列表 } PostMapping RequirePermission(user:add) public ApiResult addUser(RequestBody User user) { // ... 添加用户 } }第三步升级拦截器解析注解我们在LoginInterceptor的preHandle方法中增加注解解析逻辑在登录校验通过之后Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // ... 之前的登录校验逻辑 ... // 登录校验通过后进行权限校验 if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; // 1. 获取方法上的RequirePermission注解 RequirePermission methodAnnotation handlerMethod.getMethodAnnotation(RequirePermission.class); // 2. 如果类上也有该注解可以合并判断这里以方法注解优先 if (methodAnnotation null) { methodAnnotation handlerMethod.getBeanType().getAnnotation(RequirePermission.class); } if (methodAnnotation ! null) { // 3. 当前请求需要的权限 String requiredPermission methodAnnotation.value(); // 4. 从当前登录用户信息中获取其拥有的权限列表这里从session中取实际可能来自数据库或缓存 User currentUser (User) request.getSession().getAttribute(currentUser); SetString userPermissions getPermissionsByUserId(currentUser.getId()); // 假设这个方法能获取用户权限集 // 5. 校验权限 if (!userPermissions.contains(requiredPermission)) { response.setContentType(application/json;charsetutf-8); response.setStatus(HttpStatus.FORBIDDEN.value()); // 403 禁止访问 response.getWriter().write({\code\: 403, \msg\: \权限不足\}); return false; } } } return true; }这种方式将权限规则以声明式注解的方式附加在方法上拦截器统一执行校验实现了控制逻辑与业务逻辑的完美分离代码清晰且易于维护。4.2 拦截器执行顺序与依赖管理当项目中有多个拦截器时理解并管理它们的执行顺序至关重要。假设我们有三个拦截器LogInterceptor日志记录、AuthInterceptor权限校验、PerformanceInterceptor性能监控。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { // 日志拦截器记录所有请求入口和出口应该最先开始最后结束。 registry.addInterceptor(new LogInterceptor()).addPathPatterns(/**).order(10); // 权限拦截器依赖日志需要在日志记录开始后进行校验。 registry.addInterceptor(new AuthInterceptor()).addPathPatterns(/api/**).order(20); // 性能监控拦截器需要在业务方法执行前后计时应在权限校验之后业务逻辑之前。 registry.addInterceptor(new PerformanceInterceptor()).addPathPatterns(/api/**).order(30); } }执行流程分析preHandle正序执行LogInterceptor.preHandle(order10) -AuthInterceptor.preHandle(order20) -PerformanceInterceptor.preHandle(order30)。若所有preHandle返回true则执行Controller方法。postHandle逆序执行PerformanceInterceptor.postHandle-AuthInterceptor.postHandle-LogInterceptor.postHandle。afterCompletion逆序执行PerformanceInterceptor.afterCompletion-AuthInterceptor.afterCompletion-LogInterceptor.afterCompletion。一个关键陷阱如果AuthInterceptor.preHandle校验失败返回了false那么执行链会立即中断。此时PerformanceInterceptor.preHandle以及后续所有的preHandle都不会执行。但是已经执行过preHandle且返回了true的拦截器它的afterCompletion方法仍然会被调用。在上例中如果AuthInterceptor.preHandle返回false那么LogInterceptor的afterCompletion依然会被触发而PerformanceInterceptor的任何方法都不会执行。设计拦截器时必须考虑这种部分成功、部分失败的情况确保资源清理afterCompletion中的逻辑是幂等的或能正确处理这种场景。4.3 异步请求下的拦截器在Spring MVC中当Controller方法返回DeferredResult、Callable或使用ResponseBody配合异步Servlet时请求处理是异步的。标准的HandlerInterceptor对于异步请求的生命周期处理有所不同。从Spring 3.2开始提供了AsyncHandlerInterceptor接口它继承了HandlerInterceptor增加了一个afterConcurrentHandlingStarted方法。当控制器方法开启异步处理时postHandle和afterCompletion方法不会像同步请求那样被立即调用。对于异步请求preHandle正常执行。控制器方法返回异步结果对象请求被挂起。afterConcurrentHandlingStarted方法被立即调用。此时postHandle和afterCompletion还不会执行。异步任务完成后Spring会重新发起一次内部调度一个新的拦截器链会被执行但preHandle可能不再执行取决于配置并最终调用postHandle和afterCompletion。这意味着如果你在postHandle中向模型添加数据或者在afterCompletion中记录日志对于异步请求这些操作会延迟到异步任务完成后才执行。如果你的拦截器逻辑严重依赖于请求的同步生命周期比如在postHandle中修改响应头在异步场景下可能需要调整或者使用DeferredResult等机制提供的回调接口如onCompletion来处理。5. 常见问题排查与实战技巧5.1 拦截器不生效逐项检查清单配置类未被扫描确保你的Configuration配置类位于Spring Boot主应用类SpringBootApplication所在包或其子包下或者被ComponentScan显式指定。路径匹配错误仔细检查addPathPatterns和excludePathPatterns。常见的错误是排除模式写得不全导致拦截了静态资源请求。使用/**时要特别小心。静态资源被拦截Spring Boot默认将静态资源映射在/static,/public,/resources,/META-INF/resources路径下。如果你自定义了拦截路径/**务必在excludePathPatterns中排除这些路径如/static/**。更推荐的做法是让Spring Boot的默认静态资源处理机制优先于拦截器。拦截器Bean未注入如果你在配置类中使用new关键字创建拦截器实例如new LoginInterceptor()那么这个拦截器内部通过Autowired注入的其他Bean将会失效因为该对象不是由Spring容器管理的。最佳实践是使用Component等注解将拦截器声明为Bean然后在配置类中Autowired注入。preHandle返回值错误确认你的preHandle方法在放行时返回了true。一个常见的疏忽是在方法末尾写了逻辑却忘了写return true;。过滤器Filter冲突如果项目中存在自定义的Filter并且它在链中过早地提交了响应调用了response.getWriter().close()或类似操作那么请求可能不会进入DispatcherServlet拦截器自然也不会执行。5.2 性能监控拦截器实战示例下面展示一个记录请求耗时的实用拦截器它结合了ThreadLocal来保存时间戳。Component public class PerformanceInterceptor implements HandlerInterceptor { // 使用ThreadLocal保存请求开始时间保证线程安全 private static final ThreadLocalLong startTimeThreadLocal new ThreadLocal(); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 记录请求开始时间 startTimeThreadLocal.set(System.currentTimeMillis()); return true; // 始终放行 } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { Long startTime startTimeThreadLocal.get(); if (startTime ! null) { long endTime System.currentTimeMillis(); long costTime endTime - startTime; String requestURI request.getRequestURI(); String method request.getMethod(); // 记录慢请求假设超过500毫秒为慢请求 if (costTime 500) { log.warn(慢请求告警 - URI: {} {}, 耗时: {}ms, method, requestURI, costTime); } // 常规日志记录可以输出到文件或监控系统 log.debug(请求完成 - URI: {} {}, 状态: {}, 耗时: {}ms, method, requestURI, response.getStatus(), costTime); // 非常重要清理ThreadLocal防止内存泄漏尤其是在使用线程池时 startTimeThreadLocal.remove(); } } }关键技巧与避坑使用ThreadLocal每个请求在一个独立的线程中处理ThreadLocal为每个线程提供了独立的变量副本完美契合这种“请求开始-结束”的生命周期数据传递。务必清理ThreadLocal这是使用ThreadLocal的黄金法则。必须在afterCompletion中调用remove()方法清除数据。因为Web服务器如Tomcat使用线程池如果不清除上一个请求的数据可能会残留在线程中被下一个请求读到导致严重的数据错乱。这是生产环境一个非常隐蔽的Bug来源。区分日志级别将慢请求用WARN或ERROR级别记录便于监控系统告警常规请求用DEBUG或INFO级别避免日志泛滥。5.3 拦截器内异常处理的最佳实践拦截器本身也可能抛出异常。如果异常在preHandle中抛出请求链会中断并且该拦截器之前已经成功执行preHandle的拦截器其afterCompletion方法仍然会被调用但postHandle都不会执行。建议做法在拦截器内部进行细致的异常捕获和处理避免异常抛到Spring MVC的通用异常处理器之外导致不可预知的行为。例如在权限拦截器中如果查询用户权限的数据库调用失败应该捕获这个异常然后返回一个明确的“系统错误”JSON响应或跳转到错误页而不是让一个SQLException直接抛给用户。public boolean preHandle(...) { try { // ... 权限查询等可能出错的逻辑 ... return checkPermission(); } catch (DataAccessException e) { log.error(权限校验时数据库异常, e); sendErrorResponse(response, 500, 系统内部错误权限校验失败); return false; } catch (Exception e) { log.error(权限校验时发生未知异常, e); sendErrorResponse(response, 500, 系统异常); return false; } }拦截器是Spring MVC框架中一个强大而灵活的组件它并非洪水猛兽而是帮助我们构建整洁、可维护架构的得力助手。理解其生命周期、掌握其配置技巧、规避其常见陷阱你就能让它真正为你的项目服务而不是成为麻烦的来源。从我多年的经验看把登录、权限、日志、监控这些横切关注点交给拦截器是让Controller保持“瘦”和“专注”的不二法门。