一、Spring AOP 原理详解
Spring AOP 是 Java 后端面试中出现频率最高、也最容易“翻车”的知识点之一。很多候选人都能背出“动态代理”“切面”“通知”等关键词,但一旦被追问到底层实现、执行顺序、失效场景和源码细节,往往就答不下去了。
本文先把 AOP 的来龙去脉、核心概念、两种动态代理机制、通知执行顺序和源码主线完整梳理一遍,再把高频面试题逐条拆解。
1.1 为什么要学 AOP:从 OOP 到 AOP 的演进
面向对象编程(OOP)为我们提供了很好的代码组织方式:把业务数据和操作封装成类,通过继承、多态等机制复用代码。但 OOP 在解决一类问题时显得力不从心,那就是横切关注点(Cross-cutting Concern)。
横切关注点指的是那些分布在多个模块、多个层级中的通用逻辑,例如:
日志记录
事务管理
权限校验与安全控制
性能监控与埋点
缓存处理
异常统一处理
以日志为例,如果不用 AOP,我们可能会在每个业务方法里都写上一句打印日志的代码:
java
public class UserService { public void save(User user) { System.out.println("开始保存用户: " + user.getName()); // 真正的业务逻辑 userMapper.insert(user); System.out.println("保存用户成功"); } public void update(User user) { System.out.println("开始更新用户: " + user.getName()); // 真正的业务逻辑 userMapper.update(user); System.out.println("更新用户成功"); } }这段代码的问题非常明显:
代码重复:同样的日志逻辑散落在几十上百个方法里,一旦要修改日志格式,就要改遍所有地方。
业务逻辑被污染:save、update 这些方法的核心职责是业务操作,却被大量与业务无关的代码包围,阅读和维护成本很高。
难以统一管理:事务、权限这些逻辑如果靠手动在每个方法里调用,很容易漏写、错写。
AOP(Aspect-Oriented Programming,面向切面编程)的核心思想,就是把这些横切逻辑从业务代码中抽离出来,定义成独立的“切面”,再通过某种机制在运行时或编译期把切面逻辑织入到目标方法的特定位置。最终的效果是:业务代码里只有业务逻辑,横切逻辑集中管理、统一维护,业务方法执行时横切逻辑自动生效。
简单说,AOP 不是 OOP 的对立面,而是对 OOP 的补充。OOP 解决的是纵向的“模块化”,AOP 解决的是横向的“关注点分离”。
1.2 AOP 核心概念:一张图 + 一张表讲清楚
AOP 有一套自己的术语体系,很多面试题都直接拿这些概念来问辨析。下面先用一个通俗的类比说明,再用表格整理标准定义。
假设我们要给一个房屋装修,目标对象就是“这间房子”,横切逻辑就是“在房间的某个位置打孔、装插座、装灯”。AOP 做的事情相当于:不直接改动房间本身的墙体结构,而是制定一个“施工方案”,在明确的位置统一施工。
| 术语 | 英文 | 通俗理解 | 标准定义 |
|---|---|---|---|
| 连接点 | JoinPoint | 可以施工的所有位置 | 程序执行过程中能够插入切面的点,如方法调用、方法执行、字段访问、异常抛出等 |
| 切点 | Pointcut | 我们选中要施工的具体位置 | 匹配连接点的表达式,定义通知应该应用到哪些连接点上 |
| 通知 | Advice | 具体要执行的施工动作 | 在切点处执行的横切逻辑代码,分为前置、后置、环绕、异常、最终五类 |
| 切面 | Aspect | 施工方案(位置 + 动作) | 切点与通知的组合,是对横切逻辑的模块化封装 |
| 织入 | Weaving | 把方案落实到房子的过程 | 将切面应用到目标对象并创建代理对象的过程 |
| 目标对象 | Target | 被装修的房子 | 被代理的原始业务对象 |
| 代理对象 | Proxy | 装修后交付给客户的对象 | 织入切面后生成的新对象,客户端实际持有和使用的是它 |
其中需要特别区分三组概念:
连接点和切点:连接点是“理论上的所有可能位置”,切点是“被表达式筛出来的实际拦截位置”。切点是连接点的子集。Spring AOP 只支持方法级别的连接点,即连接点都是“方法执行”。
通知和切面:通知只是“一段逻辑”,切面是“通知”加上“切点”的完整模块。一个切面类中可以包含多个通知方法。
目标对象和代理对象:目标对象是原始对象,代理对象是包装后的对象。Spring 容器中注入给使用方的通常是代理对象,所以代码里 this 调用的坑才会出现(见 1.7 节)。
1.3 Spring AOP 与 AspectJ:两个常被混为一谈的概念
面试中经常出现“Spring AOP 和 AspectJ 有什么区别”这个问题。要答好,首先要明确一点:Spring AOP 和 AspectJ 是两个不同层面、不同实现机制的 AOP 方案,Spring 对 AspectJ 的注解做了“借壳”,底层并没有完整使用 AspectJ 的织入机制。
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时(容器初始化 bean 时创建代理) | 编译期、编译后、类加载期(通过专用编译器或 agent 完成) |
| 实现方式 | 动态代理(JDK Proxy / CGLIB) | 静态织入,直接修改字节码 |
| 是否需要独立编译器 | 不需要 | 需要 ajc 编译器或 load-time weaving |
| 连接点类型 | 仅支持方法执行 | 支持方法调用、字段访问、构造器、异常处理等更丰富的连接点 |
| 性能 | 略低(运行时生成代理类) | 较高(织入发生在编译期,运行期无额外代理开销) |
| 耦合度 | 依赖 Spring 容器管理 bean | 可独立于 Spring 使用 |
为什么我们在 Spring 项目里写的 @Aspect、@Before、@Around 注解长得和 AspectJ 一样,却被叫成“Spring AOP”?这是因为 Spring 借鉴了 AspectJ 的注解风格(annotation style),在 IoC 容器初始化后,通过AspectJAutoProxyCreator读取这些注解并转换为 Spring 自己的 Advisor,再用动态代理生成代理对象。也就是说:语法是 AspectJ 的,执行是 Spring 自己的动态代理。
如果确实需要切 private 方法、字段,或者在非 Spring 管理的对象上做 AOP,才需要考虑完整引入 AspectJ。
1.4 Spring AOP 的底层实现原理:动态代理
Spring AOP 的底层本质是动态代理:在运行时为目标对象生成一个代理对象,当客户端调用目标方法时,请求先进入代理对象的拦截逻辑,再执行横切通知,最后才调用真正的目标方法。Spring 提供了两种动态代理方式:JDK 动态代理和 CGLIB 动态代理。
1.4.1 JDK 动态代理:基于接口
JDK 动态代理是 Java 原生的代理机制,核心类是java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。它的工作机制是:
目标对象必须实现了至少一个接口。
运行时动态生成一个实现了同样接口的代理类。
所有代理类方法的调用,都会被转发到 InvocationHandler 的 invoke 方法中,由 invoke 方法调用 Method.invoke 去执行真正的目标方法。
看一个精简示例:
java
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkProxyDemo { interface UserService { void save(String name); } static class UserServiceImpl implements UserService { public void save(String name) { System.out.println("业务方法执行,保存用户: " + name); } } public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before: 开启事务"); Object result = method.invoke(target, args); System.out.println("after: 提交事务"); return result; } }); proxy.save("张三"); } }运行结果:
text
before: 开启事务 业务方法执行,保存用户: 张三 after: 提交事务
从这个例子可以提炼出几个关键点:
代理对象类型由 JVM 运行时生成,类名通常形如
com.sun.proxy.$Proxy0。InvocationHandler 是横切逻辑的入口,invoke 方法中可以在目标方法前后插入任意增强逻辑。
在 invoke 里必须用 target(目标对象)调用
method.invoke,如果写成method.invoke(proxy, args),会造成无限递归。
JDK 动态代理的优点是 Java 原生支持、不引入第三方依赖;缺点是目标类必须实现接口,否则无法代理。
1.4.2 CGLIB 动态代理:基于继承
CGLIB(Code Generation Library)通过底层的 ASM 字节码技术,在运行时动态生成一个继承目标类的子类,并在子类中覆写目标方法,在覆写的方法里插入横切逻辑。核心类是Enhancer和MethodInterceptor。
java
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibDemo { static class UserService { public void save(String name) { System.out.println("业务方法执行,保存用户: " + name); } } public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { System.out.println("before: 开启事务"); Object result = methodProxy.invokeSuper(obj, args); System.out.println("after: 提交事务"); return result; } }); UserService proxy = (UserService) enhancer.create(); proxy.save("李四"); } }结果与 JDK 动态代理一致。注意 CGLIB 的几个特点:
生成的代理类是目标类的子类,因此目标类不能是 final 类,被代理的方法不能是 final 或 static。
调用父类方法应该使用
methodProxy.invokeSuper(obj, args),而不是method.invoke,避免再次走代理逻辑导致递归。Spring 从 5.x 开始内置了自己的 CGLIB 副本(
org.springframework.cglib),不再直接依赖外部 cglib 库。
1.4.3 代理选择策略:Spring 到底用哪个?
Spring AOP 在选择代理方式时遵循一条默认规则:
如果目标对象实现了接口,并且没有强制指定
proxyTargetClass,默认使用 JDK 动态代理。如果目标对象没有实现接口,或者显式配置
proxyTargetClass=true,则使用CGLIB 代理。
在 Spring Boot 中,情况略有变化。Spring Boot 2.x 默认把spring.aop.proxy-target-class设置为 true,也就是无论是否实现接口,都优先使用 CGLIB。原因主要是:
行为一致性:避免同一项目里有的 bean 走 JDK 代理、有的走 CGLIB,导致通过接口和通过类注入表现出不同行为。
避免了 JDK 代理 bean 无法通过具体类类型注入的问题。
CGLIB 在多数场景下性能已足够好,且能代理没有接口的类。
在基于注解的配置中,可以通过@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用 CGLIB;在 XML 配置中使用<aop:aspectj-autoproxy proxy-target-class="true" />。
1.5 Spring AOP 的实现方式与通知类型
在项目中使用 Spring AOP 主要有三种方式:
基于 @AspectJ 注解:最常用。用 @Aspect 声明切面类,用 @Pointcut 定义切点,用 @Before、@Around 等注解定义通知。
基于 XML 配置:在老项目中常见,用
<aop:config>、<aop:aspect>、<aop:pointcut>、<aop:before>等标签声明切面。基于编程式 API:直接使用 ProxyFactory、AspectJProxyFactory 等 API 手动创建代理,框架内部用得较多。
通知类型共五种:
| 通知类型 | 注解 | 执行时机 | 说明 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 无法阻止目标方法执行,除非自己抛异常 |
| 环绕通知 | @Around | 目标方法前后 | 最强大,可以控制目标方法是否执行、修改参数和返回值,必须调用 proceed() |
| 返回通知 | @AfterReturning | 目标方法正常返回后 | 只有方法正常结束(未抛异常)才会执行,可拿到返回值 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 只有方法抛异常才会执行,可拿到异常对象 |
| 最终通知 | @After | 目标方法结束后 | 无论正常返回还是抛异常都会执行,类似 finally |
一个标准的切面类示例:
java
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class LogAspect { // 定义切点:com.example.service 包下所有类的所有方法 @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() { } @Before("serviceLayer()") public void before(JoinPoint joinPoint) { System.out.println("前置通知: " + joinPoint.getSignature().getName()); } @Around("serviceLayer()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("环绕通知-前"); long start = System.currentTimeMillis(); Object result = pjp.proceed(); System.out.println("环绕通知-后,耗时: " + (System.currentTimeMillis() - start) + "ms"); return result; } @AfterReturning(pointcut = "serviceLayer()", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { System.out.println("返回通知: " + joinPoint.getSignature().getName() + ",返回值=" + result); } @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex") public void afterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println("异常通知: " + joinPoint.getSignature().getName() + ",异常=" + ex.getMessage()); } @After("serviceLayer()") public void after(JoinPoint joinPoint) { System.out.println("最终通知: " + joinPoint.getSignature().getName()); } }这个切面演示了五类通知在同一类中的写法。实际开发中使用最多的是环绕通知,因为事务管理、耗时统计、缓存等场景都需要控制目标方法是否执行以及如何处理执行结果。
要写好切面,切点表达式是必须掌握的基础,其中最常用的execution语法为:
text
execution(修饰符? 返回类型 类路径.方法名(参数类型) throws 异常类型?)
前面的修饰符和后面的 throws 都可以省略。例如execution(* com.example.service.*.*(..))表示:任意返回值、service 包下任意类、任意方法、任意参数的连接点。星号是通配符,两个点..表示任意参数,控制好这个语法,定位切点就非常灵活。
1.6 通知执行顺序:面试必画的执行链
当多个通知同时作用在一个目标方法上时,执行顺序是极高的高频考点。先把同一切面中五类通知的先后关系记住,再理解多个切面的叠加顺序。
同一切面围绕目标方法的执行顺序,可以画成下面这个嵌套结构:
text
@Around 前半部分 @Before 目标方法 @AfterReturning(正常返回) @After(无论成功失败都执行) @Around 后半部分
也就是说,正常执行时顺序是:
text
@Around 前 → @Before → 目标方法 → @AfterReturning → @After → @Around 后
如果目标方法抛出异常,@AfterReturning不会执行,换成@AfterThrowing执行,顺序变为:
text
@Around 前 → @Before → 目标方法抛异常 → @AfterThrowing → @After → @Around 后
这里要特别注意三点:
@Around 是最外层:环绕通知的逻辑包住了其他所有通知,所以它一定最先进入、最后退出。
@After 类似 finally:无论目标方法正常返回还是抛异常,最终通知都会执行。
@AfterReturning 和 @AfterThrowing 互斥:正常返回只会走前者,抛异常只会走后者,二者不会同时执行。
当存在多个切面时,通知按照切面顺序(@Order 注解,数值越小优先级越高)以“洋葱模型”叠加:优先级高的切面先进入、后退出,优先级低的切面后进入、先退出。例如 Order(1) 在 Order(2) 之前,那么执行顺序是 Order(1) 外层、Order(2) 内层。这一点可以类比多个 Servlet Filter 的调用链来理解。
1.7 Spring AOP 为什么失效:高频踩坑场景
面试官常问一个实战性问题:“为什么我写的 @Transactional 或者自定义注解切面没有生效?”答好这个问题,能直接体现候选人是否真正理解 Spring AOP 的代理机制。AOP 失效的常见原因可以归纳为以下几条。
第一,同类内部通过 this 调用方法。这是最常见也最隐蔽的坑。Spring AOP 的本质是给目标对象创建一个代理对象,容器里注入的是代理 bean。如果业务代码里用this.method()调用自己的方法,this 指向的是原始目标对象,不是代理对象,自然不会经过拦截器链。
java
@Service public class OrderService { public void createOrder() { // this 调用不会走代理,@Transactional 或自定义切面失效 this.sendNotify(); } @Transactional public void sendNotify() { // 事务、日志等增强逻辑在这里都不会生效 } }解决方案通常有三种:
把 this 调用改为通过 Spring 容器拿代理对象后调用:
orderService = applicationContext.getBean(OrderService.class),再用orderService.sendNotify()。自己注入自己:在类中通过
@Autowired注入当前接口或类的代理对象,用注入的实例调用。使用
AopContext.currentProxy()获取当前代理对象,但必须配置exposeProxy=true。
第二,方法是 private、static 或 final。JDK 动态代理只代理接口方法,CGLIB 通过继承子类覆写方法,而 private、static、final 方法都无法被子类覆写,因此切面无法生效。Spring AOP 的增强应作用在 public 方法上。
第三,对象没有被 Spring 容器管理。如果代码里直接 new 了一个对象、自己 new 了 Service,而不是从容器获取 bean,这个对象根本不是代理对象,AOP 自然无效。
第四,切点表达式写错。包名、方法名、通配符不匹配,或者把 @Pointcut 里的 execution 表达式拼错,导致增强从未匹配到目标方法。
第五,Spring Boot 配置问题。切面类需要被扫描,例如@Component+ 确认包路径在主启动类扫描范围内;如果使用 XML 配置,还要确认<aop:aspectj-autoproxy>配置正确。
面试回答时不要只背“this 调用会失效”,而要补一句原因:Spring AOP 是基于代理对象做增强的,this 指向原始对象,绕过了代理。这样答更有深度。
1.8 Spring AOP 源码主线:一句话串起关键类
理解源码主线有助于把零散知识点串联起来,也能在面试中体现深度。Spring AOP 从启动到方法被拦截,可以压缩为下面这条链路。
开启代理:主配置类使用
@EnableAspectJAutoProxy,该注解通过@Import引入AspectJAutoProxyRegistrar。注册后置处理器:
AspectJAutoProxyRegistrar向容器注册AnnotationAwareAspectJAutoProxyCreator,它是一个BeanPostProcessor,负责在 bean 初始化前后做增强判断。判断是否需要代理:bean 实例化后,进入
postProcessAfterInitialization方法,内部调用wrapIfNecessary判断当前 bean 是否匹配到切点。查找增强器:通过
findEligibleAdvisors找到所有匹配当前 bean 的 Advisor,包括 @Before、@Around 等注解转换成的增强器。创建代理:命中的 bean 进入
createProxy逻辑,根据是否实现接口选择JdkDynamicAopProxy或CglibAopProxy,最终返回AopProxy对象。调用时代理拦截:方法调用时先进入
JdkDynamicAopProxy的invoke或CglibAopProxy的intercept。获取拦截器链:通过
getInterceptorsAndDynamicInterceptionAdvice获取与当前方法匹配的拦截器链。链式执行:创建
ReflectiveMethodInvocation,调用proceed(),按顺序逐个执行拦截器,最后通过反射调用真正的目标方法。
这条链路的两个关键点是:代理发生在 bean 初始化之后,以及方法执行时通过拦截器链逐层推进。理解这两个时间点,就能解释很多失效场景和拦截顺序问题。
1.9 Spring AOP 高频面试题汇总
下面把 AOP 相关的高频问题拆成可以直接回答的要点。
Q1:Spring AOP 的原理是什么?
Spring AOP 基于动态代理实现。运行时为目标对象生成代理对象,调用方法时先进入代理拦截逻辑,再执行横切通知,最后调用真正的目标方法。目标对象实现接口时默认用 JDK 动态代理,否则用 CGLIB 代理,Spring Boot 2.x 默认优先 CGLIB。
Q2:JDK 动态代理和 CGLIB 有什么区别?
JDK 动态代理基于接口,通过Proxy和InvocationHandler在运行时生成实现接口的代理类;CGLIB 基于继承,通过Enhancer和MethodInterceptor生成目标类的子类。JDK 代理要求目标类实现接口,CGLIB 不要求接口但要求类和方法不能是 final。
Q3:Spring AOP 和 AspectJ 有什么区别?
Spring AOP 是运行时动态代理,只支持方法级连接点;AspectJ 是静态织入,通过编译器或 agent 修改字节码,支持字段、构造器等更丰富连接点。Spring 借用了 AspectJ 的注解语法,但底层仍是动态代理。
Q4:五类通知的执行顺序是什么?
正常:@Around 前 → @Before → 目标方法 → @AfterReturning → @After → @Around 后;异常:@Around 前 → @Before → 目标方法 → @AfterThrowing → @After → @Around 后。多个切面按@Order洋葱模型叠加。
Q5:Spring AOP 在哪些情况下会失效?
常见有同类 this 方法调用、private 或 final 或 static 方法、目标对象未被 Spring 管理、切点表达式错误、切面类未被扫描等。核心原因是绕过了 Spring 创建的代理对象。
二、SpringMVC 过程详解
SpringMVC 是 Spring 生态中最经典的 Web 框架,围绕 DispatcherServlet 构建了一套完整的前端控制器模式。面试官考察 SpringMVC 时,最核心的问题就是“一次 HTTP 请求从前端到后端再到响应,中间经历了什么”。把这条主流程以及各组件职责讲清楚,SpringMVC 相关问题基本都能应对。
2.1 从一个 Servlet 说起:为什么是 DispatcherServlet
SpringMVC 底层仍然是 Servlet 技术。在传统 Java Web 中,我们需要在web.xml里配置很多 Servlet,每个 Servlet 负责一类请求。SpringMVC 的思路是使用一个中央调度器DispatcherServlet,把所有请求先统一收进来,再按规则分发给不同的处理器。DispatcherServlet本质上就是一个 Servlet,在web.xml中被映射到根路径/或/app等 URL。
在 Servlet 3.0 之后,Spring 也支持通过AbstractAnnotationConfigDispatcherServletInitializer或WebApplicationInitializer以编程方式注册。到了 Spring Boot 时代,AutoConfiguration 会自动配置好DispatcherServlet、DispatcherServletRegistrationBean以及必要的组件,开发者几乎不用关心 Servlet 层的注册细节。
2.2 SpringMVC 核心组件:先记住六个名词
SpringMVC 的工作过程可以概括为六个核心组件协同配合,先用一张表建立整体印象:
| 组件 | 作用 | 常见实现 |
|---|---|---|
| 前端控制器 DispatcherServlet | 接收请求、统一分发,协调其他组件 | DispatcherServlet |
| 处理器映射器 HandlerMapping | 根据请求找到对应处理器 | RequestMappingHandlerMapping |
| 处理器适配器 HandlerAdapter | 负责调用具体处理器 | RequestMappingHandlerAdapter |
| 处理器 Handler | 真正执行业务逻辑的 Controller 方法 | @Controller、@RestController |
| 视图解析器 ViewResolver | 把逻辑视图名解析为具体 View | InternalResourceViewResolver |
| 视图 View | 渲染最终响应内容 | JSP、Thymeleaf 等对应 View |
其中前三个组件是主流程中最重要的:HandlerMapping 负责找处理器、HandlerAdapter 负责调处理器、ViewResolver 负责找视图。面试时把这三句话说出来,主流程就有了骨架。
2.3 一次请求的完整处理流程:把链路一步步走一遍
这是 SpringMVC 面试中最高频的问题。一次 HTTP 请求的处理流程可以拆成以下步骤:
浏览器向服务器的某个 URL 发起请求,请求首先到达
DispatcherServlet。DispatcherServlet调用HandlerMapping,根据请求路径、请求方法等条件找到对应的 Handler,同时返回一条HandlerExecutionChain,其中包含处理器以及匹配的拦截器。DispatcherServlet根据 Handler 类型找到能处理它的HandlerAdapter。找到的
HandlerAdapter负责调用 Handler,也就是执行我们 Controller 中对应的业务方法,执行业务逻辑并返回ModelAndView(或直接返回数据)。HandlerAdapter将ModelAndView返回给DispatcherServlet。DispatcherServlet调用ViewResolver解析逻辑视图名,得到具体的 View 对象。View 使用 Model 中的数据渲染页面,把结果写回 response。
DispatcherServlet将最终响应返回给浏览器。
一套完整流程可以总结为:
text
请求进入 DispatcherServlet → HandlerMapping 找 Handler → HandlerAdapter 调 Handler → 返回 ModelAndView → ViewResolver 找 View → View 渲染响应
现代前后端分离项目中,Controller 经常直接返回 JSON 数据而不是 ModelAndView,此时对应的处理器是HttpMessageConverter,负责把返回对象序列化为 JSON 写回 response。
2.4 处理器映射器 HandlerMapping:请求是如何找到 Controller 的
HandlerMapping 的职责是根据请求信息定位处理器。以最常用的RequestMappingHandlerMapping为例,它在容器初始化时会扫描所有@Controller类中标了@RequestMapping的方法,把路径作为 key、处理器方法作为 value 建立映射关系。
当请求到达时,DispatcherServlet遍历容器中的 HandlerMapping,逐个调用getHandler方法,第一个返回非 null 结果的映射器胜出。getHandler方法内部会做 URL 匹配、请求方法匹配、参数匹配等操作,匹配成功后返回一个HandlerMethod,并把它包进HandlerExecutionChain。HandlerExecutionChain里除了处理器之外,还包含一层层HandlerInterceptor拦截器,拦截器链也是在这里组装的。
面试时还需要能说出:如果项目同时配置了多个 HandlerMapping,Spring 会按顺序优先匹配;由于RequestMappingHandlerMapping有@Order(0)的高优先级,所以通常先找到基于注解的处理器。
2.5 处理器适配器 HandlerAdapter:统一不同处理器的调用方式
为什么有了 Handler 还要 HandlerAdapter?因为 Handler 的具体类型可能各不相同:有的 Handler 是HandlerMethod,有的可能是普通Controller接口实现,有的可能是其他类型的处理器。如果DispatcherServlet直接去调用 Handler,就需要写大量 if-else 判断类型。
HandlerAdapter 用适配器模式解决了这个问题:DispatcherServlet只面向HandlerAdapter接口编程,由具体的适配器去调用具体类型的 Handler。这样新增一种 Handler 类型时,只需要新增一个对应的 HandlerAdapter,DispatcherServlet的代码不用改。
最常用的RequestMappingHandlerAdapter负责调用@RequestMapping标注的方法,它内部会做参数绑定、返回值处理、@ResponseBody解析等工作。参数绑定依赖HandlerMethodArgumentResolver,返回值处理依赖HandlerMethodReturnValueHandler,这两组组件是理解 Controller 方法签名如何生效的关键。
2.6 视图解析器 ViewResolver 与视图 View
当 Handler 返回ModelAndView时,DispatcherServlet会把逻辑视图名交给ViewResolver解析。最常见的配置是:
properties
spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp
这样逻辑视图名user/list会被解析成/WEB-INF/views/user/list.jsp。ViewResolver返回的View对象负责把 Model 中的数据填充到模板里,最终通过response输出。
在前后端分离架构中,Controller 通常返回对象并配合@ResponseBody(或@RestController),此时不会走 ViewResolver,而是由HttpMessageConverter把对象序列化成 JSON 直接写回响应体。
2.7 SpringMVC 中的拦截器 HandlerInterceptor
SpringMVC 的拦截器不是 Servlet Filter,而是由HandlerMapping组装进HandlerExecutionChain的组件。它有三个核心方法:
preHandle:在 Handler 执行前调用,返回 false 可以中断请求。postHandle:在 Handler 执行后、视图渲染前调用。afterCompletion:在整个请求完成后调用,适合做资源清理。
多个拦截器的执行顺序同样遵循“洋葱模型”:preHandle按配置顺序执行,postHandle和afterCompletion按逆序执行。这一点和 AOP 的环绕通知、Servlet Filter 链非常相似。
2.8 SpringMVC 高频面试题汇总
Q1:SpringMVC 的完整请求流程是什么?
请求先到DispatcherServlet,由HandlerMapping找到 Handler 和拦截器链,再由HandlerAdapter调用 Handler,Handler 返回ModelAndView,ViewResolver解析出 View,View 渲染后把响应返回给浏览器。
Q2:DispatcherServlet 的作用是什么?
它是前端控制器,负责接收所有请求、协调 HandlerMapping、HandlerAdapter、ViewResolver 等组件,是 SpringMVC 的中央调度器。
Q3:HandlerMapping 和 HandlerAdapter 有什么区别?
HandlerMapping 负责“找”处理器,HandlerAdapter 负责“调”处理器。前者解决请求到 Handler 的映射问题,后者解决不同类型 Handler 的统一调用问题。
Q4:SpringMVC 中拦截器和 Filter 有什么区别?
Filter 属于 Servlet 规范,作用于请求进入 Servlet 之前;Interceptor 属于 SpringMVC,作用于 Handler 执行前后。Filter 能拿到 request/response,Interceptor 还能拿到 HandlerMethod,能访问更多 SpringMVC 上下文信息。
Q5:@ResponseBody 是如何生效的?
@ResponseBody会告诉RequestMappingHandlerAdapter不要走视图解析,而是使用HttpMessageConverter把返回值序列化后直接写入响应体。@RestController相当于@Controller + @ResponseBody。
三、总结
Spring AOP 和 SpringMVC 是 Java 后端面试中两条非常重要的主线:
Spring AOP的核心是动态代理,JDK 代理基于接口,CGLIB 基于继承;通知有五种,执行顺序遵循环绕包裹和洋葱模型;失效场景大多源于绕过了代理对象。
SpringMVC的核心是 DispatcherServlet,请求流程可以概括为“DispatcherServlet → HandlerMapping → HandlerAdapter → Handler → ViewResolver → View → Response”。