拦截器与 Filter
1. 概述
在 Spring MVC 中,拦截器(Interceptor) 和 过滤器(Filter) 都用于在请求处理流程中插入自定义逻辑,但二者在生命周期、作用范围和能力边界上有本质区别。本文从源码层面深入剖析 HandlerInterceptor 机制,对比 Filter 与 Interceptor 的差异,并提供 CORS 配置和 API 网关实战。
2. HandlerInterceptor 源码分析
2.1 接口定义
HandlerInterceptor 位于 org.springframework.web.servlet 包,包含三个默认方法(Java 8+):
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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,
ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
}
}三个方法说明:
| 方法 | 执行时机 | 返回值含义 | 常见用途 |
|---|---|---|---|
preHandle | 处理器执行之前 | true 继续执行链,false 中断请求 | 鉴权、限流、日志 |
postHandle | 处理器执行之后、视图渲染之前 | void | 修改 ModelAndView |
afterCompletion | 视图渲染完成之后 | void | 资源清理、耗时统计 |
2.2 执行时序
请求到达
│
▼
DispatcherServlet
│
├── 1. HandlerMapping 获取 HandlerExecutionChain
│
├── 2. 执行 Interceptor.preHandle() ──→ 任一返回 false 则中断
│
├── 3. HandlerAdapter 调用处理器方法
│
├── 4. 逆序执行 Interceptor.postHandle()
│
├── 5. 视图解析与渲染
│
└── 6. 逆序执行 Interceptor.afterCompletion()关键规则:
- preHandle 顺序执行:按注册顺序从前到后,任一返回
false则中断后续流程。 - postHandle 逆序执行:按注册顺序从后往前,仅 preHandle 返回 true 的拦截器会收到调用。
- afterCompletion 逆序执行:保证已成功执行 preHandle 的拦截器都能收到回调,适合资源清理。
2.3 HandlerExecutionChain 核心源码
Spring MVC 通过 HandlerExecutionChain 管理拦截器链调用:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response)
throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i; // 记录最后成功执行的索引
}
return true;
}
void applyPostHandle(HttpServletRequest request, HttpServletResponse response,
ModelAndView mv) throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
this.interceptorList.get(i).postHandle(request, response, this.handler, mv);
}
}
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
Exception ex) throws Exception {
for (int i = this.interceptorIndex; i >= 0; i--) {
this.interceptorList.get(i).afterCompletion(request, response, this.handler, ex);
}
}interceptorIndex 是关键设计:它记录了最后一个通过 preHandle 的拦截器索引。当请求完成或异常中断时,triggerAfterCompletion 只逆序遍历到该索引位置,确保未执行 preHandle 的拦截器不会收到 afterCompletion 回调。
2.4 preHandle 返回 false 的语义
- 后续拦截器不会执行。
- 目标 Controller 处理器不会被调用。
- 当前拦截器的
afterCompletion会被触发。 DispatcherServlet认为响应已由拦截器自行处理,不再视图渲染。
3. WebMvcConfigurer 注册拦截器
3.1 Java Config 方式
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingInterceptor())
.order(1)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
registry.addInterceptor(new AuthInterceptor())
.order(2)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/api/login");
}
}3.2 InterceptorRegistry 源码简析
public class InterceptorRegistry {
private final List<InterceptorRegistration> registrations = new ArrayList<>();
public InterceptorRegistration addInterceptor(HandlerInterceptor interceptor) {
InterceptorRegistration registration = new InterceptorRegistration(interceptor);
this.registrations.add(registration);
return registration;
}
}InterceptorRegistration 支持链式调用,路径模式采用 Ant 风格匹配(/api/**),底层使用 AntPathMatcher。排除模式优先级高于包含模式,即同时匹配时拦截器不生效。
// MappedInterceptor 运行时路径匹配逻辑
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (matches(request)) {
return this.interceptor.preHandle(request, response, handler);
}
return true; // 不匹配则跳过
}4. Filter 与 Interceptor 对比
4.1 生命周期与作用范围
| 维度 | Filter | Interceptor |
|---|---|---|
| 所属容器 | Servlet 容器(Tomcat/Jetty) | Spring 容器(IoC) |
| 规范来源 | Servlet 规范 | Spring MVC 框架 |
| 作用对象 | 所有 Web 资源(Servlet、JSP、静态资源) | 仅 Spring MVC 管理的 Controller |
| 访问上下文 | 仅 HttpServletRequest/Response | 可访问 Handler 对象和 ModelAndView |
| 依赖注入 | 需额外配置,不天然支持 | 天然支持 Spring DI |
| 粒度 | 粗粒度(URL 级别) | 细粒度(方法级别 + 参数级别) |
| 执行时机 | Servlet 容器层,早于 DispatcherServlet | Spring MVC 内部,在 HandlerMapping 之后 |
4.2 执行顺序
[客户端请求]
│
▼
┌── Servlet 容器 ─────────────────────────┐
│ │
│ 1. Filter.doFilter() 前置逻辑 │ ← Filter 链(顺序)
│ │ │
│ 2. DispatcherServlet 接收请求 │
│ │ │
│ 3. Interceptor.preHandle() │ ← Interceptor 链(顺序)
│ │ │
│ 4. Controller 处理器 │
│ │ │
│ 5. Interceptor.postHandle() │ ← Interceptor 链(逆序)
│ │ │
│ 6. 视图渲染 │
│ │ │
│ 7. Interceptor.afterCompletion() │ ← Interceptor 链(逆序)
│ │ │
│ 8. Filter.doFilter() 后置逻辑 │ ← Filter 链(逆序)
│ │ │
└──────────────────────────────────────────┘
│
▼
[客户端响应]4.3 精细时序图
时间轴 ─────────────────────────────────────────────────────▶
Filter-A Filter-B DispServlet Int-A Int-B Ctrl Int-B Int-A Filter-B Filter-A
│ │ │ │ │ │ │ │ │ │
│─前置────────▶│ │ │ │ │ │ │ │ │
│ │─前置─────────▶│ │ │ │ │ │ │ │
│ │ │─pre─────▶│ │ │ │ │ │ │
│ │ │ │─pre──▶│ │ │ │ │ │
│ │ │ │ │─ctrl─▶│ │ │ │ │
│ │ │ │ │ │◀post─│ │ │ │
│ │ │◀──after──────────│ │ │ │ │ │
│ │◀──后置────────│ │ │ │ │ │ │ │
│◀──后置───────│ │ │ │ │ │ │ │ │
◀─│ │ │ │ │ │ │ │ │ │4.4 注意事项
- Filter 不能使用 Handler 和 ModelAndView:Filter 在 Servlet 规范层面,完全不感知 Spring MVC 的 Handler 和 ModelAndView。
- Interceptor 不拦截非 DispatcherServlet 的静态资源:若静态资源由 Nginx 直接服务,拦截器不会生效。
- Filter 可以包装 Request/Response:通过
HttpServletRequestWrapper/HttpServletResponseWrapper实现请求体多次读取等能力,Interceptor 无法直接做到。
5. CORS 跨域配置
5.1 问题背景
同源策略(Same-Origin Policy)阻止跨域请求。CORS(Cross-Origin Resource Sharing)通过 HTTP 头告知浏览器允许跨域访问。
5.2 方式一:@CrossOrigin 注解
@RestController
@CrossOrigin(origins = "https://example.com", maxAge = 3600)
public class UserController {
@GetMapping("/api/user")
@CrossOrigin(origins = "https://trusted-site.com") // 方法级覆盖类级
public String getUser() {
return "{\"name\": \"张三\"}";
}
}注解属性说明:
| 属性 | 默认值 | 说明 |
|---|---|---|
origins | * | 允许的来源域名 |
methods | 简化方法(GET/POST/HEAD) | 允许的 HTTP 方法 |
allowedHeaders | * | 允许的请求头 |
allowCredentials | 未设置 | 是否允许携带凭据 |
maxAge | 1800 秒 | 预检请求缓存时间 |
5.3 方式二:全局 CorsFilter(推荐)
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}5.4 方式三:WebMvcConfigurer 配置 CORS
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}局限性:CorsRegistry 本质是为 HandlerMapping 添加 CorsProcessor,只处理 Spring MVC 映射的路径。若请求在 Filter 层被拦截并返回,CORS 头不会生效,仍需 CorsFilter。
5.5 执行顺序
请求 → CorsFilter(全局) → DispatcherServlet → HandlerMapping → @CrossOrigin(方法级)最佳实践:在网关层统一使用 CorsFilter 处理跨域,下游微服务内部无需重复配置,避免 Access-Control-Allow-Origin 重复头问题。
6. 实战:API 网关拦截器
本节实现一个综合性的 API 网关拦截器链,涵盖:登录鉴权 + 接口耗时统计 + 请求日志 + 防刷限流。
6.1 AuthInterceptor——登录鉴权
@Component
public class AuthInterceptor implements HandlerInterceptor {
private final SecretKey secretKey;
public AuthInterceptor(@Value("${jwt.secret}") String secret) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
ResponseUtils.writeJson(response, 401, "{\"code\":401,\"message\":\"缺少认证令牌\"}");
return false;
}
try {
String jwt = token.substring(7);
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey).build()
.parseClaimsJws(jwt).getBody();
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("username", claims.get("username"));
AuthenticationHolder.set(claims);
return true;
} catch (Exception e) {
ResponseUtils.writeJson(response, 401, "{\"code\":401,\"message\":\"令牌无效或已过期\"}");
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
AuthenticationHolder.clear(); // 清理线程上下文
}
}6.2 TimingInterceptor——接口耗时统计
@Slf4j
@Component
public class TimingInterceptor implements HandlerInterceptor {
private static final String START_TIME_ATTR = "_startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
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;
log.info("[耗时统计] {} {} → {} status={}, 耗时={}ms",
request.getMethod(), request.getRequestURI(), response.getStatus(), duration);
if (duration > 2000) {
log.warn("[慢接口告警] {} {} 耗时 {}ms", request.getMethod(), request.getRequestURI(), duration);
}
}
}
}6.3 LoggingInterceptor——请求日志
@Slf4j
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String REQUEST_ID_ATTR = "_requestId";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String requestId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
request.setAttribute(REQUEST_ID_ATTR, requestId);
Map<String, String> headers = new LinkedHashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, "authorization".equalsIgnoreCase(name) ? "Bearer ***" : request.getHeader(name));
}
log.info("\n[请求日志] requestId={}, method={}, uri={}, query={}, client={}, headers={}",
requestId, request.getMethod(), request.getRequestURI(),
request.getQueryString(), getClientIp(request),
OBJECT_MAPPER.writeValueAsString(headers));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
String requestId = (String) request.getAttribute(REQUEST_ID_ATTR);
log.info("[响应日志] requestId={}, status={}", requestId, response.getStatus());
if (ex != null) {
log.error("[请求异常] requestId={}, exception={}", requestId, ex.getMessage(), ex);
}
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip != null && ip.contains(",") ? ip.split(",")[0].trim() : ip;
}
}6.4 RateLimitInterceptor——防刷限流
基于令牌桶实现三级限流(全局 + 用户 + IP):
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimiter globalLimiter;
private final ConcurrentHashMap<String, RateLimiter> userLimiters = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, RateLimiter> ipLimiters = new ConcurrentHashMap<>();
private final double userPermitsPerSecond;
private final double ipPermitsPerSecond;
public RateLimitInterceptor(
@Value("${ratelimit.global.qps:100}") double globalQps,
@Value("${ratelimit.user.qps:10}") double userQps,
@Value("${ratelimit.ip.qps:20}") double ipQps) {
this.globalLimiter = RateLimiter.create(globalQps);
this.userPermitsPerSecond = userQps;
this.ipPermitsPerSecond = ipQps;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 全局限流
if (!globalLimiter.tryAcquire(0, TimeUnit.MILLISECONDS)) {
ResponseUtils.writeJson(response, 429, "{\"code\":429,\"message\":\"系统繁忙\"}");
return false;
}
// 用户级别限流
String userId = (String) request.getAttribute("userId");
if (userId != null) {
RateLimiter limiter = userLimiters.computeIfAbsent(userId, k -> RateLimiter.create(userPermitsPerSecond));
if (!limiter.tryAcquire(0, TimeUnit.MILLISECONDS)) {
ResponseUtils.writeJson(response, 429, "{\"code\":429,\"message\":\"请求过于频繁\"}");
return false;
}
}
// IP 级别限流
String ip = getClientIp(request);
RateLimiter ipLimiter = ipLimiters.computeIfAbsent(ip, k -> RateLimiter.create(ipPermitsPerSecond));
if (!ipLimiter.tryAcquire(0, TimeUnit.MILLISECONDS)) {
ResponseUtils.writeJson(response, 429, "{\"code\":429,\"message\":\"IP 被限流\"}");
return false;
}
return true;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
return ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)
? (ip.contains(",") ? ip.split(",")[0].trim() : ip)
: request.getRemoteAddr();
}
}6.5 ResponseUtils——响应工具类
public final class ResponseUtils {
private ResponseUtils() {}
public static void writeJson(HttpServletResponse response, int statusCode, String jsonBody) throws IOException {
response.setStatus(statusCode);
response.setContentType("application/json;charset=UTF-8");
response.getOutputStream().write(jsonBody.getBytes(StandardCharsets.UTF_8));
response.flushBuffer();
}
}6.6 AuthenticationHolder——用户上下文
public final class AuthenticationHolder {
private static final ThreadLocal<Claims> CONTEXT = new ThreadLocal<>();
private AuthenticationHolder() {}
public static void set(Claims claims) { CONTEXT.set(claims); }
public static Claims get() { return CONTEXT.get(); }
public static String getUserId() {
Claims claims = CONTEXT.get();
return claims != null ? claims.get("userId", String.class) : null;
}
public static void clear() { CONTEXT.remove(); }
}6.7 注册网关拦截器
@Configuration
public class GatewayInterceptorConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
private final TimingInterceptor timingInterceptor;
private final AuthInterceptor authInterceptor;
private final RateLimitInterceptor rateLimitInterceptor;
public GatewayInterceptorConfig(LoggingInterceptor loggingInterceptor,
TimingInterceptor timingInterceptor,
AuthInterceptor authInterceptor,
RateLimitInterceptor rateLimitInterceptor) {
this.loggingInterceptor = loggingInterceptor;
this.timingInterceptor = timingInterceptor;
this.authInterceptor = authInterceptor;
this.rateLimitInterceptor = rateLimitInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 第1层:请求日志
registry.addInterceptor(loggingInterceptor).order(1).addPathPatterns("/api/**");
// 第2层:耗时统计
registry.addInterceptor(timingInterceptor).order(2).addPathPatterns("/api/**");
// 第3层:限流(在鉴权之前拦截恶意流量)
registry.addInterceptor(rateLimitInterceptor).order(3).addPathPatterns("/api/**");
// 第4层:鉴权
registry.addInterceptor(authInterceptor).order(4)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**", "/api/auth/login");
}
}6.8 执行流程图
请求 → /api/user/profile
│
├── 1. LoggingInterceptor.preHandle() → 打印请求日志,生成 requestId
│
├── 2. TimingInterceptor.preHandle() → 记录 startTime
│
├── 3. RateLimitInterceptor.preHandle() → 三级限流检查
│
├── 4. AuthInterceptor.preHandle() → 解析 JWT Token
│
├── 5. UserController.getProfile() → 执行业务逻辑
│
├── 6. 各拦截器 postHandle()(逆序,本示例均未实现)
│
├── 7. 视图渲染
│
├── 8. AuthInterceptor.afterCompletion() → 清除 AuthenticationHolder
│
├── 9. RateLimitInterceptor.afterCompletion() →(未实现)
│
├── 10. TimingInterceptor.afterCompletion() → 计算并打印耗时
│
└── 11. LoggingInterceptor.afterCompletion() → 打印响应日志7. Filter 注册方式补充
7.1 @WebFilter + @ServletComponentScan
@WebFilter(urlPatterns = "/*")
public class CharacterEncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}启动类加 @ServletComponentScan:
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}7.2 FilterRegistrationBean(推荐)
通过 FilterRegistrationBean 注册可以灵活指定顺序:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistration(CorsFilter corsFilter) {
FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(corsFilter);
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
@Bean
public FilterRegistrationBean<CharacterEncodingFilter> encodingFilter() {
FilterRegistrationBean<CharacterEncodingFilter> registration =
new FilterRegistrationBean<>();
registration.setFilter(new CharacterEncodingFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registration;
}
}8. 总结与选型建议
| 场景 | 推荐方案 |
|---|---|
| 修改请求/响应编码 | Filter |
| 跨域处理 | CorsFilter(全局)或 @CrossOrigin(局部) |
| 请求日志记录 | Interceptor(可访问 Handler)或 Filter(更底层) |
| 性能监控(接口耗时) | Interceptor(afterCompletion 天然适合) |
| 权限校验 | Interceptor(可结合 HandlerMethod 信息) |
| 限流防刷 | Interceptor(可结合用户上下文精细化限流) |
| XSS/CSRF 防护 | Filter(在到达 Controller 之前做清理) |
| 包装 Request/Response | Filter(通过 HttpServletRequestWrapper) |
核心原则:Filter 解决 Servlet 容器层面的横切问题,Interceptor 解决 Spring MVC 处理器层面的横切问题。能使用 Interceptor 解决的问题优先在拦截器层解决,因其拥有 Spring 容器注入能力和更丰富的上下文信息。