Spring-AOP原理#
一句话答案#
AOP 通过动态代理在不修改业务代码的情况下织入横切逻辑(事务、日志、权限)。Spring Framework 默认对有接口的类用 JDK 动态代理、对无接口的类用 CGLIB;Spring Boot 2.x+ 默认全用 CGLIB(可通过
spring.aop.proxy-target-class=false切回 JDK 代理)。
核心要点
AOP 核心概念#
| 术语 | 含义 | 示例 |
|---|---|---|
| 切面(Aspect) | 横切关注点的模块化 | @Aspect 注解的类 |
| 连接点(JoinPoint) | 程序执行的某个点 | 方法调用、异常抛出 |
| 切入点(Pointcut) | 匹配连接点的表达式 | execution(* com.xxx.service.*.*(..)) |
| 通知(Advice) | 在切入点执行的动作 | @Before / @After / @Around |
| 织入(Weaving) | 将切面应用到目标对象的过程 | Spring 在运行时通过代理织入 |
五种通知类型#
@Around 环绕通知(最强大,可控制是否执行目标方法)
├── @Before 前置通知(方法执行前)
│ ↓
│ 目标方法执行
│ ↓
├── @AfterReturning 返回通知(正常返回后)
├── @AfterThrowing 异常通知(抛异常后)
└── @After 最终通知(无论正常/异常都执行,类似 finally)plaintext两种代理方式#
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 基于接口,生成实现同接口的代理类 | 基于继承,生成目标类的子类 |
| 要求 | 目标类必须实现接口 | 目标类不能是 final |
| 性能 | 调用时通过反射,稍慢 | 通过 FastClass 直接调用,稍快 |
| Spring 默认 | Spring Framework 默认(有接口时) | Spring Boot 2.x+ 默认全用 CGLIB |
Spring Boot 为什么改默认为 CGLIB?
- 避免类型转换问题:JDK 代理生成的是接口类型,注入时用实现类类型会报错
- 统一行为,减少开发者困惑
代理生成时机#
Spring 容器启动
→ Bean 实例化
→ 属性注入
→ BeanPostProcessor 的 postProcessAfterInitialization
→ AbstractAutoProxyCreator 检查是否需要代理
├── 有匹配的 Advisor → 创建代理对象
└── 无匹配 → 返回原始 Beanplaintext关键类: AnnotationAwareAspectJAutoProxyCreator(继承自 AbstractAutoProxyCreator),在 Bean 初始化完成后判断是否需要代理。
深挖:拦截器责任链如何驱动通知顺序#
多个 Advice 怎么串成一条链:
- 每个
@Before/@After/@Around等通知,在底层都被适配成一个MethodInterceptor(如MethodBeforeAdviceInterceptor、AspectJAroundAdvice)。 - 代理被调用时,Spring 把匹配该方法的所有拦截器排成一个
List<MethodInterceptor>,封装进ReflectiveMethodInvocation。
proceed() 递归驱动整条链(这是执行顺序的底层原因):
ReflectiveMethodInvocation.proceed():
if (拦截器都走完) → 反射调用目标方法 method.invoke(target)
else → 取下一个拦截器,调 interceptor.invoke(this)
拦截器内部再调 mi.proceed() → 进入下一层plaintext- 每个拦截器在调用
proceed()之前的代码 = 前置逻辑,之后的代码 = 后置逻辑。 - 正是这种「调用前/调用后包裹 +
proceed()递归下钻」的结构,决定了@Around包在最外、@Before先于目标方法、@After/@AfterReturning在返回时执行——顺序不是写死的 if-else,而是责任链递归天然形成的洋葱模型。
面试常追的一句话: @Around 里如果不调 joinPoint.proceed(),目标方法和它内层的所有通知都不会执行——因为链断了。
深挖:Spring AOP vs AspectJ 的根因对比#
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入方式 | 运行时动态代理 | 编译期(CTW)/ 加载期(LTW)字节码织入 |
| 能切什么 | 只能切 Spring 容器管理的 Bean 的方法 | 构造器、字段、静态方法、任意类(含非 Bean) |
| 自调用 | 失效(绕过代理) | 生效(字节码直接改了方法本身) |
| 依赖 | 无需特殊编译 | 需 ajc 编译器或 LTW agent |
根因: Spring AOP 基于代理,增强逻辑写在「代理对象」里,所以只有通过代理对象调用、且是被 Spring 管理的 Bean 方法才能被拦截——这就是自调用失效、private/构造器切不到的本质。AspectJ 直接把横切逻辑织入字节码本身,方法体被真正改写,因此不受代理边界限制,能切构造器、字段访问、final 方法乃至非 Spring 管理的对象。
AOP 在 Spring 中的典型应用#
- @Transactional:事务管理(最常见)
- @Cacheable:缓存
- @Async:异步执行
- @PreAuthorize:权限控制(Spring Security)
- 自定义注解:日志、限流、幂等
面试回答(2分钟版)
Spring AOP 的实现原理是动态代理。在 Bean 初始化完成后,BeanPostProcessor(具体是 AbstractAutoProxyCreator)检查是否有匹配的切面,有的话就创建代理对象替换原始 Bean。代理方式有两种:JDK 动态代理基于接口、CGLIB 基于继承生成子类。注意区分两个层面的默认行为:Spring Framework 默认有接口用 JDK 代理、无接口用 CGLIB;Spring Boot 2.x 之后通过自动配置将默认改为全用 CGLIB,主要是为了避免 JDK 代理生成接口类型导致注入实现类时报错的问题。AOP 在 Spring 中最典型的应用是 @Transactional,事务管理就是通过 AOP 在方法前后开启/提交/回滚事务实现的。
追问与易错
追问方向:
- “AOP 和拦截器有什么区别?”→ AOP 基于代理作用于 Bean 方法,拦截器基于 Servlet 作用于 HTTP 请求
- “@Transactional 为什么自调用失效?”→ 自调用不经过代理对象,直接调 this.method()
- “CGLIB 为什么不能代理 final 方法?”→ CGLIB 通过子类继承,final 方法不能被重写
易错点:
- ❌ “AOP 是编译时织入”——Spring AOP 是运行时通过代理织入,AspectJ 才是编译时
- ❌ “JDK 代理比 CGLIB 快”——JDK 代理通过反射调用,CGLIB 通过 FastClass 直接调用,CGLIB 调用更快
- ❌ “Spring AOP 能拦截 private 方法”——代理只能拦截 public 方法