5,拦截器
对于拦截器这节的知识,我们需要学习如下内容:
-
拦截器概念
-
入门案例
-
拦截器参数
-
拦截器工作流程分析
5.1 拦截器概念
讲解拦截器的概念之前,我们先看一张图:
(1)浏览器发送一个请求会先到Tomcat的web服务器
(2)Tomcat服务器接收到请求以后,会去判断请求的是静态资源还是动态资源
(3)如果是静态资源,会直接到Tomcat的项目部署目录下去直接访问
(4)如果是动态资源,就需要交给项目的后台代码进行处理
(5)在找到具体的方法之前,我们可以去配置过滤器(可以配置多个),按照顺序进行执行
(6)然后进入到到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
(7)如果满足规则,则进行处理,找到其对应的controller类中的方法进行执行,完成后返回结果
(8)如果不满足规则,则不进行处理
(9)这个时候,如果我们需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
这个就是拦截器要做的事。
-
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
-
作用:
-
在指定的方法调用前后执行预先设定的代码
-
阻止原始方法的执行
-
总结:拦截器就是用来做增强
-
看完以后,大家会发现
-
拦截器和过滤器在作用和执行顺序上也很相似
所以这个时候,就有一个问题需要思考:拦截器和过滤器之间的区别是什么?
-
归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
-
拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强
🧠 理论理解
拦截器是 SpringMVC 的核心扩展点,允许在请求到达 Controller 前后、处理完成后,插入横切逻辑(cross-cutting concern)。它基于 Java 的回调机制和责任链模式,实现按顺序“拦截”请求,决定是否放行、是否追加行为。
与过滤器(Filter)相比,拦截器更专注于 SpringMVC 内部执行链,只作用于 Spring 管理的请求,更易集成 Spring 上下文、获取 Controller 方法等细节。
🏢 企业实战理解
在大厂,拦截器是后台系统、微服务架构的标配组件。
-
字节跳动后台项目:常用来做用户身份验证(如登录态、Token 校验)、埋点日志、A/B 实验分流。
-
Google Cloud Console:利用拦截器动态注入 trace-id,实现分布式链路追踪。
-
阿里云 API 网关:拦截器配合网关实现动态路由、限流、降级、黑白名单。
这些场景都需要精细化控制请求链,而不仅是 Servlet 层粗粒度过滤。
面试题 1
拦截器和过滤器有什么区别?你在实际项目中怎么选择?
✅ 答案:
-
拦截器(Interceptor):SpringMVC 层提供,拦截的是进入 DispatcherServlet 的请求,能拿到 Spring 上下文、Controller 方法、注解信息。
-
过滤器(Filter):Servlet 规范提供,拦截所有请求(包括静态资源、非 Spring 管理的请求),独立于 Spring 存在。
选择:
-
与 SpringMVC 紧密相关的(如登录校验、权限检查、业务拦截)用拦截器。
-
与整个 Web 容器相关的(如编码、跨域、全局防御、日志埋点、请求修饰)用过滤器。
在字节、阿里项目里,两者通常配合使用,过滤器在外层做全局策略,拦截器在内层做业务策略。
场景题 1
字节跳动后台系统有多级拦截器链:登录校验拦截器、权限拦截器、埋点日志拦截器。你发现线上日志显示部分请求没有经过埋点拦截器,只有权限校验日志,为什么?你怎么排查?
✅ 答案:
首先分析:拦截器是责任链模型,任何一个 preHandle
返回 false
,后续链条(包括其他拦截器、Controller 方法)都不会执行。
排查步骤:
1️⃣ 检查日志发现权限拦截器 preHandle
在某些请求上返回了 false
(比如因为用户无权限)。
2️⃣ 因为权限拦截器排在埋点拦截器之前,直接中断了链条。
解决方案:
-
调整拦截器顺序,让埋点拦截器最先执行(记录全量请求)。
-
即使
preHandle
返回false
,在afterCompletion
中补充日志埋点(确保不中断链路追踪)。
字节内部在埋点系统中,通常会优先挂载 trace 拦截器,确保所有请求都能生成 trace-id。
场景题 2
你在阿里云工作,某个团队在拦截器 preHandle
里做了数据库查询,线上突发流量导致数据库压力飙升。请问为什么设计有问题?怎么优化?
✅ 答案:
问题分析:拦截器 preHandle
属于请求同步链路,是高频执行点。做重逻辑(特别是数据库 IO、远程调用)会直接拖慢 QPS。
优化方案:
-
尽量把数据库查询提前放到网关层或缓存层(比如 Redis 缓存权限信息)。
-
必须查数据库的场景,用批量预加载、延迟加载、异步方式。
-
引入分布式限流、熔断机制,避免请求洪峰打爆核心链路。
阿里云内部的微服务平台通常用 Sentinel、Hystrix 做保护,拦截器里只走缓存或上下文数据。
5.2 拦截器入门案例
5.2.1 环境准备
-
创建一个Web的Maven项目
-
pom.xml添加SSM整合所需jar包
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itheima</groupId> <artifactId>springmvc_12_interceptor</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> <configuration> <port>80</port> <path>/</path> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>8</source> <target>8</target> </configuration> </plugin> </plugins> </build> </project>
-
创建对应的配置类
public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } //乱码处理 @Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); return new Filter[]{filter}; } } @Configuration @ComponentScan({"com.itheima.controller"}) @EnableWebMvc public class SpringMvcConfig{ }
-
创建模型类Book
public class Book { private String name; private double price; public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } @Override public String toString() { return "Book{" + "书名='" + name + '\'' + ", 价格=" + price + '}'; } }
-
编写Controller
@RestController @RequestMapping("/books") public class BookController { @PostMapping public String save(@RequestBody Book book){ System.out.println("book save..." + book); return "{'module':'book save'}"; } @DeleteMapping("/{id}") public String delete(@PathVariable Integer id){ System.out.println("book delete..." + id); return "{'module':'book delete'}"; } @PutMapping public String update(@RequestBody Book book){ System.out.println("book update..."+book); return "{'module':'book update'}"; } @GetMapping("/{id}") public String getById(@PathVariable Integer id){ System.out.println("book getById..."+id); return "{'module':'book getById'}"; } @GetMapping public String getAll(){ System.out.println("book getAll..."); return "{'module':'book getAll'}"; } }
最终创建好的项目结构如下:
面试题 2
SpringMVC 拦截器的三个方法(preHandle、postHandle、afterCompletion)有什么区别?分别适合做哪些事情?
✅ 答案:
-
preHandle
:Controller 方法执行前调用,决定请求是否放行。适合做:登录校验、权限校验、参数校验、流量控制。 -
postHandle
:Controller 方法执行后调用(但视图渲染前)。适合做:修改视图模型、追加响应数据(主要对视图有效,对纯 JSON 意义不大)。 -
afterCompletion
:请求完全结束后调用(无论是否异常)。适合做:资源清理、异常日志记录、性能统计。
在大厂实践中,preHandle
是最常用的钩子,afterCompletion
用于保证无论成功失败都能收集指标或输出审计日志。
场景题 3
Google Cloud 后台系统支持多租户,每个租户有不同的路由和访问权限。你如何用拦截器实现多租户上下文注入?
✅ 答案:
设计方案:
-
在
preHandle
中解析请求头或 Token,提取租户 ID(tenantId)。 -
将
tenantId
写入线程上下文(如 ThreadLocal、RequestContext)。 -
后续业务层、数据库层(如 MyBatis、多租户数据源)可以直接读取当前上下文。
注意事项: -
确保
afterCompletion
中清理 ThreadLocal,防止线程池复用带来的脏数据。 -
对跨服务请求,租户上下文需透传 trace(结合分布式 tracing)。
Google Cloud Platform 内部有专门的 Context 管理模块和拦截器挂钩,确保租户、请求上下文贯穿调用链。
5.2.2 拦截器开发
步骤1:创建拦截器类
让类实现HandlerInterceptor接口,重写接口中的三个方法。
@Component
//定义拦截器类,实现HandlerInterceptor接口
//注意当前类必须受Spring容器控制
public class ProjectInterceptor implements HandlerInterceptor {
@Override
//原始方法调用前执行的内容
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}
@Override
//原始方法调用后执行的内容
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
@Override
//原始方法调用完成后执行的内容
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
注意:拦截器类要被SpringMVC容器扫描到。
步骤2:配置拦截器类
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
//配置拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/books" );
}
}
步骤3:SpringMVC添加SpringMvcSupport包扫描
@Configuration
@ComponentScan({"com.itheima.controller","com.itheima.config"})
@EnableWebMvc
public class SpringMvcConfig{
}
步骤4:运行程序测试
使用PostMan发送http://localhost/books
如果发送http://localhost/books/100
会发现拦截器没有被执行,原因是拦截器的addPathPatterns
方法配置的拦截路径是/books
,我们现在发送的是/books/100
,所以没有匹配上,因此没有拦截,拦截器就不会执行。
步骤5:修改拦截器拦截规则
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
//配置拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*" );
}
}
这个时候,如果再次访问http://localhost/books/100
,拦截器就会被执行。
最后说一件事,就是拦截器中的preHandler
方法,如果返回true,则代表放行,会执行原始Controller类中要请求的方法,如果返回false,则代表拦截,后面的就不会再执行了。
步骤6:简化SpringMvcSupport的编写
@Configuration
@ComponentScan({"com.itheima.controller"})
@EnableWebMvc
//实现WebMvcConfigurer接口可以简化开发,但具有一定的侵入性
public class SpringMvcConfig implements WebMvcConfigurer {
@Autowired
private ProjectInterceptor projectInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置多拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");
}
}
此后咱们就不用再写SpringMvcSupport
类了。
最后我们来看下拦截器的执行流程:
当有拦截器后,请求会先进入preHandle方法,
如果方法返回true,则放行继续执行后面的handle[controller的方法]和后面的方法
如果返回false,则直接跳过后面方法的执行。
🧠 理论理解
拦截器通过实现 HandlerInterceptor
接口(或使用 HandlerInterceptorAdapter
)定义三类方法:
-
preHandle
:控制是否放行。 -
postHandle
:业务执行后,调整响应数据。 -
afterCompletion
:请求完成后收尾工作。
配置方式有两种:通过WebMvcConfigurer
或WebMvcConfigurationSupport
,前者更灵活。
🏢 企业实战理解
在大厂项目中,拦截器不仅是开发用的,还会被运维和平台层“挂钩”。
-
字节的 RPC 框架有统一链路拦截器,用于埋点分析、性能监控。
-
阿里云的多租户系统,用拦截器为每个租户动态注入上下文(如租户 ID、权限信息)。
-
Google、OpenAI 的 API 系统,用拦截器统一实现跨 API 的安全验证、流控、熔断。
这些功能常常比单个业务模块更关键,影响整体系统的可观测性和稳定性。
面试题 3
如果系统配置了多个拦截器,它们的执行顺序是怎样的?能举一个实际应用场景吗?
✅ 答案:
-
preHandle
按配置顺序执行(先进先出)。 -
postHandle
和afterCompletion
按配置顺序逆序执行(后进先出)。
实际场景:
-
拦截器 1:Trace 拦截器(埋点、日志、链路追踪)。
-
拦截器 2:权限拦截器(登录、角色、菜单权限)。
-
拦截器 3:接口幂等性拦截器(防重复提交)。
这三者需要按顺序串联起来保证链路上下文、业务校验和接口安全。阿里、字节的后台系统中,这种多层拦截是常规配置。
场景题 4
你在 OpenAI 团队开发 API 平台,客户要求每次调用 API 后要有操作审计记录,包括:谁调用、调用了什么、成功还是失败。你怎么用拦截器实现?
✅ 答案:
方案设计:
-
在
preHandle
中记录:调用方身份(从 Header、Token 中解析)、请求 URL、入参摘要。 -
在
postHandle
或afterCompletion
中记录:请求结果、状态码、异常信息(如果有)。 -
将审计数据异步写入日志系统或审计数据库(避免阻塞主线程)。
OpenAI 这类高 QPS 系统会用专门的异步消息队列(如 Kafka)接收审计日志,后端用批处理或流式处理系统(如 Flink)分析审计数据。
5.3 拦截器参数
5.3.1 前置处理方法
原始方法之前运行preHandle
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("preHandle");
return true;
}
-
request:请求对象
-
response:响应对象
-
handler:被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装
使用request对象可以获取请求数据中的内容,如获取请求头的Content-Type
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String contentType = request.getHeader("Content-Type");
System.out.println("preHandle..."+contentType);
return true;
}
使用handler参数,可以获取方法的相关信息
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod hm = (HandlerMethod)handler;
String methodName = hm.getMethod().getName();//可以获取方法的名称
System.out.println("preHandle..."+methodName);
return true;
}
5.3.2 后置处理方法
原始方法运行后运行,如果原始方法被拦截,则不执行
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("postHandle");
}
前三个参数和上面的是一致的。
modelAndView:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
因为咱们现在都是返回json数据,所以该参数的使用率不高。
5.3.3 完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("afterCompletion");
}
前三个参数与上面的是一致的。
ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
因为我们现在已经有全局异常处理器类,所以该参数的使用率也不高。
这三个方法中,最常用的是==preHandle==,在这个方法中可以通过返回值来决定是否要进行放行,我们可以把业务逻辑放在该方法中,如果满足业务则返回true放行,不满足则返回false拦截。
🧠 理论理解
-
preHandle
:拿到HttpServletRequest
、HttpServletResponse
,可以提前解析请求头、参数、URL。 -
handler
:通常是HandlerMethod
,能解析出目标方法、注解、类信息。 -
postHandle
:能读取ModelAndView
,调整返回的视图或数据(对 JSON 场景意义不大)。 -
afterCompletion
:即使出错也执行,用于收尾、清理、记录异常。
🏢 企业实战理解
在大厂场景中:
-
字节跳动会在
preHandle
中提取 trace-id,贯穿到所有日志、埋点。 -
Google 的 API 系统,在
preHandle
中做请求签名校验,防止重放攻击。 -
NVIDIA 的 GPU 云平台,用
afterCompletion
写入审计日志,保证操作记录完整性。
这些参数不仅是技术细节,更是支撑系统运维、数据安全、性能优化的基石。
面试题 4
在高并发场景下,拦截器设计要注意哪些问题?
✅ 答案:
-
不要在拦截器中存储请求相关状态到全局变量(因为拦截器是单例)。
-
避免在拦截器中执行重逻辑、重 IO 操作(如写 DB、远程调用),应尽量异步或用消息队列。
-
保证幂等性,防止多线程场景下拦截器引入脏数据。
-
可结合分布式锁、限流、断路器(如 Sentinel、Hystrix)保护核心服务。
字节、阿里等大厂的微服务通常用网关层的拦截器实现限流、熔断,单体项目则结合本地缓存、异步化等优化性能。
场景题 5
你在腾讯负责后台系统,安全部门要求所有 POST、PUT、DELETE 接口都必须有 CSRF 防御机制。你如何基于 SpringMVC 拦截器实现?
✅ 答案:
实现步骤:
1️⃣ 在 preHandle
中判断请求方法是否为 POST、PUT、DELETE。
2️⃣ 检查请求头或请求参数中是否携带合法的 CSRF Token。
3️⃣ 如果没有或不合法,直接返回错误响应(HTTP 403 Forbidden)。
4️⃣ 如果合法,放行进入后续业务逻辑。
注意:这种安全拦截器通常在网关或统一安全模块里统一配置,避免每个微服务单独实现。
5.4 拦截器链配置
目前,我们在项目中只添加了一个拦截器,如果有多个,该如何配置?配置多个后,执行顺序是什么?
5.4.1 配置多个拦截器
步骤1:创建拦截器类
实现接口,并重写接口中的方法
@Component
public class ProjectInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...222");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...222");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...222");
}
}
步骤2:配置拦截器类
@Configuration
@ComponentScan({"com.itheima.controller"})
@EnableWebMvc
//实现WebMvcConfigurer接口可以简化开发,但具有一定的侵入性
public class SpringMvcConfig implements WebMvcConfigurer {
@Autowired
private ProjectInterceptor projectInterceptor;
@Autowired
private ProjectInterceptor2 projectInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置多拦截器
registry.addInterceptor(projectInterceptor).addPathPatterns("/books","/books/*");
registry.addInterceptor(projectInterceptor2).addPathPatterns("/books","/books/*");
}
}
步骤3:运行程序,观察顺序
拦截器执行的顺序是和配置顺序有关。就和前面所提到的运维人员进入机房的案例,先进后出。
-
当配置多个拦截器时,形成拦截器链
-
拦截器链的运行顺序参照拦截器添加顺序为准
-
当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
-
当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle:与配置顺序相同,必定运行
postHandle:与配置顺序相反,可能不运行
afterCompletion:与配置顺序相反,可能不运行。
这个顺序不太好记,最终只需要把握住一个原则即可:==以最终的运行结果为准==
🧠 理论理解
当系统中配置多个拦截器,它们按注册顺序形成链式结构:
-
preHandle
按顺序执行(先进先出)。 -
postHandle
、afterCompletion
按逆序执行(后进先出)。
这本质是责任链模式,允许多个拦截器独立负责自己的逻辑,但要注意链中任意一个返回false
,后续链条都不再继续。
🏢 企业实战理解
在大厂中,拦截器链常被用来分层:
-
第一级:全局级,比如 trace、日志、异常处理。
-
第二级:业务模块级,比如权限、流控、灰度策略。
-
第三级:接口专属,比如参数转换、动态注入。
字节、阿里、Google 都有专门的“拦截器平台”或“中间件”,帮助统一配置和管理这些链路,甚至支持动态启停、在线调优,保证系统稳定可靠。
面试题 5
拦截器中获取到 HandlerMethod 后,能做什么高级用法?
✅ 答案:
通过 HandlerMethod 可以:
-
获取 Controller 类、方法、方法参数。
-
读取方法或类上的注解,比如自定义
@RequiresPermission
、@AuditLog
。 -
动态决定是否拦截或放行(基于注解、反射信息)。
企业实践中,很多基于注解的权限拦截、埋点统计、灰度策略、A/B 测试分流,都是通过 HandlerMethod 动态解析注解和方法元信息实现的。
面试题 6
你如何设计一个全局异常处理 + 拦截器配合的方案,保证系统健壮性?
✅ 答案:
-
拦截器层:只做请求前后的预处理、埋点、数据记录,不处理异常。
-
全局异常处理器(如
@ControllerAdvice
):捕获所有未被处理的异常,返回统一错误响应,记录日志、报警。 -
afterCompletion
:用于记录请求级别的异常、耗时、请求上下文信息,补充全局日志链。
在阿里、字节、Google 这些大厂,通常采用分层异常处理架构,拦截器专注埋点,核心异常处理由框架统一接管,并与监控系统(如 Sentry、Prometheus)联动。