SpringMVC拦截器深度解析:从核心原理到动态权限控制实战
1. 从“请求”到“响应”:为什么我们需要拦截器?
在任何一个稍微有点规模的Web应用里,你肯定遇到过这样的场景:用户登录后,才能访问个人中心;管理员操作前,需要校验权限;每次请求来了,得记录下日志看看谁在访问、花了多长时间。这些事儿,如果每个Controller方法里都写一遍校验、记录、权限判断的代码,那代码很快就会变得臃肿不堪,维护起来简直是噩梦。
SpringMVC的拦截器(Interceptor),就是为了解决这个“横切关注点”问题而生的。你可以把它想象成高速公路上的收费站和检查站。用户的HTTP请求就是一辆车,从入口(DispatcherServlet接收请求)到目的地(具体的Controller方法)之间,会经过好几个这样的“站点”。拦截器就是这些站点,它可以在车辆到达Controller之前(预处理)、Controller处理完毕之后(后处理)、以及整个请求完成之后(完成回调)这三个关键节点,对请求和响应进行拦截和处理。
和过滤器(Filter)不同,过滤器是Servlet规范的一部分,工作得更底层,它能过滤一切请求,包括静态资源。而拦截器是SpringMVC框架层面的,它的能力更强,因为它能获取到Spring的上下文,能知道当前请求具体是要交给哪个Handler(Controller方法)处理,也能方便地操作Handler相关的模型数据。简单说,过滤器像是小区的门卫,管所有进出的人;拦截器像是公司前台,只处理来公司办事的访客,并且清楚访客要找哪个部门。
最近看到“qwq拦截器”这类网络热词在开发者社区流传,虽然带着点戏谑的意味,但也侧面反映了拦截器在实际开发中的高频使用和重要性。它绝不是一个可有可无的装饰品,而是构建清晰、健壮、可维护的Web层架构的核心组件之一。接下来,我们就深入它的内部,看看怎么把它用好、用对。
2. 拦截器的核心三阶段:预处理、后处理与完成回调
理解拦截器的执行时机,是正确使用它的前提。一个拦截器的生命周期紧密环绕一次请求-响应过程,主要分为三个阶段,对应着HandlerInterceptor接口的三个方法。
2.1preHandle:请求的“安检门”
这是拦截器最先执行的方法,在HandlerAdapter调用具体的Controller方法之前被触发。
public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } // ... 其他方法 }它的核心职责是进行请求的预处理和决定是否放行。
- 参数解读:
request,response:标准的Http对象。handler:将要执行的处理器对象,通常是HandlerMethod,封装了目标Controller方法的信息。你可以通过它获取方法上的注解、参数等,实现更精细的控制。
- 返回值意义:
true:安检通过,请求会继续向下传递,执行下一个拦截器的preHandle或最终的Controller方法。false:安检不通过,请求就此打住。后续的拦截器、Controller方法都不会执行。此时,你需要负责通过response对象向客户端返回一个合理的响应(如重定向到登录页、返回错误JSON),否则浏览器会一直等待。
典型应用场景:
- 身份认证(Authentication):检查Session或Token中是否存在登录信息。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect("/login"); return false; // 未登录,中断请求,重定向到登录页 } return true; // 已登录,放行 } - 权限校验(Authorization):在已认证的基础上,判断用户是否有权限访问当前资源。这通常需要结合
handler参数,解析方法上的注解(如@PreAuthorize,但更常见的做法是在拦截器里自定义逻辑)。 - 请求日志记录:记录请求的URL、IP、时间等,用于监控和调试。
- 防重复提交:检查请求携带的Token,防止表单重复提交。
注意:
preHandle方法按拦截器配置的顺序执行。如果某个拦截器返回false,它以及它之前的拦截器的afterCompletion方法依然会被调用(如果它们的preHandle都返回了true),但它之后的拦截器的preHandle和所有拦截器的postHandle都不会执行。
2.2postHandle:渲染前的“化妆师”
这个方法在Controller方法执行之后,但在视图渲染之前被调用。此时,Controller方法已经返回了ModelAndView对象(或对应的模型数据)。
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { }它的核心职责是对模型数据(Model)或视图(View)进行后处理。
- 参数解读:
- 前三个参数同
preHandle。 modelAndView:Controller处理后的结果。可能是null(例如@ResponseBody注解的方法直接写回响应体),也可能包含了模型数据和视图信息。
- 前三个参数同
- 返回值:无。
典型应用场景:
- 向所有视图添加公共模型数据:比如,在每个页面上都需要显示当前登录用户信息、网站公告等。你可以在这里向
modelAndView中添加这些通用的属性。@Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (modelAndView != null) { // 假设从请求属性中获取了当前用户 User currentUser = (User) request.getAttribute("currentUser"); modelAndView.addObject("currentUser", currentUser); // 添加站点名称 modelAndView.addObject("siteName", "我的技术博客"); } } - 统一修改视图名称:根据某些条件,动态改变要渲染的视图路径。
- 对模型数据进行二次加工或校验。
重要提示:
postHandle方法按拦截器配置的逆序执行。也就是说,最后配置的拦截器的postHandle会最先执行。另外,如果preHandle返回false,则postHandle不会被调用。
2.3afterCompletion:收尾的“清洁工”
这是拦截器链中最后执行的方法,在整个请求完成之后,即视图渲染完毕(或@ResponseBody内容写入完成)之后调用。无论请求处理过程中是否发生异常,这个方法都会被调用(前提是该拦截器的preHandle返回了true)。
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { }它的核心职责是进行资源清理和最终处理。
- 参数解读:
ex:请求处理过程中抛出的异常。如果整个过程顺利,此参数为null。
- 返回值:无。
典型应用场景:
- 性能监控与统计:记录请求的总耗时。通常会在
preHandle中记录开始时间,存入ThreadLocal或请求属性中,在afterCompletion中计算耗时并记录日志或发送到监控系统。@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { long startTime = (Long) request.getAttribute("startTime"); long endTime = System.currentTimeMillis(); log.info("请求 [{}] 耗时:{} ms", request.getRequestURI(), endTime - startTime); } - 资源清理:清理
ThreadLocal变量,防止内存泄漏。这是使用ThreadLocal时至关重要的一步。 - 异常日志的补充记录:虽然Spring有全局异常处理器(
@ControllerAdvice),但在这里可以记录一些更上下文相关的异常信息。
注意:
afterCompletion方法也按拦截器配置的逆序执行。它的执行类似于try-finally块中的finally,是进行收尾工作的安全地带。
3. 实战:如何定义并配置一个拦截器
理解了原理,我们来动手实现一个完整的拦截器。我们将实现一个日志拦截器,记录请求的入参、出参和耗时。
3.1 第一步:实现HandlerInterceptor接口
创建一个类,实现HandlerInterceptor接口,并重写你需要的方法。通常我们会使用@Component注解将其交给Spring管理。
package com.example.demo.interceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; import java.util.stream.Collectors; @Component @Slf4j public class ApiLogInterceptor implements HandlerInterceptor { private static final String START_TIME_ATTR = "startTime"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 记录开始时间 request.setAttribute(START_TIME_ATTR, System.currentTimeMillis()); // 2. 记录请求信息(注意:GET参数在URL中,POST参数需要从流中读取,这里简单处理) String requestURI = request.getRequestURI(); String method = request.getMethod(); String queryString = request.getQueryString(); String clientIP = getClientIp(request); // 获取HandlerMethod信息,记录是哪个Controller方法 String handlerInfo = "Unknown Handler"; if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; handlerInfo = handlerMethod.getBeanType().getSimpleName() + "#" + handlerMethod.getMethod().getName(); } log.info("[请求开始] IP: {}, URI: {}, Method: {}, Handler: {}, Query: {}", clientIP, requestURI, method, handlerInfo, queryString); // 记录POST请求的Body(注意:Body流只能读一次,需要额外处理,如使用ContentCachingRequestWrapper) // 此处为简化示例,实际项目需谨慎处理。 return true; // 始终放行 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 计算耗时 Long startTime = (Long) request.getAttribute(START_TIME_ATTR); if (startTime != null) { long duration = System.currentTimeMillis() - startTime; int status = response.getStatus(); log.info("[请求结束] URI: {}, Status: {}, Duration: {} ms, Exception: {}", request.getRequestURI(), status, duration, ex != null ? ex.getMessage() : "None"); } // 清理属性 request.removeAttribute(START_TIME_ATTR); } /** * 获取客户端真实IP(考虑了代理情况) */ private String getClientIp(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } // 对于通过多个代理的情况,第一个IP为客户端真实IP if (ip != null && ip.contains(",")) { ip = ip.split(",")[0].trim(); } return ip; } }代码要点解析:
ThreadLocalvsRequest Attribute:这里选择将开始时间存储在HttpServletRequest的属性中,而不是ThreadLocal。因为在标准的Servlet容器和SpringMVC中,一次请求的生命周期内,处理线程可能发生变化(例如遇到异步处理),ThreadLocal不一定可靠。而Request Attribute是跟随请求对象走的,更为安全。HandlerMethod类型判断:不是所有handler都是HandlerMethod,例如对于静态资源请求,可能是ResourceHttpRequestHandler。进行类型判断可以避免ClassCastException。- 获取客户端IP:直接使用
request.getRemoteAddr()可能拿到的是代理服务器的IP。通过读取常见的代理转发头(如X-Forwarded-For)能更准确地获取真实IP。 - POST Body读取问题:在
preHandle中直接读取request.getInputStream()会导致Controller无法再次读取参数。如果需要记录POST Body,需要使用ContentCachingRequestWrapper对原Request进行包装,这是一个常见的“坑”。
3.2 第二步:配置拦截器并指定拦截路径
实现了拦截器,还需要告诉SpringMVC在哪些路径下使用它。这需要通过实现WebMvcConfigurer接口(或继承WebMvcConfigurationSupport,但更推荐前者)来配置。
package com.example.demo.config; import com.example.demo.interceptor.ApiLogInterceptor; 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 ApiLogInterceptor apiLogInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiLogInterceptor) .addPathPatterns("/api/**") // 拦截所有以 /api 开头的请求 .excludePathPatterns("/api/public/**", "/api/auth/login"); // 排除登录接口和公共接口 // 可以继续添加更多拦截器 // registry.addInterceptor(new AuthInterceptor()).addPathPatterns("/**"); } }配置参数详解:
.addInterceptor(interceptor):注册拦截器实例。.addPathPatterns(String... patterns):指定要拦截的路径模式。支持Ant风格(?,*,**)和正则表达式。?匹配一个字符。*匹配零个或多个字符(不包含路径分隔符/)。**匹配零个或多个目录。- 例如:
/user/*匹配/user/123,但不匹配/user/123/profile;/user/**则两者都匹配。
.excludePathPatterns(String... patterns):指定要排除的路径模式。常用于登录、验证码、静态资源等不需要拦截的接口。- 拦截器顺序:在
addInterceptors方法中注册的顺序,就是拦截器preHandle的执行顺序,也是postHandle和afterCompletion的逆序。通常把最通用的拦截器(如日志)放在前面,把最具体的拦截器(如权限校验)放在后面。
4. 进阶:拦截器中的那些“坑”与最佳实践
在实际项目中,仅仅实现基础功能是不够的。下面这些我踩过的坑和总结的经验,可能比官方文档更有用。
4.1 路径匹配的优先级与陷阱
路径配置看似简单,但理解不透彻容易导致拦截失效或过度拦截。
registry.addInterceptor(interceptorA) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/login", "/admin/static/**"); registry.addInterceptor(interceptorB) .addPathPatterns("/**");问题:对于路径/admin/user/list,哪个拦截器生效?答案是:两个都生效。InterceptorB配置了/**会匹配所有路径。执行顺序是A.preHandle->B.preHandle-> ... ->B.postHandle->A.postHandle。
最佳实践:
- 精确匹配优先:尽量使用精确的路径模式,避免滥用
/**。/**通常只用于全局性的拦截器(如日志、跨域)。 - 理清业务边界:将不同业务模块的拦截器分开配置,并利用
excludePathPatterns精细控制。例如,API日志拦截器拦截/api/**,管理后台权限拦截器拦截/admin/**,并排除/admin/login。 - 注意静态资源:Spring Boot默认将
/static,/public等目录下的资源映射为静态资源。如果你配置了/**的拦截器,默认会拦截到对.js,.css,.png等静态资源的请求。通常我们需要排除它们:.excludePathPatterns("/static/**", "/public/**", "/resources/**", "/error");"/error"路径是Spring Boot默认的错误页面路径,也建议排除,避免拦截器本身出错导致循环重定向。
4.2 异步请求(Async)下的拦截器行为
这是拦截器的一个重大变化点。在Spring MVC中,当Controller方法返回DeferredResult、Callable或使用@Async时,请求处理是异步的。
preHandle:正常执行,在异步任务提交前。postHandle:不会立即执行!它会在异步任务开始后立即被调用,此时异步任务可能还没完成,模型数据可能不完整。这对于需要依赖异步处理结果的postHandle逻辑来说是致命的。afterCompletion:同样,会在异步任务开始后、主请求线程结束时立即被调用,而不是在异步任务完成后。
解决方案:使用AsyncHandlerInterceptor它是HandlerInterceptor的子接口,增加了两个专门用于异步处理的方法:
default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { }当Controller开启异步处理时,postHandle和afterCompletion不会被调用,取而代之的是afterConcurrentHandlingStarted被调用。你可以在这里进行一些资源清理。
如果需要在异步任务真正完成后执行逻辑,就不能依赖拦截器了。你需要使用:
DeferredResult的setResultHandler或Callable的返回后处理。- Spring的
@EventListener监听ServletRequestHandledEvent事件。 - 在异步任务内部(如
Async注解的方法里)手动调用你的后处理逻辑。
实战建议:对于有异步接口的项目,日志记录、性能统计等收尾工作,最好转移到异步任务完成后的回调中,或者使用更全局的解决方案如Servlet Filter或AOP。
4.3 拦截器 vs 过滤器 vs 切片(AOP)
这是面试常考题,也是设计时容易混淆的地方。三者的区别和应用场景如下表所示:
| 特性 | 过滤器 (Filter) | 拦截器 (Interceptor) | 切片 (Aspect) |
|---|---|---|---|
| 所属规范/框架 | Servlet 规范 (J2EE) | Spring MVC 框架 | Spring AOP 框架 |
| 作用范围 | 最广,所有请求(包括静态资源) | 仅Spring MVC管理的请求 | Spring容器管理的Bean的方法调用 |
| 获取Spring上下文 | 不能直接获取,需通过SpringBeanAutowiringSupport等 | 能,本身就是Spring Bean | 能,基于Spring代理 |
| 获取Handler信息 | 不能 | 能,通过handler参数 | 不能(但可以获取方法签名) |
| 执行时机 | 在DispatcherServlet之前/之后 | 在DispatcherServlet之内,Controller方法前后 | 在Bean方法调用前后 |
| 典型场景 | 字符编码转换、CORS跨域、压缩、安全框架(如Shiro)的入口过滤 | MVC特定处理:登录校验、权限验证、日志记录、模型数据增强 | 业务层通用处理:事务管理、性能监控、缓存、日志(非Web层)、参数校验 |
如何选择?
- 需要处理静态资源、或做最底层的请求/响应包装(如读取Body)?-> 用过滤器。
- 需要对Web请求进行预处理、后处理,且需要知道是哪个Controller方法?-> 用拦截器。
- 需要对Service层业务方法进行通用增强(如记录执行时间、缓存)?-> 用AOP切片。
4.4 拦截器中的异常处理
在拦截器方法中抛出异常会怎样?
- 在
preHandle中抛出异常:请求会被中断,进入Spring MVC的异常处理流程(@ControllerAdvice或HandlerExceptionResolver),该拦截器的afterCompletion仍然会被调用(ex参数不为null),但后续拦截器和Controller不会执行。 - 在
postHandle或afterCompletion中抛出异常:异常会被捕获并记录到日志,但可能不会再走全局异常处理流程,因为响应可能已经提交了一部分。这可能导致不完整的响应或奇怪的客户端行为。
最佳实践:拦截器中的代码应尽可能健壮,做好异常捕获和处理,避免异常向上抛出影响主流程。对于可预见的错误(如权限不足),应在preHandle中通过response.sendError()或返回错误JSON并return false来优雅处理。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // ... 校验逻辑 if (!hasPermission) { response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"msg\":\"无权限访问\"}"); return false; } return true; } catch (Exception e) { log.error("拦截器预处理异常", e); // 即使出错,也返回一个统一的错误响应,而不是抛出异常 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return false; } }5. 动态拦截:实现更灵活的拦截规则
有时,静态的路径匹配无法满足复杂需求。比如,我们想根据数据库配置动态决定哪些接口需要拦截,或者根据用户角色动态排除某些路径。这就需要实现动态拦截。
核心思路是:自定义一个拦截器,在preHandle方法中,根据当前请求的URI、用户信息等,动态查询规则引擎(可以是数据库、配置文件、缓存等),来决定是否放行。
5.1 基于数据库配置的动态拦截器示例
假设我们有一张表interceptor_rule,存储了需要拦截的URL模式。
定义规则服务:
@Service public class DynamicInterceptorRuleService { @Autowired private RuleRepository ruleRepository; // 假设的DAO /** * 判断当前请求是否需要被拦截 * @param requestUri 请求URI * @param userRole 当前用户角色(可从Session获取) * @return true 需要拦截, false 放行 */ public boolean shouldIntercept(String requestUri, String userRole) { List<InterceptorRule> rules = ruleRepository.findActiveRules(); for (InterceptorRule rule : rules) { // 实现简单的Ant路径匹配,生产环境可用Spring的AntPathMatcher if (pathMatches(rule.getUrlPattern(), requestUri)) { // 检查规则是否对当前用户角色生效 return !rule.getExcludedRoles().contains(userRole); } } return false; // 没有匹配规则,默认放行 } private boolean pathMatches(String pattern, String path) { // 简化实现,实际应使用org.springframework.util.AntPathMatcher // 这里仅作示例 AntPathMatcher matcher = new AntPathMatcher(); return matcher.match(pattern, path); } }实现动态拦截器:
@Component public class DynamicAuthInterceptor implements HandlerInterceptor { @Autowired private DynamicInterceptorRuleService ruleService; @Autowired private UserService userService; // 获取当前用户 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestUri = request.getRequestURI(); // 获取当前用户角色(示例) User currentUser = userService.getCurrentUser(request); String role = (currentUser != null) ? currentUser.getRole() : "ANONYMOUS"; if (ruleService.shouldIntercept(requestUri, role)) { // 执行具体的拦截逻辑,如权限校验 if (!checkPermission(currentUser, requestUri)) { response.sendError(403, "Forbidden"); return false; } } // 不需要拦截或校验通过,放行 return true; } private boolean checkPermission(User user, String uri) { // 具体的权限校验逻辑 return true; // 示例 } }配置拦截器:这个动态拦截器通常配置为拦截所有路径
/**,因为它内部自己做了判断。@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(dynamicAuthInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/static/**"); // 仍然可以排除绝对不需要判断的路径 }
这种方式的优点是灵活性极高,规则变更无需重启应用。缺点是每次请求都需要查询规则,可能带来性能开销,需要通过缓存规则来优化。
5.2 结合注解实现方法级细粒度控制
另一种更常见的动态控制方式,是结合自定义注解和拦截器。这在权限控制场景下非常流行。
定义权限注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); // 权限标识符,如 "user:add" }在Controller方法上使用:
@RestController @RequestMapping("/api/user") public class UserController { @PostMapping @RequirePermission("user:add") public Result addUser(@RequestBody User user) { // ... return Result.success(); } }在拦截器中解析注解:
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; // 查找方法上的注解 RequirePermission annotation = handlerMethod.getMethodAnnotation(RequirePermission.class); if (annotation != null) { String requiredPermission = annotation.value(); // 获取当前用户权限列表(从Session、Token等) Set<String> userPermissions = getCurrentUserPermissions(request); if (!userPermissions.contains(requiredPermission)) { response.sendError(403, "缺少权限: " + requiredPermission); return false; } } } return true; }
这种方式将拦截规则声明在了代码层面,清晰直观,且能与Spring Security等安全框架的理念结合。它比纯路径匹配更精确,直接关联到业务方法。
6. 性能考量与拦截器链优化
当项目中有十几个甚至更多拦截器时,每个请求都要走过这一长串链条,性能影响不容忽视。虽然单个拦截器的开销很小,但积少成多。
优化策略:
- 精简拦截器逻辑:
preHandle中的代码应尽可能轻量,避免耗时的IO操作(如频繁的数据库查询)。对于需要查库的权限校验,应使用缓存(如Redis)存储用户-权限关系。 - 合理安排拦截器顺序:将最可能快速失败(如格式校验)、或最轻量(如日志记录)的拦截器放在前面。将重量级的、需要复杂计算的拦截器放在后面。这样,无效请求能在前期就被快速拒绝,减少后续开销。
- 利用
excludePathPatterns:这是最有效的优化手段之一。确保静态资源、健康检查端点(如/actuator/health)、内网API等明确不需要拦截的路径被精确排除。 - 避免在拦截器中做重复工作:如果多个拦截器都需要当前用户信息,可以在第一个拦截器中查询并放入
request属性中,后续拦截器直接使用,避免重复查询。// 在第一个拦截器(如AuthInterceptor)中 User user = userService.getUserByToken(token); request.setAttribute("CURRENT_USER", user); // 在后续拦截器中 User user = (User) request.getAttribute("CURRENT_USER"); - 对于异步请求,慎用拦截器:如前所述,异步请求下
postHandle和afterCompletion的行为不符合预期。对于异步接口,考虑将逻辑移到异步任务内部或使用事件监听,避免无用的拦截器执行。
拦截器是SpringMVC框架赋予我们的一把利器,用好了能让Web层代码整洁如新,逻辑清晰。它的核心价值在于对“横切关注点”的封装。但在享受便利的同时,必须深刻理解它的生命周期、执行顺序、在异步场景下的特殊表现,以及如何避免常见的性能陷阱。从简单的日志记录,到复杂的动态权限网关,拦截器的设计思想始终贯穿其中。掌握它,你就掌握了构建高可维护性Web应用的一项重要技能。
