场景:两根支柱撑起整个 Spring
无论你用的是 Spring Framework 还是 Spring Boot,所有"开箱即用"的能力——依赖注入、声明式事务、@Cacheable、@Async——最终都落在两块基石上:IoC 容器负责"谁来创建对象、对象之间怎么关联",AOP 负责"在不改原代码的前提下织入横切逻辑"。把这两者的实现机制讲清楚,你才能理解为什么 this.method() 调用会让事务失效,为什么有些 Bean 注入进来是个代理而不是原类。
IoC 容器:控制反转的实现机制
从 BeanDefinition 到 Bean 实例
很多人以为 IoC 容器里存的就是对象,其实容器里先存的是对象的"配方"——BeanDefinition。它描述了:类是什么、作用域(singleton/prototype)、依赖哪些 Bean、初始化方法、是否懒加载等。
容器启动可以分成两大阶段:
- 注册阶段:扫描
@Component/ 解析@Configuration的@Bean方法 / 读取 XML,把所有 BeanDefinition 注册进BeanDefinitionRegistry(底层是一个Map<String, BeanDefinition>)。此阶段还会执行BeanFactoryPostProcessor,允许修改定义本身(比如占位符替换${})。 - 实例化阶段:对非懒加载的单例,触发
getBean,走"实例化 → 属性填充 → 初始化"流程,把成品放进单例池。
1 | // AbstractApplicationContext#refresh 的核心骨架(简化) |
refresh() 是整个容器的"开机引导程序",上面每一行都对应一个生命周期阶段,顺序不可颠倒。
依赖注入的本质
DI 不是什么神秘技术,核心就是:容器在创建 A 时发现 A 依赖 B,于是先把 B 创建好(或从池里取),再通过构造器/setter/反射字段塞给 A。三种注入方式:
- 构造器注入:依赖在对象创建时即确定,适合强制依赖,且能保证不可变(
final字段)。 - 字段注入(
@Autowired直接打在字段上):写起来最简洁,但无法用于final,且对单元测试不友好(必须靠反射或容器才能注值),社区普遍不推荐。 - setter 注入:适合可选依赖。
1 |
|
AOP:代理是怎么"凭空"加上逻辑的
织入机制:运行时动态代理
Spring AOP 默认采用运行时动态代理(而非 AspectJ 的编译期/加载期字节码织入)。当一个 Bean 匹配到切面的切点时,容器在 Bean 初始化阶段不会把原始对象放进容器,而是放一个代理对象。
两种代理实现:
- JDK 动态代理:目标类实现了接口时使用。基于
java.lang.reflect.Proxy,生成一个实现相同接口的代理类,所有接口方法调用先进InvocationHandler。 - CGLIB 代理:目标类没有接口时使用(或强制
proxyTargetClass=true)。通过生成目标类的子类并重写方法来织入。正因为是子类,所以final类/方法无法被 CGLIB 代理。
Spring Boot 从某个版本起默认 proxyTargetClass=true,即倾向于 CGLIB,以避免"注入接口类型却拿到代理、注入实现类却失败"的混乱。
一次代理方法调用的流转
代理方法被调用时,会构建一条拦截器链(MethodInterceptor 链),把匹配该方法的所有 Advice(前置、后置、环绕、异常)串起来,通过 MethodInvocation.proceed() 逐个推进,像洋葱一样层层包裹目标方法:
1 | // 环绕通知示意:proceed() 之前是"进入",之后是"返回" |
AOP 与 IoC 的衔接点:BeanPostProcessor
AOP 不是独立系统,它是通过 IoC 的扩展点 BeanPostProcessor 接入的。AnnotationAwareAspectJAutoProxyCreator 实现了 postProcessAfterInitialization,在每个 Bean 初始化完成后判断:这个 Bean 是否需要被某个切面增强?需要就返回代理,不需要就返回原对象。这解释了为什么 AOP 的开关、切面的收集都和容器生命周期紧密绑定。
工程权衡
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 前提 | 必须有接口 | 可代理普通类,但不能代理 final |
| 实现 | 反射调用接口方法 | 生成子类重写方法 |
| 创建开销 | 较低 | 较高(需生成字节码) |
| 调用开销 | 早期反射较慢,现代 JVM 差距已小 | 直接方法调用,略优 |
性能上,代理调用相比直接调用有额外开销(拦截器链遍历、反射或字节码跳转),但对绝大多数业务方法,这点开销相对于方法体内的 IO/计算可以忽略。真正要警惕的是滥用细粒度切面导致每个方法都被包一层代理,放大对象创建和调用成本。
常见误区与线上踩坑
踩坑一:同类内部方法调用导致 AOP 失效。 这是最经典的坑:
1 |
|
this 指向的是原始对象而非代理对象,调用根本没经过拦截器链,所有基于 AOP 的注解(@Transactional、@Cacheable、@Async)都会失效。解法:注入自身代理、用 AopContext.currentProxy(),或拆分到不同 Bean。
踩坑二:final 方法/类上的注解不生效。 CGLIB 靠子类重写,final 无法重写,注解被静默忽略,没有报错,极难发现。
误区:以为 @Autowired 注入的就是原类。 一旦目标 Bean 被 AOP 增强,你注入到的就是代理对象。用 CGLIB 时它是子类,getClass() 会显示 $$EnhancerBySpringCGLIB$$ 后缀;这也是为什么不要对注入的 Bean 用 == 或反射去比较具体实现类。
小结
IoC 容器把"对象创建与装配"从代码里反转给框架,核心是 BeanDefinition 的注册与单例的"实例化—填充—初始化"流程;AOP 借助 BeanPostProcessor 在初始化后用动态代理替换 Bean,把横切逻辑织入拦截器链。两者通过生命周期扩展点无缝咬合。记住一条:AOP 的一切增强都依赖"调用经过代理对象"这个前提,this 自调用绕过代理是所有失效问题的根源。