SpringMVC拦截器实战:从登录校验到接口限流的完整指南
1. 项目概述:SpringMVC拦截器的核心价值
在Web应用开发中,我们经常遇到这样的场景:用户登录后才能访问个人中心、管理员才能操作后台数据、或者对所有请求进行统一的日志记录和耗时统计。如果把这些逻辑分散在每个控制器方法里,代码会变得臃肿不堪,维护起来更是噩梦。SpringMVC拦截器(Interceptor)就是为了解决这类“横切关注点”而生的利器。它允许你在请求到达控制器之前、控制器处理之后、以及视图渲染之后这三个关键节点,插入自定义的处理逻辑,实现一种声明式的、非侵入式的功能增强。
简单来说,拦截器就像一道关卡,或者一个尽职的“门卫”。当一个HTTP请求进入SpringMVC的领地时,这个门卫会先检查你的“证件”(比如Session里有没有登录信息),决定是放行还是直接劝返。等请求在控制器里办完事准备返回时,门卫可能还会进行一些“善后工作”,比如记录一下这次访问。SpringMVC拦截器的设计,完美契合了AOP(面向切面编程)的思想,将那些与核心业务无关但又必须存在的公共逻辑剥离出来,让我们的控制器代码保持清爽和纯粹。
对于开发者而言,掌握拦截器意味着你掌握了构建健壮、安全、可维护Web应用的又一把钥匙。无论是做权限校验、日志审计、性能监控,还是处理全局异常、统一响应格式,拦截器都是不可或缺的组件。接下来,我们就从设计思路到实战细节,彻底拆解SpringMVC拦截器。
2. 拦截器整体设计与核心思路拆解
2.1 拦截器与过滤器的本质区别
很多刚接触的同学容易把拦截器和Servlet Filter(过滤器)搞混,它们确实有相似之处,但定位和粒度完全不同。理解这个区别,是正确选用它们的前提。
过滤器(Filter)是Servlet规范的一部分,它的工作时机非常靠前。当一个请求到达Web容器(如Tomcat)时,会先进入过滤器链。过滤器可以对请求和响应进行最原始的加工,比如修改字符编码、压缩响应内容、实现CORS跨域支持等。过滤器的能力很强大,但它对Spring的上下文(ApplicationContext)一无所知,无法直接使用Spring管理的Bean(如Service、Component)。
拦截器(Interceptor)是SpringMVC框架的一部分,它的工作时机在DispatcherServlet接收到请求之后。此时,Spring的IoC容器已经初始化完毕,请求的映射关系也已确定(即知道该由哪个Controller的哪个方法来处理)。因此,拦截器可以无缝地使用Spring容器中的任何Bean,这是它最大的优势。它的拦截目标也更明确,是针对Handler(即控制器方法)的。
一个形象的比喻是:过滤器是“城门守卫”,在请求刚进入你的王国(Web应用)时就进行检查;而拦截器是“宫殿侍卫”,在请求已经进入宫殿(DispatcherServlet),准备面见国王(Controller)时再进行更细致的盘查和记录。
2.2 拦截器的三大生命周期方法
SpringMVC拦截器的核心是一个实现了HandlerInterceptor接口的类。这个接口定义了三个方法,对应了请求处理流程的三个关键节点:
preHandle方法:在控制器方法执行之前被调用。这是最常用、最核心的方法。通常在这里进行权限验证、登录检查等。其返回值是布尔类型:true:放行,继续执行后续的拦截器和控制器方法。false:中断流程,后续的拦截器和控制器方法都不会执行。通常需要在此方法内通过HttpServletResponse直接返回响应(如重定向到登录页)。
postHandle方法:在控制器方法执行之后,视图渲染之前被调用。此时控制器方法已经执行完毕,你可以拿到控制器返回的ModelAndView对象,并对其中的模型数据(Model)或视图(View)进行修改。这个方法在异步请求处理中可能不会被调用。postHandle方法:在整个请求结束之后,即视图渲染完毕之后被调用。这个方法主要用于资源清理工作,比如记录请求完成日志、释放某些线程绑定资源等。特别注意:这个方法无论控制器执行过程中是否抛出异常,都会被调用(类似于try-catch-finally中的finally块),因此非常适合做最终的资源清理和统计。
理解这三个方法的执行时机和职责,是灵活运用拦截器的关键。一个典型的拦截器工作流程如下图所示(此处以文字描述):请求进入 ->preHandle1->preHandle2-> ... -> 控制器执行 ->postHandle2->postHandle1-> 视图渲染 ->afterCompletion1->afterCompletion2。
2.3 拦截器的注册与配置方式
定义了拦截器类之后,你需要告诉SpringMVC在哪些请求路径上使用它。这通过在配置类中实现WebMvcConfigurer接口并重写addInterceptors方法来完成。
配置的核心在于InterceptorRegistry对象,它提供了链式调用的方法来添加和配置拦截器:
addInterceptor(Interceptor interceptor):注册一个拦截器实例。addPathPatterns(String... patterns):指定需要拦截的路径模式,支持Ant风格(如/admin/**)和正则表达式。excludePathPatterns(String... patterns):指定需要排除的路径,比如登录接口、静态资源等。
这种配置方式非常灵活,你可以为不同的拦截器指定不同的拦截路径,构建出精细的拦截策略。
3. 核心细节解析与实操要点
3.1 如何实现一个基础的登录校验拦截器
让我们从一个最经典的案例开始:实现一个登录校验拦截器。假设我们的应用要求,除了登录、注册和静态资源页面,其他所有页面都需要用户登录后才能访问。
首先,创建拦截器类LoginInterceptor:
@Component // 交由Spring管理,方便注入其他Bean public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 判断请求是否针对Handler方法(避免对静态资源等无效判断) if (!(handler instanceof HandlerMethod)) { return true; // 放行静态资源请求等 } // 2. 检查Session中是否存在用户登录标识 HttpSession session = request.getSession(false); // false表示如果不存在则不创建新session if (session != null && session.getAttribute("currentUser") != null) { // 用户已登录,放行 return true; } // 3. 用户未登录,根据请求类型返回相应响应 // 判断是否为Ajax请求(通常通过请求头识别) String xRequestedWith = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) { // 对于Ajax请求,返回JSON格式的未登录状态码 response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); // 401状态码 PrintWriter writer = response.getWriter(); writer.write("{\"code\": 401, \"msg\": \"用户未登录或登录已过期\"}"); writer.flush(); } else { // 对于普通页面请求,重定向到登录页 // 注意:这里最好使用绝对路径或从配置中读取,避免路径问题 String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/login"); } // 4. 中断后续执行 return false; } // postHandle和afterCompletion在本例中非必需,可以不重写 }关键点解析与注意事项:
handler参数类型判断:preHandle的handler参数可能是HandlerMethod(对应控制器方法),也可能是ResourceHttpRequestHandler(对应静态资源)。如果不加判断,对静态资源的请求也会执行Session检查,这通常是不必要且可能引发错误的。因此,先判断类型是一个好习惯。- Session获取方式:
request.getSession(false)是关键。如果传入true(默认值),当Session不存在时,服务器会创建一个新的空Session。这会导致即使未登录的用户也会拥有一个Session ID,可能被用于某些跟踪或攻击,同时也浪费服务器资源。传入false则只在Session已存在时返回它,否则返回null。 - 区分请求类型:现代前端应用大量使用Ajax。如果用户会话过期,前端发起的Ajax请求如果被重定向到HTML登录页,前端JavaScript通常无法正确处理。因此,需要根据请求头
X-Requested-With判断,对Ajax请求返回结构化的JSON错误信息,对普通请求进行重定向。这是一种非常友好的用户体验设计。 - 路径处理:在重定向时,使用
request.getContextPath()获取应用上下文路径,再拼接目标路径,可以避免在应用部署路径不是根路径(/)时产生404错误。
3.2 配置拦截器并设置拦截/排除路径
接下来,在配置类中注册这个拦截器:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns( // 排除不需要拦截的路径 "/", "/login", "/register", "/api/login", // 登录接口 "/api/register", // 注册接口 "/css/**", // 静态资源 "/js/**", "/images/**", "/favicon.ico" ); // 可以继续添加其他拦截器 // registry.addInterceptor(new AnotherInterceptor()).addPathPatterns("/admin/**"); } }配置要点:
- 拦截路径
/**:表示拦截所有请求,这是最宽泛的配置。 - 排除路径:必须将登录、注册、静态资源等公开访问的路径明确排除,否则会导致用户无法登录,或者页面加载不了CSS/JS文件。
- 路径顺序:
excludePathPatterns的优先级高于addPathPatterns。即一个请求如果匹配了排除模式,即使它也匹配拦截模式,也不会被拦截。 - 静态资源处理:在Spring Boot中,静态资源通常有默认的映射路径(如
/static/**,/public/**)。你需要根据你的实际资源存放位置来配置排除模式。一个更稳妥的方式是确保你的静态资源请求不会被DispatcherServlet处理(例如,通过配置spring.mvc.static-path-pattern),这样它们根本不会进入拦截器链。
3.3 拦截器链的执行顺序与优先级
在实际项目中,我们往往有多个拦截器,比如一个负责日志,一个负责权限,一个负责防刷。它们的执行顺序至关重要。
执行顺序由注册顺序决定。在addInterceptors方法中,先注册的拦截器,其preHandle方法会先执行,但postHandle和afterCompletion方法会后执行。这形成了一个“栈”式的调用结构。
假设我们注册了A、B两个拦截器:
A.preHandle->B.preHandle- 控制器方法执行
B.postHandle->A.postHandle- 视图渲染
B.afterCompletion->A.afterCompletion
preHandle的连锁效应:如果A.preHandle返回false,则请求被中断,B.preHandle和控制器方法都不会执行。但已经执行了preHandle且返回true的拦截器,其afterCompletion方法仍然会被调用。例如,如果A放行,B拦截,那么执行流是:A.preHandle(true) ->B.preHandle(false) ->A.afterCompletion。这一点在编写资源清理逻辑时需要特别注意。
实操心得:对于有依赖关系的拦截器,一定要规划好注册顺序。例如,一个需要记录用户操作日志的拦截器,它可能需要依赖权限拦截器解析出的当前用户信息。那么权限拦截器就必须注册在日志拦截器之前,确保在日志拦截器的
preHandle中能拿到用户数据。
4. 实操过程与核心环节实现
4.1 实现一个全局请求日志与耗时统计拦截器
除了权限,日志是另一个拦截器的典型应用场景。我们希望记录每一个请求的详细信息,包括IP、URL、参数、耗时以及响应状态,这对于问题排查和系统监控非常有价值。
@Component @Slf4j // 使用Lombok注解简化日志声明 public class RequestLogInterceptor implements HandlerInterceptor { // 使用ThreadLocal来保存请求开始时间,保证线程安全 private static final ThreadLocal<Long> START_TIME_HOLDER = new ThreadLocal<>(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 记录请求开始时间 START_TIME_HOLDER.set(System.currentTimeMillis()); // 记录请求基本信息(注意:在生产环境,打印包含敏感信息的参数需谨慎) if (log.isInfoEnabled()) { String queryString = request.getQueryString(); String requestURI = request.getRequestURI(); String clientIP = getClientIpAddress(request); String method = request.getMethod(); log.info("Request Start => IP: [{}], Method: [{}], URI: [{}], Params: [{}]", clientIP, method, requestURI, queryString); } return true; // 始终放行,此拦截器仅用于记录 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 计算耗时 Long startTime = START_TIME_HOLDER.get(); if (startTime != null) { long duration = System.currentTimeMillis() - startTime; int status = response.getStatus(); String requestURI = request.getRequestURI(); // 根据是否有异常和耗时长短,选择不同的日志级别 if (ex != null) { log.error("Request Error => URI: [{}], Status: [{}], Duration: [{}ms], Exception: [{}]", requestURI, status, duration, ex.getMessage(), ex); } else if (duration > 1000) { log.warn("Request Slow => URI: [{}], Status: [{}], Duration: [{}ms]", requestURI, status, duration); } else { log.info("Request End => URI: [{}], Status: [{}], Duration: [{}ms]", requestURI, status, duration); } } // 务必清理ThreadLocal,防止内存泄漏 START_TIME_HOLDER.remove(); } /** * 获取客户端真实IP(考虑了代理情况) */ private String getClientIpAddress(HttpServletRequest request) { String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } // 对于通过多个代理的情况,第一个IP才是真实IP if (ip != null && ip.contains(",")) { ip = ip.substring(0, ip.indexOf(',')).trim(); } return ip; } }配置这个拦截器时,通常我们会把它放在链的最前面,以确保它能记录最完整的耗时,并排除对静态资源的记录以提升性能:
@Override public void addInterceptors(InterceptorRegistry registry) { // 日志拦截器放在最前面 registry.addInterceptor(requestLogInterceptor) .addPathPatterns("/**") .excludePathPatterns("/css/**", "/js/**", "/images/**", "/favicon.ico"); // 其他拦截器,如登录拦截器 registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns(...); // 排除路径 }4.2 利用拦截器实现接口防刷与限流
在高并发场景下,防止恶意用户或脚本频繁调用接口(刷单、刷票、暴力破解)是刚需。拦截器可以作为一个轻量级的限流闸口。
下面实现一个基于内存(使用Guava的RateLimiter或简单计数器)的简易防刷拦截器。这里展示一个基于IP和URI的计数器方案:
@Component public class RateLimitInterceptor implements HandlerInterceptor { // 使用ConcurrentHashMap存储计数器,Key为"IP:URI" private final ConcurrentHashMap<String, RequestCounter> requestCountMap = new ConcurrentHashMap<>(); // 清理过期计数器的时间间隔(毫秒) private static final long CLEAN_UP_INTERVAL = 60 * 1000L; // 最后一次清理时间 private volatile long lastCleanUpTime = System.currentTimeMillis(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 定期清理过期计数器,避免内存无限增长 cleanUpExpiredCounters(); String clientIP = getClientIpAddress(request); // 复用之前的获取IP方法 String requestURI = request.getRequestURI(); String key = clientIP + ":" + requestURI; // 获取或创建该IP+URI的计数器 RequestCounter counter = requestCountMap.computeIfAbsent(key, k -> new RequestCounter()); // 定义规则:例如,每秒最多5次请求(这里简化成时间窗口内计数) long currentTime = System.currentTimeMillis(); if (counter.isAllowed(currentTime, 5, 1000L)) { // 1秒内最多5次 return true; } else { log.warn("触发限流:IP={}, URI={}", clientIP, requestURI); response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); // 429状态码 response.getWriter().write("{\"code\": 429, \"msg\": \"请求过于频繁,请稍后再试\"}"); return false; } } /** * 简单的计数器类 */ private static class RequestCounter { private final LinkedList<Long> timestamps = new LinkedList<>(); synchronized boolean isAllowed(long currentTime, int maxRequests, long timeWindowMillis) { // 移除时间窗口之外的旧时间戳 while (!timestamps.isEmpty() && currentTime - timestamps.peekFirst() > timeWindowMillis) { timestamps.pollFirst(); } // 判断当前数量是否超过限制 if (timestamps.size() < maxRequests) { timestamps.addLast(currentTime); return true; } return false; } } private void cleanUpExpiredCounters() { long now = System.currentTimeMillis(); if (now - lastCleanUpTime > CLEAN_UP_INTERVAL) { // 遍历map,移除长时间未访问的key(例如超过10分钟) long expiredTime = now - 10 * 60 * 1000L; requestCountMap.entrySet().removeIf(entry -> { // 这里简化处理:如果计数器里最新的时间戳都很旧,就移除 // 更严谨的做法需要维护计数器的最后访问时间 return entry.getValue().timestamps.isEmpty() || (now - entry.getValue().timestamps.peekLast() > expiredTime); }); lastCleanUpTime = now; } } }这个简易限流器的局限性:
- 单机内存限制:此方案基于应用实例的内存,在集群部署下无效。生产环境需要使用Redis等分布式缓存来实现集群限流。
- 精度与平滑性:滑动窗口算法比固定窗口更公平,上述简易计数器可以演化为更精确的滑动窗口或令牌桶算法(使用Guava
RateLimiter更简单)。 - 清理策略:需要妥善设计过期数据的清理策略,防止内存泄漏。
尽管有局限,但对于小型应用或作为第一道防线,这样的拦截器实现简单且有效。配置时,可以将其应用到特定的、需要保护的API路径上,例如/api/order/**、/api/submit/**。
5. 常见问题与排查技巧实录
在实际使用拦截器时,你可能会遇到一些“坑”。下面是我总结的一些典型问题及其解决方案。
5.1 拦截器不生效的排查步骤
这是最常见的问题。如果你的拦截器逻辑没有被执行,请按以下顺序检查:
- 检查拦截器类是否被Spring管理:确保你的拦截器类上有
@Component或其它Spring注解(如@Service),或者在配置类中通过@Bean方法手动创建。如果拦截器实例是通过new关键字创建的,Spring无法对其进行依赖注入,且其生命周期也不受管理。 - 检查配置类是否被加载:确保你的
WebMvcConfig配置类位于Spring Boot的主应用类(@SpringBootApplication标注的类)的子包或同级包下,或者被@ComponentScan显式扫描到。你可以通过在配置类的构造方法或某个方法上加@PostConstruct打印日志来验证。 - 检查拦截路径是否正确:仔细核对
addPathPatterns和excludePathPatterns。一个常见的错误是,你试图拦截/api/user/profile,但却排除了/api/**。记住,排除模式的优先级更高。使用/**时要特别小心。 - 检查静态资源路径:如果你的请求是获取CSS、JS、图片等静态资源,并且这些资源的请求路径没有被
excludePathPatterns排除,同时Spring MVC的静态资源处理机制没有生效(例如,你自定义了WebMvcConfigurer并重写了addResourceHandlers但配置有误),那么这些请求可能会“意外地”走到控制器映射环节,从而被拦截器处理。这通常不是期望的行为。确保静态资源被正确排除或由默认机制处理。 - 查看拦截器链顺序:如果你有多个拦截器,并且前面的某个拦截器在
preHandle中返回了false,那么后面的拦截器就不会执行。检查日志或调试,确认执行流在哪个环节被中断了。 - 确认请求是否经过DispatcherServlet:拦截器是SpringMVC框架的一部分,只有映射到
DispatcherServlet的请求才会经过拦截器链。如果你有一些通过Servlet原生API或其它框架处理的请求(例如,直接映射的Servlet、WebSocket端点),这些请求是不会触发拦截器的。
5.2 拦截器中获取请求Body内容的问题
有时我们想在拦截器里记录或校验请求的Body内容(比如JSON参数)。但是,HttpServletRequest的输入流(getInputStream()或getReader())通常只能读取一次。如果在拦截器中读取了,那么后续的控制器方法里就读取不到了,会导致参数绑定失败。
解决方案:
使用Spring提供的ContentCachingRequestWrapper来包装原始的HttpServletRequest。这个包装类会将请求Body内容缓存到内存中,允许你多次读取。
@Component public class RequestBodyLogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 包装请求,使其支持多次读取Body if (request instanceof ContentCachingRequestWrapper) { // 已经包装过,避免重复包装 logRequest((ContentCachingRequestWrapper) request); } else { // 通常我们会在过滤器中统一包装,这里只是演示在拦截器中处理 // 更佳实践是在一个Filter中提前包装Request ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request); // 注意:包装后需要“预读”一次才能将内容缓存起来 wrapper.getParameterMap(); // 触发缓存 // 此时可以读取缓存的body内容 byte[] content = wrapper.getContentAsByteArray(); if (content.length > 0) { String body = new String(content, wrapper.getCharacterEncoding()); log.info("Request Body: {}", body); } // 注意:这里无法替换Request对象,所以此方案在拦截器中不完美 } return true; } }重要提示:更标准、更可靠的做法是在一个过滤器(Filter)中,最早地对
HttpServletRequest进行包装,然后将包装后的对象传递下去。这样,无论是后续的过滤器、拦截器还是控制器,都能安全地多次读取Body。在拦截器中做这件事,时机可能有点晚,且替换Request对象比较麻烦。
5.3 异步请求(Async)与拦截器的特殊行为
当控制器方法返回DeferredResult、Callable或使用@ResponseBody配合异步Servlet时,请求处理是异步的。这会影响到拦截器方法的执行:
postHandle方法可能不执行或提前执行:对于异步请求,postHandle会在控制器方法返回后、异步任务开始前就被调用,此时异步任务的结果(如DeferredResult.setResult)还没有产生。因此,在postHandle中你拿不到最终的响应数据。afterCompletion的执行时机:afterCompletion会在异步请求完全结束后(即异步任务执行完毕,响应被发送)才被调用。所以它仍然是资源清理和最终日志记录的安全位置。
如何为异步请求定制逻辑?
Spring提供了AsyncHandlerInterceptor接口,它继承了HandlerInterceptor,并增加了一个方法:
afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler):该方法在异步处理开始时被调用(即控制器方法返回Callable或DeferredResult之后)。此时,postHandle和afterCompletion(属于当前线程)都不会被调用了。你可以在这里做一些针对异步请求的初始清理或标记工作。
如果你的拦截器需要感知异步处理,可以实现这个接口。但大多数情况下,我们只需要知道:对于异步请求,把关键的清理和日志记录逻辑放在afterCompletion中是安全的,而postHandle中的逻辑可能对异步请求无效。
5.4 拦截器中的异常处理
在拦截器的preHandle、postHandle、afterCompletion中都有可能抛出异常。
preHandle中抛出异常:该异常会向上传播,导致请求处理中断,后续的拦截器和控制器都不会执行。这个异常最终会被Spring MVC的异常解析器(HandlerExceptionResolver)处理,例如被@ControllerAdvice标注的全局异常处理器捕获。但是,已经成功执行了preHandle(返回true)的拦截器,其afterCompletion方法仍然会被调用,并且ex参数会携带这个异常对象。postHandle中抛出异常:同样会被异常解析器处理。但注意,如果postHandle抛出异常,后续拦截器的postHandle(如果存在)将不会执行,但所有拦截器的afterCompletion都会执行。afterCompletion中抛出异常:这是一个比较棘手的情况。因为此时响应可能已经提交给了客户端,再抛出异常很难处理,通常只会被记录到日志中,而无法改变HTTP响应。因此,afterCompletion中的代码必须尽可能健壮,避免抛出运行时异常。
最佳实践:在拦截器方法中,尤其是afterCompletion,使用try-catch块包裹可能出错的逻辑,并记录错误日志,而不是让异常抛出。
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { // 你的资源清理或日志记录逻辑 cleanUpResources(); } catch (Exception e) { log.error("拦截器afterCompletion方法执行清理时发生异常", e); // 不要再次抛出e } // 处理传入的ex(控制器抛出的异常) if (ex != null) { log.error("请求处理过程中发生异常", ex); } }5.5 拦截器性能优化考量
拦截器在每个匹配的请求上都会执行,因此其性能直接影响应用的整体吞吐量。
- 避免在拦截器中执行重型操作:如复杂的数据库查询、远程RPC调用等。拦截器的逻辑应尽可能轻量。例如,登录校验拦截器应该只做Session或Token的验证,而不应该每次请求都去数据库查询完整的用户信息。用户信息可以在登录时查询一次并缓存到Session或Token中。
- 合理使用排除路径:对于静态资源、健康检查端点(如
/actuator/health)、公开API等,务必在excludePathPatterns中排除,避免不必要的拦截开销。 - 注意ThreadLocal的清理:如上文日志拦截器示例,在
afterCompletion中务必调用ThreadLocal.remove(),防止内存泄漏。因为Tomcat等Web服务器使用线程池,线程会被复用,如果不清理,旧的数据会残留,导致数据错乱或内存无法释放。 - 缓存重复计算的结果:例如,在防刷拦截器中,解析客户端IP的
getClientIpAddress方法可能会被频繁调用。可以考虑将解析结果缓存在请求属性(request.setAttribute)中,供同一个请求的后续环节使用,避免重复解析HTTP头。
通过深入理解这些原理、细节和避坑指南,你就能游刃有余地运用SpringMVC拦截器,为你的Web应用构建起强大、灵活且高效的横切面功能层。记住,好的工具用在合适的地方,才能发挥最大价值。
