Spring AOP 作为面向切面编程的实现,是 Spring 框架中仅次于 IoC 的核心功能。它通过动态代理机制实现代码的横切复用,在事务管理、日志记录、权限控制等场景中应用广泛。理解 AOP 的底层实现,尤其是动态代理的选择逻辑和切面织入流程,是面试中的重要加分项。本文结合源码与面试场景,解析核心原理与实战要点。
一、AOP 核心概念与设计思想
在深入源码前,需先明确 AOP 的核心概念,这是理解后续流程的基础。
1. 核心术语(面试基础题)
- 切面(Aspect):横切关注点的模块化(如日志切面、事务切面),通常由@Aspect注解标识。
- 连接点(Joinpoint):程序执行过程中的可插入点(如方法调用、字段赋值),Spring AOP 中仅支持方法执行(method execution)。
- 切入点(Pointcut):筛选连接点的条件(如 “所有 Service 类的 save 方法”),通过表达式(如execution(* com.example.service.*.save(..)))定义。
- 通知(Advice):切面在连接点执行的操作,包括:
-
- @Before:方法执行前;
-
- @AfterReturning:方法正常返回后;
-
- @AfterThrowing:方法抛出异常后;
-
- @After:方法执行后(无论正常 / 异常);
-
- @Around:环绕方法执行(可控制方法是否执行)。
- 目标对象(Target):被代理的原始对象。
- 代理对象(Proxy):AOP 为目标对象创建的代理实例,实际执行时会先调用通知逻辑。
面试应答技巧:用 “日志记录” 场景举例说明 —— 切面是日志模块,连接点是所有 Service 方法,切入点是 “以 save 开头的方法”,通知是日志打印逻辑,目标对象是 Service 实例,代理对象是 Spring 创建的 Service 代理。
2. AOP 的设计思想:代理模式的应用
Spring AOP 的核心是动态代理:在运行时为目标对象创建代理对象,将切面逻辑织入代理对象的方法中,从而实现对目标对象的增强,且不修改其源码。
这种设计的优势在于:
- 低侵入:业务代码与横切逻辑分离(如 Service 类只关注业务,日志由切面负责);
- 高复用:横切逻辑(如事务、日志)可在多个地方复用;
- 易维护:修改横切逻辑只需调整切面,无需修改所有业务类。
二、动态代理机制:JDK 与 CGLIB 的选择逻辑
面试高频问:Spring AOP 如何选择 JDK 动态代理和 CGLIB 代理?两者的区别是什么?
Spring AOP 支持两种动态代理方式,由DefaultAopProxyFactory决定最终使用哪种代理。
1. 两种代理方式的核心区别
维度 |
JDK 动态代理 |
CGLIB 代理 |
原理 |
基于接口实现,代理类实现目标对象的接口 |
基于继承实现,代理类继承目标对象 |
要求 |
目标对象必须实现接口 |
目标对象可以是类(无需接口),但不能是 final 类 |
效率 |
方法调用时通过反射,低版本 JDK 中效率较低 |
通过生成目标对象的子类重写方法,效率较高(尤其是多次调用时) |
2. 代理选择的源码逻辑(DefaultAopProxyFactory.createAopProxy())
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 条件1:是否优化(默认false)或是否有接口或代理目标为接口
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 否则使用CGLIB代理
return new ObjenesisCglibAopProxy(config);
} else {
// 有接口则使用JDK代理
return new JdkDynamicAopProxy(config);
}
}
核心规则:
- 若目标对象实现了接口,默认使用 JDK 动态代理;
- 若目标对象未实现接口,必须使用 CGLIB 代理;
- 若强制设置proxyTargetClass = true(如@EnableAspectJAutoProxy(proxyTargetClass = true)),则无论是否有接口,均使用 CGLIB 代理。
实战注意:Spring Boot 2.x 中,@EnableAutoConfiguration默认启用proxyTargetClass = true,因此即使目标对象有接口,也可能使用 CGLIB 代理。
三、切面织入流程:从切面定义到代理对象创建
切面织入是 AOP 的核心流程,指将切面的通知逻辑与目标对象的方法关联起来,最终生成代理对象。流程可分为 “切面解析” 和 “代理创建” 两个阶段。
1. 切面解析:识别切面与通知(@Aspect注解的处理)
- 触发点:@EnableAspectJAutoProxy注解导入AspectJAutoProxyRegistrar,注册AnnotationAwareAspectJAutoProxyCreator(核心处理器)。
- 核心逻辑:AnnotationAwareAspectJAutoProxyCreator扫描容器中带@Aspect注解的 Bean,解析其中的@Pointcut、@Before等注解,将其转换为 Spring AOP 的内部对象(Advisor、Pointcut、Advice)。
例如,@Before("execution(* save(..))")会被解析为:
- Pointcut:匹配所有 save 方法的表达式;
- BeforeAdvice:包含通知逻辑的对象;
- Advisor:将 Pointcut 和 Advice 组合的 “切面顾问”。
2. 代理对象的创建与方法执行
当容器初始化目标对象(如 Service)时,AnnotationAwareAspectJAutoProxyCreator(实现了BeanPostProcessor)会在初始化后阶段(postProcessAfterInitialization()) 为其创建代理对象,替换原始对象存入容器。
(1)代理对象的方法执行流程(以 JDK 代理为例)
- 调用代理对象的方法(如service.save());
- 代理对象将调用转发给JdkDynamicAopProxy.invoke()方法;
- invoke()方法查找该方法匹配的所有Advisor(通知);
- 按顺序执行通知逻辑(如@Before → 目标方法 → @AfterReturning);
- 返回结果。
(2)通知执行顺序(实战必须掌握)
同一切入点的通知执行顺序:
- @Around(开始) → @Before → 目标方法执行 → @Around(结束) → @After → @AfterReturning/@AfterThrowing
注意:@Around通知需要手动调用ProceedingJoinPoint.proceed()才能执行目标方法,若不调用则目标方法被拦截。
四、@Transactional 注解的实现原理(AOP 的典型应用)
@Transactional是 AOP 的典型应用,面试常问:事务是如何通过 AOP 实现的?为什么自调用(同一类中的方法调用)会导致事务失效?
1. 事务切面的织入流程
- 切面定义:Spring 的TransactionInterceptor是事务通知的实现类,包含事务的开启、提交、回滚逻辑。
- 切入点:匹配带@Transactional注解的方法(或 XML 配置的事务规则)。
- 通知类型:@Around环绕通知,在方法执行前后管理事务。
2. 事务执行的核心逻辑
// 简化的事务通知逻辑
public Object invoke(MethodInvocation invocation) throws Throwable {
// 1. 获取事务属性(如传播行为、隔离级别)
TransactionAttribute txAttr = getTransactionAttribute(invocation);
// 2. 开启事务
TransactionStatus status = transactionManager.getTransaction(txAttr);
try {
// 3. 执行目标方法(如Service的业务方法)
Object result = invocation.proceed();
// 4. 无异常则提交事务
transactionManager.commit(status);
return result;
} catch (Throwable ex) {
// 5. 有异常则回滚事务(根据@Transactional的rollbackFor配置)
transactionManager.rollback(status);
throw ex;
}
}
3. 自调用导致事务失效的原因(实战高频问题)
场景:Service 类中,方法 A 调用同一类中的方法 B,方法 B 有@Transactional注解,但事务不生效。
根源:AOP 代理对象的方法调用才会触发通知逻辑。自调用时,this.methodB()是原始对象的调用(未经过代理),因此事务通知不会执行。
解决方案:
- 注入自身代理对象(通过@Autowired或AopContext.currentProxy()获取),调用代理对象的方法;
- 将方法拆分到不同的类中,避免自调用。
五、面试高频问题与应答框架
1. 问:Spring AOP 与 AspectJ 的区别?
应答框架:
“两者的核心区别体现在实现方式和功能范围:
- 实现方式:Spring AOP 是运行时动态代理(JDK/CGLIB),在 JVM 运行时创建代理对象;AspectJ 是编译期 / 类加载期织入,直接修改目标类的字节码。
- 功能范围:Spring AOP 仅支持方法级别的连接点;AspectJ 支持字段、构造方法、静态方法等更多连接点。
- 使用场景:Spring AOP 适合简单的横切逻辑(如事务、日志),集成到 Spring 生态中;AspectJ 适合复杂的切面需求,但需要额外的编译或织入步骤。”
2. 问:如何解决 CGLIB 代理无法代理 final 方法的问题?
应答框架:
“CGLIB 通过继承目标类并重写方法实现代理,而 final 方法无法被重写,因此会导致切面逻辑无法织入 final 方法。
解决方案有三种:
- 避免将需要增强的方法声明为 final;
- 若方法必须为 final,让目标类实现接口,强制 Spring 使用 JDK 动态代理(需确保接口包含该方法);
- 若使用 Spring Boot,可通过@EnableAspectJAutoProxy(proxyTargetClass = false)禁用 CGLIB,优先使用 JDK 代理。
实际开发中,更推荐第一种方案,遵循‘需要增强的方法不声明为 final’的编码规范。”
3. 问:@Around 通知与其他通知的区别?
应答框架:
“@Around是功能最强大的通知类型,与其他通知的核心区别在于控制权:
- 其他通知(如@Before、@After)仅能在目标方法执行前后添加逻辑,无法阻止或修改目标方法的执行;
- @Around通知通过ProceedingJoinPoint.proceed()手动触发目标方法,因此可以:
- 控制目标方法是否执行(不调用proceed()则拦截);
- 修改目标方法的参数(通过proceed(args)传递新参数);
- 修改目标方法的返回值(包装proceed()的结果)。
使用时需注意:必须调用proceed(),否则目标方法和后续通知(如@After)都不会执行。”
六、实战总结
Spring AOP 的底层是动态代理与切面织入的结合,核心流程可概括为:解析切面定义→创建代理对象→执行方法时织入通知逻辑。理解代理选择逻辑、通知执行顺序及@Transactional的实现原理,不仅能应对面试中的深度提问,更能在实战中避免事务失效、代理不生效等常见问题。
下一篇将解析 SpringBoot 自动配置的核心机制,包括@SpringBootApplication注解的作用、AutoConfigurationImportSelector的工作流程,这是 SpringBoot 简化开发的关键所在。”