Spring 核心(IOC + AOP)深度指南
Spring 核心(IOC + AOP)深度指南
本文面向已经在项目里用过 Spring/Spring Boot、准备高级/资深 Java 岗位面试的同学。目标不是背"IOC 就是控制反转"这类概念复述,而是讲清楚"容器启动到底做了哪些事""循环依赖为什么必须用三级缓存""AOP 代理具体怎么生成、怎么串联多个切面""生产上事务和 AOP 会踩什么坑"。
源码基准版本说明:本文源码分析基于 Spring Framework 6.x(对应 Spring Boot 3.x、JDK 17)。IOC 三级缓存、AOP 动态代理、
BeanPostProcessor扩展点这些核心机制,在 Spring 5/6/7 大版本之间的结构和调用链路是稳定的,具体源码行号会随版本、甚至随小版本的重构略有出入,本文一律不给"第几行"这种容易过时且无法验证的细节,只给真实存在、跨版本基本稳定的类名/方法名/字段名(如DefaultSingletonBeanRegistry#getSingleton、singletonObjects/earlySingletonObjects/singletonFactories、AbstractAutowireCapableBeanFactory#doCreateBean、AnnotationAwareAspectJAutoProxyCreator等),配合简化后的伪代码还原关键逻辑。文中对不确定的实现细节(尤其是三级缓存"为什么不能是两级"背后的内部字段命名等)会明确标注"建议对照本地源码复核",不做没有把握的断言。
目录
每节标题保持简短,核心结论以 要点提示放在每节正文开头。
一、IOC 容器整体架构
要点:
BeanFactory是最底层的容器契约,只负责"按需创建/获取 Bean";ApplicationContext是面向应用的容器,在BeanFactory基础上叠加了国际化、事件发布、资源加载、以及最关键的——启动时立即初始化所有非懒加载单例。容器启动的本质是"把配置元数据(XML/注解/配置类)解析成统一的BeanDefinition,再按BeanDefinition批量创建 Bean"这两阶段的分离。
1.1 BeanFactory 与 ApplicationContext 的职责边界
BeanFactory 是 Spring IOC 容器的根接口,只定义了最基本的能力:getBean()、containsBean()、isSingleton()、getType() 等,语义上是一个懒加载、按需服务的对象工厂——默认实现 DefaultListableBeanFactory 在没有被外部驱动之前,不会主动去实例化任何单例 Bean。
ApplicationContext 继承自 BeanFactory(准确说是通过 ListableBeanFactory、HierarchicalBeanFactory 等一系列接口间接继承),同时还组合了:
| 能力 | 对应接口 | 解决的问题 |
|---|---|---|
| Bean 管理 | ListableBeanFactory / HierarchicalBeanFactory |
按类型/名称批量获取 Bean,支持父子容器 |
| 国际化 | MessageSource |
message.properties 多语言文案解析 |
| 事件发布 | ApplicationEventPublisher |
ApplicationEvent/ApplicationListener 观察者模式 |
| 资源加载 | ResourcePatternResolver |
classpath*: 等通配符资源加载 |
| 环境抽象 | EnvironmentCapable |
Profile、PropertySource(配置文件、系统变量、启动参数的统一视图) |
两者最容易被追问的一点区别是初始化时机:BeanFactory 是懒加载的(调用 getBean() 时才真正实例化),而 ApplicationContext(准确说是它的典型实现 AbstractApplicationContext)在 refresh() 流程的最后阶段会主动把所有非懒加载(lazy-init=false)的单例 Bean 全部创建完毕(finishBeanFactoryInitialization() 里调用 preInstantiateSingletons())。这样设计的好处是:配置错误、依赖缺失这类问题能在应用启动阶段集中暴露,而不是分散到运行期某次请求第一次触碰到某个 Bean 时才报错——对于一个需要长期稳定运行的服务,"启动即报错"远比"运行到一半才报错"更可控。
日常开发中几乎不会直接面向 BeanFactory 编程,ApplicationContext(AnnotationConfigApplicationContext、Spring Boot 里的 AnnotationConfigServletWebServerApplicationContext 等)才是实际使用的容器,但理解这层职责划分,是理解后面 refresh() 里"为什么要分好几步"的前提。
1.2 BeanDefinition:容器启动的中间表示
要点:不管配置来自 XML、
@Component扫描还是@Configuration类里的@Bean方法,容器启动的第一阶段都是把这些异构的配置来源统一解析成同一种数据结构——BeanDefinition,之后所有创建 Bean 的逻辑都只认BeanDefinition,不再关心它最初来自哪种配置方式。这是"配置解析"和"实例创建"两阶段解耦的关键。
BeanDefinition 本质是一份 Bean 的"配置元数据",不是 Bean 实例本身,常见字段包括:beanClassName(要实例化的类)、scope(singleton/prototype)、lazyInit、dependsOn、propertyValues(setter 注入的属性值)、constructorArgumentValues(构造器参数)、factoryBeanName/factoryMethodName(工厂方法创建方式,@Bean 方法就是走这条路径)。常见实现类有 RootBeanDefinition、GenericBeanDefinition、AnnotatedGenericBeanDefinition、ScannedGenericBeanDefinition。
XML、注解、@Configuration 类如何解析成 BeanDefinition
三种主流配置方式最终都要落到同一个终点——调用 BeanDefinitionRegistry#registerBeanDefinition(name, beanDefinition) 把解析结果注册进容器(DefaultListableBeanFactory 内部维护了一个 Map<String, BeanDefinition> 保存全部注册信息),但解析路径不同:
| 配置来源 | 核心解析组件 | 产出的 BeanDefinition 类型 |
|---|---|---|
XML <bean> 标签 |
XmlBeanDefinitionReader → BeanDefinitionDocumentReader → BeanDefinitionParserDelegate |
GenericBeanDefinition/RootBeanDefinition |
@Component/@Service/@Repository/@Controller 扫描 |
ClassPathBeanDefinitionScanner(底层用 ClassPathScanningCandidateComponentProvider 找候选类) |
ScannedGenericBeanDefinition |
@Configuration 类里的 @Bean 方法 |
ConfigurationClassPostProcessor → ConfigurationClassParser 解析类 → ConfigurationClassBeanDefinitionReader 把每个 @Bean 方法转成一个 BeanDefinition |
ConfigurationClassBeanDefinition(工厂方法模式,factoryBeanName 指向该配置类自身) |
@Configuration 类的处理值得单独展开,因为它是 Spring Boot 时代最主流的配置方式:ConfigurationClassPostProcessor 本质是一个特殊的 BeanFactoryPostProcessor(准确说是它的子接口 BeanDefinitionRegistryPostProcessor,见 1.3 节),它会解析 @ComponentScan(触发扫描)、@Import(导入其他配置类或 ImportSelector/ImportBeanDefinitionRegistrar)、@ImportResource(导入 XML)、以及类中所有 @Bean 方法,把它们统一转换成 BeanDefinition 注册进容器。
一个容易被问到的细节:@Configuration 类默认会被 CGLIB 增强("full 模式",区别于 proxyBeanMethods=false 的"lite 模式")。原因是如果配置类里一个 @Bean 方法在 Java 代码层面直接调用另一个 @Bean 方法(比如 beanA() 方法体里写 beanB()),如果不做任何处理,这就是一次普通的 Java 方法调用,会绕开容器直接 new 出一个新对象,破坏单例语义。CGLIB 增强后,配置类会被替换成一个子类代理,代理会拦截这类方法调用,改为"先去容器里查有没有这个 Bean,有就直接返回已有实例,没有才真正执行原方法逻辑"——这也是为什么"@Bean 方法互相调用能拿到同一个单例"这件事只在 @Configuration(full 模式)下成立,如果类上只标了 @Component 而不是 @Configuration,方法间调用就是普通调用,不会走容器。
1.3 refresh() 方法的核心步骤全貌
要点:
AbstractApplicationContext#refresh()是整个容器启动的总入口,本质上是一条严格顺序执行的流水线:先准备环境 → 解析配置得到全部BeanDefinition→ 让BeanFactoryPostProcessor有机会在真正创建 Bean 之前修改这些元数据 → 注册好所有BeanPostProcessor(包括 AOP、@Autowired处理器)→ 最后才批量创建所有单例 Bean。顺序不能乱——比如BeanPostProcessor必须在所有单例创建之前注册完毕,否则先创建的 Bean 就享受不到后创建的BeanPostProcessor的处理,这是排查"为什么某个 AOP/自动注入没生效"时经常要回到的一个基础事实。
refresh() 的核心步骤(简化版,重点讲每一步解决的问题):
| 步骤 | 方法 | 解决的问题 |
|---|---|---|
| 1 | prepareRefresh() |
记录启动时间、初始化 Environment 中的属性源,校验必需的配置项是否存在 |
| 2 | obtainFreshBeanFactory() |
创建 DefaultListableBeanFactory,加载全部 BeanDefinition(对应 1.2 节的解析过程) |
| 3 | prepareBeanFactory(beanFactory) |
设置类加载器、SpEL 解析器;注册 ApplicationContextAwareProcessor(一个 BeanPostProcessor,负责回调各种 Aware 接口);注册 BeanFactory/ApplicationContext 等内置可解析依赖 |
| 4 | postProcessBeanFactory(beanFactory) |
子类扩展点,比如 Web 环境在这里注册 request/session 作用域 |
| 5 | invokeBeanFactoryPostProcessors(beanFactory) |
在任何 Bean 真正实例化之前,让 BeanFactoryPostProcessor 修改 BeanDefinition 元数据;ConfigurationClassPostProcessor 正是在这一步执行,完成 @Configuration/@ComponentScan/@Bean 的解析注册;PropertySourcesPlaceholderConfigurer 在这一步解析 ${...} 占位符 |
| 6 | registerBeanPostProcessors(beanFactory) |
找出所有 BeanPostProcessor 类型的 BeanDefinition,实例化并注册到容器(按 PriorityOrdered/Ordered 排序),AnnotationAwareAspectJAutoProxyCreator(AOP)、AutowiredAnnotationBeanPostProcessor(@Autowired)都是在这一步真正成为可用的处理器 |
| 7 | initMessageSource() |
初始化国际化组件 |
| 8 | initApplicationEventMulticaster() |
初始化事件广播器,为事件机制做准备 |
| 9 | onRefresh() |
子类扩展点,如 ServletWebServerApplicationContext 在这里启动内嵌 Tomcat |
| 10 | registerListeners() |
注册 ApplicationListener Bean,处理已缓存的早期事件 |
| 11 | finishBeanFactoryInitialization(beanFactory) |
核心步骤:preInstantiateSingletons() 触发所有非懒加载单例的创建,即第二节讲的 doCreateBean 全流程 |
| 12 | finishRefresh() |
发布 ContextRefreshedEvent,启动实现了 Lifecycle 接口的 Bean |
第 5、6、11 步是理解整个容器机制的主干:第 5 步只操作元数据(BeanDefinition),第 6 步把"能够介入 Bean 创建过程"的处理器准备好,第 11 步才真正按元数据批量创建 Bean 实例——这个"先备好所有加工工序,再统一走流水线"的顺序,正是 BeanFactoryPostProcessor 和 BeanPostProcessor 语义差异的根源(详见 2.2 节)。
二、Bean 的创建与生命周期
要点:单例 Bean 的创建全部收敛到
AbstractAutowireCapableBeanFactory#doCreateBean这一个方法,核心步骤固定为"实例化 → 属性填充 → Aware 回调 → 初始化前置处理 → 初始化(afterPropertiesSet/init-method)→ 初始化后置处理",AOP 代理、@Autowired注入、@PostConstruct回调都不是独立的"魔法",而是各自挂在这条生命周期链路上某个固定环节的BeanPostProcessor。
2.1 doCreateBean 的核心步骤
doCreateBean 大致等价于下面这段简化伪代码(真实源码有更多异常处理和条件分支,这里只保留主干逻辑):
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 1. 实例化:选择构造器(可能涉及构造器参数解析),反射创建"裸对象"
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 2. 提前暴露:仅当"单例 + 允许循环引用 + 当前确实在创建中"时才会执行
// 把一个 ObjectFactory(还没决定要不要生成代理)放进三级缓存
boolean earlySingletonExposure = mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName);
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
// 3. 属性填充:处理 @Autowired、setter 注入、XML 里配置的 <property>
populateBean(beanName, mbd, instanceWrapper);
// 4. 初始化:Aware 回调 -> postProcessBeforeInitialization
// -> afterPropertiesSet/init-method -> postProcessAfterInitialization
exposedObject = initializeBean(beanName, exposedObject, mbd);
// 5. 注册销毁回调(DisposableBean/destroy-method/@PreDestroy)
registerDisposableBeanIfNecessary(beanName, bean, mbd);
return exposedObject;
}
initializeBean 内部则是:
protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
invokeAwareMethods(beanName, bean); // BeanNameAware/BeanClassLoaderAware/BeanFactoryAware 直接调用
Object wrappedBean = bean;
// 前置处理:ApplicationContextAwareProcessor 在这里回调 ApplicationContextAware 等接口;
// CommonAnnotationBeanPostProcessor 在这里执行 @PostConstruct
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
invokeInitMethods(beanName, wrappedBean, mbd); // InitializingBean#afterPropertiesSet -> 自定义 init-method
// 后置处理:AnnotationAwareAspectJAutoProxyCreator 在这里判断是否需要生成 AOP 代理
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
return wrappedBean;
}
几个必须记住的关键点:
addSingletonFactory发生在populateBean之前——也就是说,一个 Bean 的字段/依赖还完全没有被注入时,它的"早期引用"就已经可以被其他 Bean 拿到了。这是三级缓存能解决循环依赖的物理基础,第三节会详细展开。- AOP 代理默认在
applyBeanPostProcessorsAfterInitialization阶段生成(即所有属性填充、init-method都跑完之后),除非发生了循环依赖,代理被迫提前到getEarlyBeanReference阶段生成(同样在第三节展开)。 @PostConstruct和afterPropertiesSet/init-method不是同一个环节:@PostConstruct由CommonAnnotationBeanPostProcessor在前置处理(postProcessBeforeInitialization)阶段执行,afterPropertiesSet/init-method是initializeBean里单独的invokeInitMethods步骤,时机在前置处理之后、后置处理(AOP 代理生成)之前。
2.2 BeanPostProcessor 与 BeanFactoryPostProcessor 的本质区别
要点:两者的本质区别是作用对象——
BeanFactoryPostProcessor操作的是BeanDefinition(配置元数据,此时 Bean 还没有被实例化),BeanPostProcessor操作的是已经实例化出来的 Bean 对象;对应到refresh()的时间线,前者在第 5 步执行,后者在第 6 步注册、并在第 11 步创建每一个 Bean 时被逐个调用。
| 维度 | BeanFactoryPostProcessor | BeanPostProcessor |
|---|---|---|
| 作用对象 | BeanDefinition(元数据) |
Bean 实例(对象) |
| 调用时机 | refresh() 第 5 步,所有 Bean 实例化之前,只执行一次 |
每个 Bean 的 doCreateBean 过程中,每个 Bean 都会经历一次 |
| 能做的事 | 修改/新增 BeanDefinition(比如改属性值、动态注册新 Bean) | 包装/替换 Bean 实例(比如生成代理对象、注入依赖、执行回调) |
| 典型内置实现 | PropertySourcesPlaceholderConfigurer(解析 ${} 占位符)、ConfigurationClassPostProcessor(解析 @Configuration/@Bean,本质是子接口 BeanDefinitionRegistryPostProcessor) |
AutowiredAnnotationBeanPostProcessor(@Autowired 注入)、AnnotationAwareAspectJAutoProxyCreator(AOP 代理)、CommonAnnotationBeanPostProcessor(@Resource/@PostConstruct) |
一个常被忽略但很关键的子接口:BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor,多出一个 postProcessBeanDefinitionRegistry() 方法,能够注册全新的 BeanDefinition(而不只是修改已有的),ConfigurationClassPostProcessor 正是这个子接口的实现——这也是为什么 @ComponentScan/@Bean 能够"凭空"新增 Bean:它们不是在改已有配置,而是在这一步往容器里注册了之前根本不存在的 BeanDefinition。invokeBeanFactoryPostProcessors 内部会保证 BeanDefinitionRegistryPostProcessor 先于普通的 BeanFactoryPostProcessor 执行,逻辑上也说得通:先把"能新增哪些 Bean"确定下来,再执行"修改已有元数据"的处理器。
2.3 @Autowired 依赖注入原理
@Autowired 由 AutowiredAnnotationBeanPostProcessor 处理,它同时实现了两个接口,分别在两个不同时机介入:
MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition:在 Bean 定义合并完成后,提前扫描该 Bean 的 Class,找出所有标注了@Autowired/@Value的字段和方法,构建成InjectionMetadata并缓存起来(避免每次创建 Bean 都重新反射扫描注解,是一个典型的"元数据预解析 + 缓存"优化)。InstantiationAwareBeanPostProcessor#postProcessProperties:在populateBean阶段被调用,取出上一步缓存好的InjectionMetadata,调用其inject()方法,对每一个需要注入的字段/方法参数,委托给DefaultListableBeanFactory#resolveDependency()完成依赖查找。
resolveDependency 的查找逻辑简化描述:先按类型在容器里找候选 Bean(doResolveDependency → findAutowireCandidates),如果只有一个候选,直接返回;如果有多个候选,再按字段名/参数名做二次匹配(相当于用名字消歧义),也可以用 @Qualifier 或 @Primary 显式指定优先级;如果一个候选都没有且 required=true(@Autowired 默认值),抛出 NoSuchBeanDefinitionException。
这里有一个对理解循环依赖至关重要的事实:resolveDependency 最终会调用 getBean(dependencyBeanName)——也就是说,A 注入 B 这个动作,本质上是 A 在自己的 populateBean 阶段,反过来触发容器去"获取"(如果还没创建,就是"创建")B。正是这个"注入即触发获取/创建"的递归特性,配合下一节的三级缓存,才使得 A、B 互相依赖能够被正确解开。
三、循环依赖与三级缓存
要点:Spring 只能解决单例 Bean 之间、通过 setter/字段注入产生的循环依赖,解决手段是"先造出一个不完整的裸对象,把它的引用提前登记到缓存里,再去填充它的属性"——因为 Java 对象引用的本质是指针,只要对象已经存在于堆上,其他 Bean 拿到的引用和这个对象后续被填充完属性后是同一个对象。三级缓存存在的意义,全部指向一个目标:让 AOP 代理在真正需要提前暴露时才生成,且只生成一次、所有拿到早期引用的地方看到的是同一个代理对象。
3.1 什么是循环依赖
循环依赖指 Bean A 依赖 Bean B,同时 Bean B(直接或间接)依赖 Bean A,形成一个闭环。以最简单的两方循环为例:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
为什么构造器注入无法解决循环依赖
如果把上面的字段注入换成构造器注入:
@Component
public class A {
public A(B b) { this.b = b; }
}
@Component
public class B {
public B(A a) { this.a = a; }
}
问题出在 doCreateBean 的第一步——createBeanInstance:构造器注入要求在调用 new A(b) 之前,参数 b 必须已经是一个完整可用的对象。容器创建 A 时,走到 createBeanInstance 需要先解析构造器参数 B,于是递归去创建 B;创建 B 时同样在 createBeanInstance 阶段需要先解析构造器参数 A,于是又递归回来创建 A——但此时 A 的创建也还停留在 createBeanInstance 这一步,连裸对象都还没有,没有任何"半成品引用"可以被提前暴露出去。这个递归没有出口,Spring 会通过一个记录"当前正在创建中的 Bean 名称"的集合(singletonsCurrentlyInCreation)检测到 A 在创建过程中又被要求创建 A 自己,直接抛出 BeanCurrentlyInCreationException,而不是无限递归导致 StackOverflowError。
一句话总结:三级缓存能生效的前提是"实例化"和"属性填充"是两个独立的步骤,能在实例化完成、属性填充之前,先把这个半成品对象的引用亮出来;构造器注入把"实例化"和"依赖解析"捆绑在了同一步,没有半成品可亮。
为什么 setter 和字段注入可以解决循环依赖
setter/字段注入下,createBeanInstance 只需要调用无参(或至少不依赖 B 的)构造器,就能拿到一个裸对象——此时对象已经在堆上存在,只是字段 b 还是 null。Spring 抓住这个时间窗口,在裸对象创建完、属性还没填充之前,把"如何获取这个 Bean 早期引用"的一个工厂方法注册进三级缓存(addSingletonFactory)。
走一遍完整过程:容器创建 A → 实例化拿到裸对象 → 把 A 的 ObjectFactory 放进三级缓存 → 开始 populateBean(A),发现需要注入 B → 递归 getBean("b") 触发创建 B → B 实例化拿到裸对象 → B 的 ObjectFactory 放进三级缓存 → 开始 populateBean(B),发现需要注入 A → 调用 getSingleton("a") → 在三级缓存里找到 A 此前注册的 ObjectFactory,执行它拿到 A 的早期引用,晋升到二级缓存 → B 拿着这个"裸的/或已决定要代理的" A 引用完成自己的属性填充和初始化,B 创建完毕,晋升到一级缓存 → 回到 A 的 populateBean,顺利拿到完整的 B,A 也完成剩余的初始化流程,晋升到一级缓存。
关键在于 B 里持有的那个 A 的引用,和 A 最终创建完毕后放进一级缓存的对象,指向的是堆上同一块内存——B 构造时 A 还是空壳,但等到整个容器初始化流程走完、A 的属性也填好之后,B 里那个引用自然而然地"变成"了完整的 A,不需要任何额外的替换动作,这正是"先暴露引用、后填充内容"这个思路能成立的物理基础。
3.2 三级缓存分别是什么
三级缓存都是 DefaultSingletonBeanRegistry 这个类里的成员字段:
| 缓存 | 字段名 | 类型 | 存放的内容 |
|---|---|---|---|
| 一级缓存 | singletonObjects |
Map<String, Object> |
完全初始化好、可以直接对外使用的单例 Bean |
| 二级缓存 | earlySingletonObjects |
Map<String, Object> |
已经从三级缓存的工厂方法里"生产"出来的早期引用(可能是裸对象,也可能已经是代理对象),但这个 Bean 本身还没有走完属性填充和初始化流程 |
| 三级缓存 | singletonFactories |
Map<String, ObjectFactory<?>> |
Bean 实例化完成后立即注册的一个工厂方法,调用它才会真正产生早期引用(这一步才会决定要不要生成 AOP 代理) |
3.3 为什么二级缓存不够用
这是三级缓存里最高频、也最容易被问倒的追问点。核心矛盾在于 AOP:Spring 的设计目标是"只有真的发生循环依赖、确实需要提前暴露引用时,才去决定这个 Bean 要不要生成代理",而不是对所有 Bean 都在实例化后立刻做代理判断(大多数 Bean 根本不会陷入循环依赖,没必要为它们提前承担这个判断/生成成本,正常路径下代理仍然在 initializeBean 的 postProcessAfterInitialization 阶段——也就是属性填充和 init-method 都跑完之后——才生成)。
如果只保留"singletonObjects + singletonFactories"两级(去掉 earlySingletonObjects),会出现两个问题:
- 同一个 Bean 可能被重复决策、生成出不一致的早期引用:假如 A 被多个 Bean 循环依赖到(比如 B 和 C 都依赖 A,且都在 A 创建过程中反过来被 A 间接依赖),每次其他 Bean 调用
getSingleton("a")都要重新执行三级缓存里的工厂方法。如果没有二级缓存把"第一次执行工厂方法得到的结果"缓存下来,工厂方法(getEarlyBeanReference,内部逻辑等价于 AOP 的wrapIfNecessary)就可能被反复调用,存在生成出多个不同代理实例的风险,破坏"同一个单例 Bean 在任何地方拿到的都应该是同一个对象"这一基本契约。 - 无法和"正常流程"的代理生成时机对齐:
AnnotationAwareAspectJAutoProxyCreator内部会维护一个记录"哪些 Bean 已经被提前生成过早期引用(早期代理)"的登记(社区里通常称为earlyProxyReferences,用于在postProcessAfterInitialization阶段判断"这个 Bean 是不是已经在更早的时候,因为被循环依赖到,就已经决定过要不要代理了",如果是,直接复用早期已经生成的那个引用,不再重复包装;如果不是,才在这个正常阶段第一次做代理判断)。二级缓存的存在,本质上就是把"三级缓存工厂方法只执行一次"这件事落到实处的存储介质。
这一段"内部字段命名/
earlyProxyReferences的具体实现"是社区里被广泛引用、且逻辑自洽的解释,但字段的精确命名和实现细节建议对照本地 Spring 源码中AbstractAutoProxyCreator的具体版本复核,不同小版本可能有重构。
简化一句话:二级缓存解决的不是"能不能找到早期引用"的问题(三级缓存的工厂方法本身就能生产),而是"同一个早期引用只生产一次、之后所有人拿到的都是同一个对象"的问题——这本质上是一个"缓存计算结果,避免重复计算 + 保证结果一致性"的常规工程手段,只是这里"计算"的对象恰好是"要不要生成 AOP 代理"这个有副作用、不能重复执行的决策。
3.4 getSingleton 的查找逻辑与 addSingletonFactory 的时机
DefaultSingletonBeanRegistry#getSingleton(String beanName, boolean allowEarlyReference) 的查找顺序(伪代码简化):
public Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存:如果已经完全创建好,直接返回,不用往下走(避免不必要的加锁)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 一级缓存没有,且这个 Bean 正处于创建中(说明可能是循环依赖场景),查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// double check,避免并发场景下重复执行三级缓存的工厂方法
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 3. 二级缓存也没有,去三级缓存找工厂方法,执行它拿到早期引用
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
if (factory != null) {
singletonObject = factory.getObject(); // 这里可能触发 AOP 代理生成
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName); // 从三级缓存移除,避免重复执行
}
}
}
}
}
}
return singletonObject;
}
addSingletonFactory 的调用时机在 doCreateBean 里非常关键——它紧跟在实例化之后、属性填充之前:
// AbstractAutowireCapableBeanFactory#doCreateBean 内
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
populateBean(beanName, mbd, instanceWrapper); // 三级缓存注册完,才开始属性填充
这个先后顺序不能颠倒:如果先 populateBean 再 addSingletonFactory,等于把"暴露早期引用"这个动作推迟到属性填充之后,那循环依赖场景下需要用到早期引用的另一方(B 需要注入 A)在此之前根本找不到任何可用的 A,循环依赖也就无法被打破。
3.5 完整案例:A 与 B 循环依赖且 A 有 AOP 切面
场景:A 依赖 B(字段注入),B 依赖 A(字段注入),并且 A 上标注了 @Transactional(或者被某个自定义 @Aspect 切面匹配到),意味着 A 最终应该被注入到 B 里的是代理后的对象,而不是原始 A 对象——否则 B 调用 a.someMethod() 时事务/切面逻辑完全不会生效。假设容器按 A 先于 B 创建的顺序处理:
- 容器开始创建单例 A,
doCreateBean(A):createBeanInstance反射调用无参构造器,得到裸对象rawA(还没有被代理,字段b也还是null)。 - 因为 A 是单例、允许循环引用、且正处于创建中,执行
addSingletonFactory("a", () -> getEarlyBeanReference("a", mbd, rawA)),把这个 lambda(还没执行)放进三级缓存singletonFactories。注意此时并没有立刻决定 A 要不要代理,只是把"将来需要时怎么决定"这件事包装成了一个待执行的工厂方法。 - 开始
populateBean(A),AutowiredAnnotationBeanPostProcessor发现 A 需要注入字段b,于是调用resolveDependency→ 最终getBean("b"),容器开始创建单例 B。 doCreateBean(B):createBeanInstance得到裸对象rawB,同样把 B 的ObjectFactory注册进三级缓存。populateBean(B),发现 B 需要注入字段a,调用getBean("a")→ 内部走到getSingleton("a", true):一级缓存没有 A(还没创建完),二级缓存也没有,于是去三级缓存找到第 2 步注册的工厂方法并首次执行——调用getEarlyBeanReference("a", mbd, rawA)。getEarlyBeanReference内部会遍历所有实现了SmartInstantiationAwareBeanPostProcessor的处理器,其中就包括AnnotationAwareAspectJAutoProxyCreator:它检查 A 是否匹配任何切面的Pointcut(这里因为@Transactional命中了内置的事务 Advisor),发现命中,于是提前在这里生成了 A 的 CGLIB/JDK 代理对象proxyA,并做好登记(标记"A 已经被提前代理过,正常流程里不要再代理一次")。这个proxyA被返回、放入二级缓存earlySingletonObjects,同时从三级缓存移除。getBean("a")最终把proxyA返回给 B 的属性填充逻辑,B.a = proxyA。B 后续的初始化流程正常走完(假设 B 自身不需要被代理),B 创建完毕,放入一级缓存。- 回到第 3 步的调用栈,A 拿到完整创建好的
B(B.a字段已经是proxyA),A.b = B赋值完成,populateBean(A)结束。 initializeBean(A)继续走 Aware 回调、前置处理、init-method,最后到applyBeanPostProcessorsAfterInitialization:AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization检查到 A 已经在第 6 步被提前代理过(命中之前的登记),于是跳过重复代理,直接把已有的proxyA作为 A 的最终产出。- A 创建完毕,
proxyA被放入一级缓存singletonObjects(同时清理二级、三级缓存里关于 A 的残留)。
最终结果:容器里唯一的 A 单例就是 proxyA,B 里持有的 a 字段也是同一个 proxyA——如果没有三级缓存这套"提前决策 + 登记 + 复用"的机制,一种可能的错误结果是 B 里注入的是没被代理的 rawA(导致 B 调用 A 方法时事务/切面完全不生效),另一种可能的错误结果是 A 被代理了两次、B 手里的代理对象和容器最终对外提供的 A 并不是同一个实例(同一个"单例"出现了两个不同对象,破坏单例语义)。三级缓存通过"只在真正需要早期引用时才决策代理,决策一次后登记复用",把这两种错误都规避掉了。
四、AOP 原理
要点:Spring AOP 是运行期基于代理的 AOP(区别于 AspectJ 编译期/类加载期的字节码织入),核心链路是:
AnnotationAwareAspectJAutoProxyCreator作为BeanPostProcessor在 Bean 初始化后期判断是否需要代理 →DefaultAopProxyFactory决定用 JDK 动态代理还是 CGLIB → 代理对象把匹配到的多个Advisor转换成统一的MethodInterceptor链 →ReflectiveMethodInvocation#proceed()通过递归调用把链上的每个拦截器像"洋葱"一样嵌套执行。
4.1 JDK 动态代理与 CGLIB 的选择逻辑
选择逻辑收敛在 DefaultAopProxyFactory#createAopProxy(AdvisedSupport config),简化后大致是:
public AopProxy createAopProxy(AdvisedSupport config) {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config); // 目标本身就是接口/已经是JDK代理,只能走JDK代理
}
return new ObjenesisCglibAopProxy(config); // 用 CGLIB 生成目标类的子类
}
return new JdkDynamicAopProxy(config); // 默认:有接口就用 JDK 代理
}
也就是说:默认情况下"目标类实现了接口"就用 JDK 动态代理,没有接口就必须用 CGLIB;如果显式配置 proxyTargetClass=true,则强制使用 CGLIB(前提是目标类不是 final)。
一个非常容易被忽视、但生产上很常见的事实:Spring Boot 从 2.x 开始,spring.aop.proxy-target-class 默认值是 true(由 AopAutoConfiguration 设置),也就是说大多数 Spring Boot 应用里,即使 Bean 实现了接口,实际生成的也是 CGLIB 代理,而不是传统印象里"有接口优先 JDK 代理"。这么设计是为了行为一致性:如果同一个应用里一部分 Bean 因为没接口用了 CGLIB、另一部分因为有接口用了 JDK 代理,会导致"能不能按具体实现类类型注入"这件事变得不可预测(JDK 代理只能被注入到接口类型的字段,注入到实现类类型字段会报 ClassCastException),统一用 CGLIB 可以规避这个不一致。
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理方式 | 基于接口,运行时生成实现了同一组接口的代理类 | 基于继承,运行时生成目标类的子类并重写方法 |
| 前提条件 | 目标类必须实现至少一个接口 | 目标类和方法不能是 final(final 类/方法无法被子类重写) |
| 能否代理到无接口方法 | 不能,代理对象只能强转成接口类型 | 能,代理对象是目标类的子类,可以直接按目标类类型使用 |
| Spring Boot 默认行为 | 仅在显式关闭 proxy-target-class 时使用 |
默认(spring.aop.proxy-target-class=true) |
4.2 AnnotationAwareAspectJAutoProxyCreator 如何介入
AnnotationAwareAspectJAutoProxyCreator 继承自 AbstractAutoProxyCreator,本质是一个 BeanPostProcessor(准确说是子接口 SmartInstantiationAwareBeanPostProcessor),在容器启动的 registerBeanPostProcessors 阶段(1.3 节第 6 步)就已经被注册为可用的处理器。它的核心逻辑分布在两个方法上:
postProcessAfterInitialization(正常路径,绝大多数 Bean 走这里):调用内部的wrapIfNecessary(bean, beanName, cacheKey):- 如果这个 Bean 之前已经在
getEarlyBeanReference阶段被判断过(说明发生过循环依赖,见 3.5 节),直接跳过,返回已有结果。 - 调用
getAdvicesAndAdvisorsForBean()找出所有匹配该 Bean 的Advisor——AnnotationAwareAspectJAutoProxyCreator的"AnnotationAware"体现在这里:除了容器里显式声明的AdvisorBean,它还会通过BeanFactoryAspectJAdvisorsBuilder扫描所有标注了@Aspect的 Bean,用ReflectiveAspectJAdvisorFactory把里面的@Before/@After/@Around/@AfterReturning/@AfterThrowing方法解析成一个个Advisor。 - 如果没有任何匹配的
Advisor,直接返回原始 Bean(这也是为什么没有被任何切面命中的 Bean,压根不会被代理,getBean()拿到的就是原始类型)。 - 如果有匹配的
Advisor,构造ProxyFactory,把匹配到的Advisor加进去,调用getProxy(),内部委托给 4.1 节的DefaultAopProxyFactory生成最终的代理对象。
- 如果这个 Bean 之前已经在
getEarlyBeanReference(仅循环依赖场景触发,见 3.5 节第 6 步):逻辑和wrapIfNecessary基本一致,只是触发时机更早,且会额外做登记,防止后续postProcessAfterInitialization重复代理。
4.3 Advisor Advice Pointcut 与拦截器链
三个核心概念的关系是"Advice 回答做什么,Pointcut 回答在哪做,Advisor 把两者绑在一起":
| 概念 | 职责 | 典型实现 |
|---|---|---|
Advice |
横切逻辑本身("做什么"),AOP Alliance 标准接口 MethodInterceptor 是其中一种(环绕型),invoke(MethodInvocation) |
MethodBeforeAdviceInterceptor、AspectJAroundAdvice 等 |
Pointcut |
匹配规则("在哪做"),由 ClassFilter(类级过滤)+ MethodMatcher(方法级过滤)组成 |
AspectJExpressionPointcut(解析 execution(...) 等切点表达式) |
Advisor |
Pointcut + Advice 的组合("在哪做 + 做什么") |
DefaultPointcutAdvisor、AspectJExpressionPointcutAdvisor |
@Aspect 类里用 @Before/@Around 等注解标注的方法,会被 ReflectiveAspectJAdvisorFactory 统一适配、包装成实现了 MethodInterceptor 的对象(比如 @Before 方法被包装进 AspectJMethodBeforeAdvice,再经由 MethodBeforeAdviceInterceptor 适配成标准的环绕式拦截器),这样不管原始注解是哪一种,进入拦截器链之后统一都是同一种"环绕式"接口,链式调用逻辑可以完全一致地处理它们。
拦截器链的串联逻辑:代理对象的方法被调用时(JDK 代理走 JdkDynamicAopProxy#invoke,CGLIB 走 CglibAopProxy.DynamicAdvisedInterceptor#intercept),都会执行大致相同的逻辑:
// 简化伪代码
List<Object> chain = advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
return invocation.proceed();
ReflectiveMethodInvocation#proceed() 的核心是一个基于游标的递归调用:
public Object proceed() throws Throwable {
// currentInterceptorIndex 初始为 -1
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint(); // 链走到头,反射调用真正的目标方法
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
MethodInterceptor interceptor = (MethodInterceptor) interceptorOrInterceptionAdvice;
return interceptor.invoke(this); // 把自己(this)传给下一个拦截器
}
每个 MethodInterceptor 的 invoke(MethodInvocation invocation) 实现里,典型写法是"做一些前置逻辑 → 调用 invocation.proceed()(继续驱动链条往下走)→ 做一些后置/异常处理逻辑",这正是"环绕通知"的原理:因为每一层拦截器都是先执行自己的前半部分逻辑,再主动调用 proceed() 把控制权交给下一层,等下一层(以及更下面所有层,直至真正的目标方法)全部返回后,才继续执行自己的后半部分逻辑,多个切面叠加时天然形成"外层先进入、内层先返回"的洋葱模型,执行顺序由 Advisor/@Aspect 的 @Order 或 Ordered 接口决定链上的先后位置。
4.4 @Transactional 作为 AOP 的典型应用
@Transactional 的执行入口是 TransactionInterceptor(实现了 MethodInterceptor,作为一个特殊的 Advice 被 BeanFactoryTransactionAttributeSourceAdvisor 包装成 Advisor,参与 4.3 节的拦截器链),核心逻辑在基类 TransactionAspectSupport#invokeWithinTransaction():
- 通过
TransactionAttributeSource(典型实现AnnotationTransactionAttributeSource)解析当前方法/类上的@Transactional属性(隔离级别、传播行为、超时、rollbackFor等)。 createTransactionIfNecessary()→ 委托给PlatformTransactionManager#getTransaction(definition),这一步是传播行为真正生效的地方:AbstractPlatformTransactionManager通过doGetTransaction()检查当前线程(TransactionSynchronizationManager内部基于ThreadLocal保存当前线程绑定的数据库连接等资源)是否已经处于某个事务中,再按Propagation枚举值分别处理(是否要求已存在事务、是否要开启新事务、是否要挂起已有事务等)。- 调用
invocation.proceed()(回到 4.3 节的链式调用)真正执行业务方法。 - 方法正常返回:
commitTransactionAfterReturning()提交事务(除非被标记为 rollback-only)。 - 方法抛出异常:
completeTransactionAfterThrowing()先调用rollbackOn(ex)判断这个异常是否满足回滚条件(默认只对RuntimeException和Error回滚,受检异常默认不回滚,除非显式配置了rollbackFor),满足则回滚,不满足则仍然提交。 cleanupTransactionInfo()在finally里恢复调用前的事务上下文(支持事务方法嵌套调用的场景)。
REQUIRES_NEW 为什么要挂起当前事务
PROPAGATION_REQUIRES_NEW 语义是"不管当前有没有事务,都新开一个完全独立的物理事务,和外层事务的提交/回滚互不影响"。要做到"互不影响",就必须先把外层事务从当前线程上摘下来:AbstractPlatformTransactionManager#suspend() 会把 TransactionSynchronizationManager 里当前线程绑定的资源(比如 JDBC 场景下的 ConnectionHolder)以及已注册的 TransactionSynchronization 全部解绑,打包进一个 SuspendedResourcesHolder;随后 startTransaction() 开启一个全新的物理事务(对 JDBC 而言意味着获取一个新的数据库连接,重新设置 autoCommit=false)。内层方法在这个全新事务里独立提交或回滚,互不牵连外层;内层事务结束后,resume() 把之前挂起的 SuspendedResourcesHolder 重新绑定回当前线程,外层事务才继续往下走。
这也解释了一个常见误区:REQUIRES_NEW 的内层事务提交后,如果外层事务后续回滚,内层已提交的数据不会被撤销——因为内层事务在数据库层面就是一次完全独立、已经 COMMIT 的物理事务,不存在"跟着外层一起回滚"这回事;反过来外层事务如果最终提交,也不会因为内层曾经存在过而受到任何额外影响,两者是完全解耦的两次物理事务。这个特性常被用在"无论主流程成不成功,这条操作日志/审计记录都必须落库"这类场景。
4.5 AOP 失效的经典场景
| 失效场景 | 根本原因 |
|---|---|
同一个类内部方法调用(this.otherMethod()) |
Spring AOP 基于代理,只有从代理对象的入口调用才会经过拦截器链;this 指向目标对象本身,同类内部调用是普通的 Java 方法调用,完全绕开了代理 |
方法是 private/static/final |
CGLIB 靠生成子类重写方法实现代理,private/static 子类不可见/不适用重写语义,final 禁止重写;JDK 代理基于接口,private 方法压根不在接口定义里,两种代理方式都覆盖不到 |
方法不是 public |
Spring AOP(尤其是声明式事务)官方约定只支持在 public 方法上生效,非 public 方法即便被切点表达式匹配到,实际处理上也可能不生效,具体表现建议对照所用版本文档核实 |
目标对象不是 Spring 管理的 Bean(比如手动 new 出来的) |
没有经过容器的 doCreateBean/BeanPostProcessor 流程,自然不会被 AnnotationAwareAspectJAutoProxyCreator 处理,也就没有代理 |
异步方法内部调用(@Async 同理) |
原理与"同类内部调用"完全一致,本质都是没有经过代理入口 |
业务代码自己 try-catch 吞掉了异常、没有重新抛出 |
不是"代理没生效",而是 TransactionInterceptor 的 completeTransactionAfterThrowing 根本感知不到异常,自然按"正常返回"处理,事务照常提交;这是最容易被误判为"AOP/事务失效"、实际上是业务代码问题的场景 |
通用解决思路是想办法让调用经过代理对象,而不是直接调用目标对象自身:常见手法包括把需要被代理的方法拆分到另一个 Bean 里、通过 ApplicationContext/@Lazy 注入自身的代理引用后调用、或者开启 exposeProxy=true 后在方法内用 AopContext.currentProxy() 显式拿到当前代理对象再调用。
五、常见面试高频问题清单
要点:这一节是前四节原理的复用,每道题都能在前面找到对应的原理支撑,回答时建议先给结论、再补一句"为什么",避免只背结论。
5.1 Spring Bean 默认单例,是否线程安全
不安全,且"单例"和"线程安全"是两个不同维度的问题:Spring 只保证同一个 Bean 名称在容器里只有一个实例,不会对这个实例的任何方法做同步包装。如果 Bean 是无状态的(没有可变的实例字段,典型如只注入了其他单例依赖、方法内只使用局部变量的 Service/DAO),并发调用天然安全;如果 Bean 内部有可变的实例字段(比如一个计数器、一个非线程安全的 SimpleDateFormat),并发访问就会有线程安全问题,需要业务自己解决(改用局部变量、ThreadLocal、加锁、或者把作用域改成 prototype)。
5.2 @Component 和 @Bean 的区别
@Component(及派生的 @Service/@Repository/@Controller)标注在类上,通过组件扫描被发现,由容器反射调用构造器完成实例化,适用于自己能修改源码的类;@Bean 标注在方法上(通常在 @Configuration 类里),方法体本身就是创建逻辑的完整定义(可以是手写 new、调用第三方 SDK 的构造方式等),适用于没有源码修改权限的第三方类(比如注册一个 RestTemplate、某个云厂商 SDK 的 Client),或者创建过程本身需要一些代码逻辑(条件判断、参数拼装)。两者最终都会被解析成 BeanDefinition(详见 1.2 节),差异只体现在"如何产生实例"这一步:反射构造器 vs 工厂方法调用。
5.3 @Autowired 和 @Resource 的区别
| 维度 | @Autowired | @Resource |
|---|---|---|
| 来源 | Spring 自有注解 | JSR-250 标准注解(Spring 6/Jakarta EE 体系下包名为 jakarta.annotation.Resource) |
| 默认匹配策略 | 按类型(byType),多个候选再按字段名/参数名兜底,可配合 @Qualifier 精确指定 |
按名称(byName,默认取字段名或 name 属性),找不到同名再退化按类型查找 |
| 处理的 BeanPostProcessor | AutowiredAnnotationBeanPostProcessor |
CommonAnnotationBeanPostProcessor(同时也处理 @PostConstruct/@PreDestroy) |
| 可选注入 | required=false 显式声明 |
没有直接等价属性,一般结合业务逻辑自行判断 |
| 可移植性 | 绑定 Spring 框架 | 标准注解,理论上可移植到其他支持 JSR-250 的容器 |
生产上 @Autowired 更常用(Spring 原生、IDE 支持更完善),但如果团队希望减少对具体框架注解的依赖,或者明确需要按名称注入,@Resource 是更合适的选择。
5.4 三级缓存能不能改成两级
能,但会牺牲设计目标——这是本文 3.3 节的结论复用。如果去掉三级缓存(singletonFactories),只保留一级和二级,就必须在 Bean 刚实例化完、还没做属性填充时立刻决定它最终要不要生成 AOP 代理,并把(可能已经是代理的)早期引用直接放进二级缓存;这等于把本该在 postProcessAfterInitialization(所有 Bean 都要经过的正常收尾阶段)才做的代理判断,无差别提前到所有 Bean 身上,即便这个 Bean 根本不会陷入循环依赖,也要提前承担这个决策的代价。三级缓存用一个"延迟执行的工厂方法"把这个决策推迟到真正需要早期引用的那一刻才触发,绝大多数不涉及循环依赖的 Bean 完全用不到这个工厂方法,代理仍然按最自然的时机(初始化完成后)生成——这才是设计成三级而不是两级的真正原因。
5.5 BeanFactoryPostProcessor 和 BeanPostProcessor 各举例
BeanFactoryPostProcessor:PropertySourcesPlaceholderConfigurer(解析 Bean 定义里${...}形式的占位符)、ConfigurationClassPostProcessor(其子接口BeanDefinitionRegistryPostProcessor的实现,负责解析@Configuration/@ComponentScan/@Bean,是注解驱动配置能落地的核心)。BeanPostProcessor:AutowiredAnnotationBeanPostProcessor(处理@Autowired/@Value注入)、AnnotationAwareAspectJAutoProxyCreator(AOP 代理生成)、CommonAnnotationBeanPostProcessor(处理@Resource/@PostConstruct/@PreDestroy)。
5.6 Spring 事务失效的常见场景
汇总 4.5 节的通用 AOP 失效场景,加上事务特有的几种:
- 方法非
public、同类内部调用、@Transactional标在接口方法上而实际用的是 CGLIB 代理(这类场景本质都是"没有经过代理入口",见 4.5 节)。 - 抛出的是受检异常,但没有配置
rollbackFor——TransactionInterceptor默认只对RuntimeException/Error回滚(见 4.4 节第 5 步)。 - 业务代码自己
try-catch把异常吞掉,没有重新抛出,TransactionInterceptor感知不到异常,按正常返回处理,事务照常提交。 - 数据库存储引擎本身不支持事务(如 MySQL 的 MyISAM 引擎),这是数据库层面的限制,和 Spring 无关。
- 方法内部手动开线程执行数据库操作——
@Transactional依赖ThreadLocal绑定当前线程的数据库连接,另起的线程拿到的是一条新连接,不在同一个事务范围内。 - 事务传播行为配置不当,比如某个本应独立提交(
REQUIRES_NEW)的操作被写成默认的REQUIRED,跟随外层事务一起被回滚,或者反过来本应参与外层事务的操作被误配成REQUIRES_NEW,导致部分数据提交、部分数据因为外层回滚而不一致。
结语:面试回答的通用框架
回答 Spring 相关问题时,比较稳妥的表达顺序和 Dubbo/RPC 类问题是一致的:这个机制要解决什么具体问题 → Spring 给出的默认方案/实现思路是什么,具体经过哪些类/方法 → 这个方案有什么局限或适用边界 → 生产上遇到这个局限时该怎么权衡取舍。
比如被问"讲讲循环依赖",不要只回答"用三级缓存解决",而是完整讲清楚:循环依赖是 A 依赖 B、B 又依赖 A 形成的闭环 → 构造器注入因为实例化和依赖解析绑死在一步没法解决,setter/字段注入可以把实例化和属性填充拆成两步,先暴露半成品引用 → 三级缓存的核心不是"多存一份数据",而是把"要不要生成 AOP 代理"这个有副作用的决策,延迟到真正发生循环依赖时才触发、且只触发一次 → 如果只用两级缓存,等于牺牲了"只在必要时才决策代理"这个设计目标,所有 Bean 都要提前承担决策成本——这个"是什么 → 怎么做 → 为什么这么做 → 不这么做会怎样"的框架,同样适用于 AOP 代理选型、BeanPostProcessor 扩展点、事务传播行为等几乎所有 Spring 核心面试题。

浙公网安备 33010602011771号