场景:两根支柱撑起整个 Spring

无论你用的是 Spring Framework 还是 Spring Boot,所有"开箱即用"的能力——依赖注入、声明式事务、@Cacheable@Async——最终都落在两块基石上:IoC 容器负责"谁来创建对象、对象之间怎么关联",AOP 负责"在不改原代码的前提下织入横切逻辑"。把这两者的实现机制讲清楚,你才能理解为什么 this.method() 调用会让事务失效,为什么有些 Bean 注入进来是个代理而不是原类。

IoC 容器:控制反转的实现机制

从 BeanDefinition 到 Bean 实例

很多人以为 IoC 容器里存的就是对象,其实容器里先存的是对象的"配方"——BeanDefinition。它描述了:类是什么、作用域(singleton/prototype)、依赖哪些 Bean、初始化方法、是否懒加载等。

容器启动可以分成两大阶段:

  1. 注册阶段:扫描 @Component / 解析 @Configuration@Bean 方法 / 读取 XML,把所有 BeanDefinition 注册进 BeanDefinitionRegistry(底层是一个 Map<String, BeanDefinition>)。此阶段还会执行 BeanFactoryPostProcessor,允许修改定义本身(比如占位符替换 ${})。
  2. 实例化阶段:对非懒加载的单例,触发 getBean,走"实例化 → 属性填充 → 初始化"流程,把成品放进单例池。
1
2
3
4
5
6
7
// AbstractApplicationContext#refresh 的核心骨架(简化)
public void refresh() {
prepareBeanFactory(beanFactory);
invokeBeanFactoryPostProcessors(beanFactory); // 改 BeanDefinition
registerBeanPostProcessors(beanFactory); // 注册 Bean 后置处理器
finishBeanFactoryInitialization(beanFactory); // 实例化所有单例
}

refresh() 是整个容器的"开机引导程序",上面每一行都对应一个生命周期阶段,顺序不可颠倒。

依赖注入的本质

DI 不是什么神秘技术,核心就是:容器在创建 A 时发现 A 依赖 B,于是先把 B 创建好(或从池里取),再通过构造器/setter/反射字段塞给 A。三种注入方式:

  • 构造器注入:依赖在对象创建时即确定,适合强制依赖,且能保证不可变(final 字段)。
  • 字段注入(@Autowired 直接打在字段上):写起来最简洁,但无法用于 final,且对单元测试不友好(必须靠反射或容器才能注值),社区普遍不推荐。
  • setter 注入:适合可选依赖。
1
2
3
4
5
6
7
8
@Service
public class OrderService {
private final StockService stockService;
// 构造器注入:依赖清晰、可测试、可保证 final
public OrderService(StockService stockService) {
this.stockService = stockService;
}
}

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
2
3
4
5
6
7
8
9
10
11
// 环绕通知示意:proceed() 之前是"进入",之后是"返回"
@Around("@annotation(com.demo.Timed)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // 调用下一个拦截器,最终到目标方法
} finally {
long cost = System.nanoTime() - start;
log.info("{} 耗时 {} ns", pjp.getSignature(), cost);
}
}

AOP 与 IoC 的衔接点:BeanPostProcessor

AOP 不是独立系统,它是通过 IoC 的扩展点 BeanPostProcessor 接入的。AnnotationAwareAspectJAutoProxyCreator 实现了 postProcessAfterInitialization,在每个 Bean 初始化完成后判断:这个 Bean 是否需要被某个切面增强?需要就返回代理,不需要就返回原对象。这解释了为什么 AOP 的开关、切面的收集都和容器生命周期紧密绑定。

工程权衡

维度 JDK 动态代理 CGLIB
前提 必须有接口 可代理普通类,但不能代理 final
实现 反射调用接口方法 生成子类重写方法
创建开销 较低 较高(需生成字节码)
调用开销 早期反射较慢,现代 JVM 差距已小 直接方法调用,略优

性能上,代理调用相比直接调用有额外开销(拦截器链遍历、反射或字节码跳转),但对绝大多数业务方法,这点开销相对于方法体内的 IO/计算可以忽略。真正要警惕的是滥用细粒度切面导致每个方法都被包一层代理,放大对象创建和调用成本。

常见误区与线上踩坑

踩坑一:同类内部方法调用导致 AOP 失效。 这是最经典的坑:

1
2
3
4
5
6
7
8
9
@Service
public class UserService {
@Transactional
public void outer() {
this.inner(); // 直接 this 调用,绕过代理,@Transactional 不生效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* ... */ }
}

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 自调用绕过代理是所有失效问题的根源。