1. 先说清楚这篇要聊什么
有朋友私信我说,上一篇文章看完之后,手写了一个最简单的Spring Demo,Bean的定义、依赖注入、配置文件这些概念都有印象了,但是一到面试题或者看源码,脑子里还是乱的。尤其是“三级缓存”、“循环依赖”、“Bean的生命周期”这几个词,听着都熟,但让我讲清楚它到底是什么、为什么要这么设计,我就卡住了。
这篇就是来解决这个问题的。
我会从“循环依赖”这个最常见的痛点切入,把Spring IoC容器里Bean的创建过程掰开了讲清楚。读完这篇,你再去看那些源码分析文章,会发现它们说的每一句话你都能对上号了。这篇适合已经会用Spring写简单项目、但对原理还停留在“背概念”阶段的同学。内容会偏底层一些,但我尽量用大家都能听懂的方式来说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 循环依赖是什么,为什么Spring要费劲去解决它
2.1 用一个实际场景引出循环依赖
先看一个最简朴的例子。你在写一个订单系统,OrderService需要调用UserService查询用户信息,而UserService又把OrderService作为方法回调注入进来。代码长这样:
java复制@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder() {
userService.getUserById(1L);
}
}
@Service
public class UserService {
@Autowired
private OrderService orderService;
public void getUserById(Long id) {
// ...
}
}
这就是典型的循环依赖:A需要B,B又需要A。
如果你用构造器注入,ApplicationContext一启动就会报错,提示BeanCurrentlyInCreationException。但如果你用的是字段注入(就像上面这样),项目能正常启动,Spring会绕过这个难题,把两个Bean都创建好。
关键在于:Spring到底是怎么做到的?它为什么就能处理字段注入的循环,处理不了构造器注入的循环?
2.2 常规思路下的死循环问题
假设你是Spring的作者,要创建A和B这两个Bean。最简单的思路是:先创建A,发现A需要B,就去创建B;创建B的时候又发现B需要A,于是再回到创建A……这不就成了“先有鸡还是先有蛋”的死循环了吗?
除非你换一个角度:**我不一定非要先把A完整创建好,才能开始创建B。**只要A在创建早期就把自己的“半成品”暴露出来,让B能拿到这个半成品去注入,等B创建完了,A再继续完成剩下的初始化,不就能解开这个圈了吗?
这个思路,翻译成Spring的实现,就是那三级缓存。
2.3 三级缓存的整体结构与设计初衷
Spring的DefaultSingletonBeanRegistry里面维护了三个Map,这就是传说中的三级缓存。它们各自的职责如下:
| 缓存层级 | 名称 | 存放内容 | 解决的问题 |
|---|---|---|---|
| 一级缓存 | singletonObjects | 创建完成的完整Bean | 保证单例Bean在整个容器中只存在一份,后续直接获取 |
| 二级缓存 | earlySingletonObjects | 提前暴露的早期Bean(可能是原始对象,也可能是代理对象) | 供其他Bean在依赖注入时拿到“半成品”的稳定引用 |
| 三级缓存 | singletonFactories | ObjectFactory工厂 | 延迟生成代理对象,保存Bean的早期引用,支持AOP场景 |
只从表格看,你可能会觉得一级缓存不就很够用了吗?为什么还要二级缓存和三级缓存?别急,下面我把每个缓存的作用通过源码流程拆开讲。
3. 三级缓存的核心机制拆解
3.1 Bean的创建过程分几步
一个普通单例Bean的创建流程,简化来看是这样的:
- 实例化:通过构造器创建一个原始Java对象;
- 属性填充:把@Autowired、@Resource这些依赖逐个注入进去;
- 初始化:执行各种BeanPostProcessor,包括AOP代理生成、@PostConstruct回调等等。
正常情况下,这三步一气呵成,走完就放进一级缓存。但遇到循环依赖时,Spring会在第1步和第2步之间插入一个关键动作:把当前这个半成品对象打包成一个ObjectFactory,放到三级缓存里。
这一步就是整个设计里最魂的地方。我用代码来描述一下Spring实际做事的顺序:
java复制// 伪代码,展示实例化后的关键步骤
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 第1步:实例化
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// 关键动作:提前暴露单例工厂
// 这里不是直接把对象放进二级缓存,而是放进一个ObjectFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 第2步:属性填充
populateBean(beanName, mbd, instanceWrapper);
// 第3步:初始化
exposedObject = initializeBean(beanName, exposedObject, mbd);
return exposedObject;
}
注意addSingletonFactory这一步,它接收的不是Bean本身,而是一个Lambda表达式。换句话说,Spring在此时并不知道这个Bean最终会被谁依赖,也不知道需不需要做代理,它只是先把“生成早期引用”的能力注册到了三级缓存里。真正调用这个Lambda的时机,是在别的Bean依赖它的时候。
3.2 为什么不能只有一级缓存
假设Spring没有二级缓存也没有三级缓存,只在创建完成后把Bean放进一级缓存。那A创建到一半,B来要A的时候,一级缓存里根本没有A,B的创建就会失败。
所以一定要有一个“提前暴露”的机制。一级缓存管“完成的Bean”,提前暴露的Bean得单独放一个地方,这就是二级缓存存在的基础理由。
那为什么有了二级缓存还不够,还要三级缓存?这就涉及到AOP了。
3.3 二级缓存做不到的事:AOP代理的时机问题
设想一个更复杂的场景:A依赖B,B依赖A,而且A被AOP切面代理了,最终容器里需要的A是一个代理对象,不是原始对象。
如果只靠二级缓存,Spring可以在实例化A之后,立刻判断A需不需要被代理,如果需要就马上生成代理,放进二级缓存。这样B注入A的时候,拿到的是代理对象A,看起来也说得通。
但问题是:**如果系统里根本没有发生循环依赖呢?**A实例化之后,B不依赖它,那A什么时候需要被代理?按照Spring的设计,是在initializeBean阶段,执行BeanPostProcessor的postProcessAfterInitialization方法时,才真正生成AOP代理。
如果只靠二级缓存,Spring就必须在实例化阶段就提前生成所有可能的代理对象,这会带来两个问题。
第一个问题是浪费:大量的代理对象会被提前创建,但根本没人依赖它们,白白消耗内存和CPU。
第二个问题是顺序:AOP代理的生成依赖很多通知器、切面判断,在实例化阶段就做这件事,会破坏Spring原有初始化流程的顺序性。
所以Spring的策略是:**先不确定是否需要代理,把判断逻辑延迟到“有人真正来要”的时候。**这个延迟操作就是三级缓存里的ObjectFactory。当一个Bean被循环依赖时,另一个Bean来索取引用,Spring才会执行这个工厂方法,在工厂方法里调用getEarlyBeanReference,去判断并生成AOP代理。
这就是三级缓存存在的意义。总结一句话:**一级缓存存成品,二级缓存存被提前暴露的早期引用,三级缓存存的是“如何生成早期引用”的工厂。**三级缓存是为了把AOP代理的创建时机尽量推迟。
3.4 提前引用与最终引用的差异处理
这里还有一个细节容易被忽略。当getEarlyBeanReference生成了AOP代理对象,把它放进了二级缓存,这个代理对象被B注入。等到A自己继续走完初始化流程,在initializeBean里又执行了一遍postProcessAfterInitialization,此时会不会再生成一个不同的代理对象?两个代理对象就冲突了?
Spring对这个问题做了保护。在doCreateBean的最后,有一小段逻辑:
java复制// 伪代码:初始化完成后,确认最终暴露的对象
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
// 如果提前引用的对象和初始化完成后的对象是同一个,
// 说明没有生成新的代理,直接使用提前暴露的引用
exposedObject = earlySingletonReference;
}
}
}
这段代码的意思很直白:如果早期就暴露过A,而且初始化阶段没有让A的引用发生改变,那就用早期暴露的那个对象作为最终对象。如果初始化阶段生成了新的代理对象,那就用新的代理对象。这一层判断保证了容器在任何时候拿到的都是同一个引用,不会出现一个Bean有两个实例的情况。
4. 源码级别追踪完整调用链
4.1 getSingleton方法与三个缓存的读写
三级缓存的操作都发生在DefaultSingletonRegistry里。看这个类的核心方法,一切就很清楚了。
java复制// DefaultSingletonBeanRegistry 核心代码简化版
/** 一级缓存:存放完整的单例Bean */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 三级缓存:存放ObjectFactory,用于提前生成Bean的早期引用 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** 二级缓存:存放提前暴露的早期Bean */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
当你调用getBean的时候,最终会走到这个getSingleton方法:
java复制protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先查一级缓存,有就直接返回
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 一级缓存没有,查二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 二次确认,避免并发问题
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 从三级缓存拿到ObjectFactory,执行它的getObject方法
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 生成完成后,从三级缓存移除,放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
这个流程有几个很关键的细节,值得逐一说明。
第一个细节是isSingletonCurrentlyInCreation这个方法。它不是随便就能从二级缓存、三级缓存拿数据的,前提是当前这个Bean正在创建中。这个判断很重要,它保证了正常查Bean的时候,一级缓存没有就直接走创建流程,不会误用半成品。
第二个细节是synchronized锁。三个缓存不是普通的HashMap,而是ConcurrentHashMap加synchronized的组合。在从三级缓存升级到二级缓存的过程中,Spring加了一把锁,防止多个线程同时执行同一个ObjectFactory,避免重复生成代理对象。
第三个细节,也是很多人理解偏的地方:二级缓存里的早期Bean,会被移出**三级缓存,但还会再经历一次完整的属性填充和初始化。**它只是提前把引用给了别人,自己该走的流程一步都不会少。
4.2 属性填充阶段如何触发循环依赖解决
在AbstractAutowireCapableBeanFactory的populateBean方法里,Spring会解析当前Bean的所有依赖,逐个调用getBean去获取依赖的Bean。以字段注入为例,AutowiredAnnotationBeanPostProcessor在postProcessProperties方法中,会为每个被@Autowired标注的字段调用beanFactory.resolveDependency。
resolveDependency最终会调用getBean(beanName),从而进入上面说的getSingleton流程。这时候,如果这个被依赖的Bean还在创建中,就会触发三级缓存逻辑,拿到早期引用。
换句话说,**三级缓存解决循环依赖的时机,就发生在属性填充阶段,发生在某个Bean调用getBean获取依赖的那一刻。**这不是一个事先安排好的“解圈”动作,而是getSingleton在运行时自然产生的效果。
4.3 一个完整的三级缓存流转例子
这里我放一个完整的执行时序,大家对照着看会清晰很多。假设有三个Bean,A依赖B,C依赖A,而且C依赖A的时候A还在创建中。
- 创建A:实例化完成,A还不是完整对象,先把ObjectFactory放进三级缓存;
- 填充A的属性,发现需要B,调用getBean(B);
- 创建B:实例化完成,B的ObjectFactory也放进三级缓存,填充B的属性,发现需要A;
- 调用getBean(A)查A:一级缓存没有,二级缓存没有,三级缓存里有A的ObjectFactory,执行它,可能生成代理对象,然后放进二级缓存;
- B拿到A的早期引用,B的属性填充完成,B走完初始化,被放进一级缓存;
- 回到A的创建流程,A从getBean(B)返回,继续填充剩下的属性,走完初始化,放进一级缓存。
整个过程,A虽然中途被“借走”过一次,但它最终还是完整创建好了,并且在放在一级缓存里的那个A,和B拿到的那个A是同一个引用。
4.4 源码之外的直观验证思路
如果上面这些文字还是有点抽象,我教你一个非常朴素的验证方法。在启动类里加一个ApplicationRunner,然后从容器里拿A和B的引用,再从B里面反射取出它的A依赖,比较这三个对象是否相同。如果三级缓存的逻辑是完备的,这三者必然指向同一个对象。
java复制@Component
public class CircularDependencyCheckRunner implements ApplicationRunner {
@Autowired
private ApplicationContext context;
@Override
public void run(ApplicationArguments args) throws Exception {
OrderService orderService = context.getBean(OrderService.class);
UserService userService = context.getBean(UserService.class);
Field field = UserService.class.getDeclaredField("orderService");
field.setAccessible(true);
OrderService injectedOrderService = (OrderService) field.get(userService);
System.out.println("容器中的OrderService: " + System.identityHashCode(orderService));
System.out.println("UserService内部注入的OrderService: " + System.identityHashCode(injectedOrderService));
System.out.println("两者是否相同: " + (orderService == injectedOrderService));
}
}
这段代码用System.identityHashCode打印了两个对象的内存标识。如果三级缓存生效,打印出来的两个数字是相同的。
5. 为什么构造器注入和原型Bean无法解决循环依赖
5.1 构造器注入的循环依赖为什么一定报错
原因其实很简单:构造器注入发生在实例化阶段,而这个阶段Bean还没有真正创建出来。你想想看,要调用A的构造器,首先要能拿到B的实例;但B又需要调用A的构造器,才能创建A。两个Bean都卡在“实例化”这步,谁都没办法提前暴露自己,三级缓存根本来不及起效。
代码上看得更清楚。Spring在createBeanInstance的时候会调用构造器,而构造器参数里的依赖是通过getBean去获取的。此时doCreateBean才走了第一步,addSingletonFactory还没执行,三级缓存里什么都没有,自然无法解决。
所以,如果你的项目里出现了BeanCurrentlyInCreationException,排查方向很明确:先看是不是用了构造器注入形成了循环依赖。如果是,代码结构调整比任何配置都有用。
5.2 原型Bean的循环依赖为什么也解决不了
原型Bean的作用域和单例不同,每次getBean都会创建一个新实例。Spring从一开始就没打算缓存它们,也没有任何一级二级三级缓存。对于原型Bean来说,一旦发生A依赖B、B依赖A的情况,就是单纯的递归调用,堆栈溢出是必然结果。
你如果看一下AbstractBeanFactory的doGetBean方法,会发现里面有一段这样的逻辑检测:
java复制if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
在创建原型Bean之前,Spring会先检查当前线程是否正在创建同名的原型Bean。是的话直接抛异常,避免无休止的递归。这个设计说明Spring对“什么能解、什么不能解”边界非常明确。
5.3 是设计缺陷还是合理取舍
有些人把“构造器注入无法解决循环依赖”看作Spring的缺陷,我倒不这么认为。这更像是设计上的有意取舍。构造器注入本身强调的是依赖必须在对象创建时就完全就绪,如果允许循环依赖,反而破坏了构造器注入的初衷,也让代码的依赖关系变得更加难以梳理。
循环依赖本质上是一种设计味道比较重的代码结构。Spring提供了字段注入这种“后门”来兜底,但并不是在鼓励你滥用它。该重构的时候还是要重构。
6. 实际项目里循环依赖怎么处理
6.1 优先考虑代码层重构
如果你只是在一个小Demo里面遇到循环依赖,最直接的办法就是调整依赖方向。把A和B相互依赖改成一个单向依赖,通常可以把公共的逻辑抽到一个新的Service里,让A和B都依赖这个新的Service,而不是互相依赖。
还有一种常见情况是,A依赖B只是为了调用B的一个方法,而这个方法其实可以移到A自己身上,或者通过事件机制来解耦。这类重构并不复杂,但能把依赖关系理干净,长期来看收益很大。
6.2 使用@Lazy注解打破循环
当你不打算重构,或者循环依赖的点比较分散时,可以在任一边的注入上加上@Lazy注解。
java复制@Service
public class OrderService {
@Lazy
@Autowired
private UserService userService;
}
@Lazy的作用是让Spring在这里创建一个代理对象注入进来,而不是立即去创建UserService的真实实例。等到真正调用userService的方法时,代理对象才去容器里获取真实Bean。这样就在依赖注入阶段斩断了循环。
不过使用@Lazy要注意一点,它会在启动阶段延迟Bean的创建,如果被延迟的Bean在启动期间就被其他地方触发创建,那和普通的循环依赖解决路径会有一点差异,但通常不会出问题。我见过不少项目用@Lazy临时应对循环依赖,但它会让依赖关系变得更隐晦,还是建议有限度地使用。
6.3 Spring Boot 2.6以后默认禁止循环依赖,说明了什么
Spring Boot 2.6版本做了一件很有态度的事情:默认禁止循环依赖。启动的时候如果你的项目里有循环依赖,直接抛异常,不会像以前那样悄悄通过三级缓存帮你兜底。
这个变化传递出的信号非常明确:**循环依赖不应该成为项目里的常规操作。**官方宁可让项目启动失败,也要逼开发者把代码结构调整干净。
如果你正在升级Spring Boot版本,遇到启动失败,而且确实暂时不好改代码,可以通过配置文件临时放开限制:
properties复制spring.main.allow-circular-references=true
但这个配置只能作为过渡方案,长期保留会让项目的依赖图越来越难维护。我强烈建议把这个配置当作一个技术债来对待,尽快清理掉实际的循环依赖。
7. 三级缓存和AOP的关系深入理解
7.1 getEarlyBeanReference是怎么生成早期代理的
循环依赖要配合AOP,整个三级缓存的设计才真正体现出价值。看AbstractAutowireCapableBeanFactory里的getEarlyBeanReference:
java复制protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject;
}
在执行到getEarlyBeanReference的时候,Spring会调用所有的SmartInstantiationAwareBeanPostProcessor。AbstractAutoProxyCreator就是这个接口的一个重要实现,它的getEarlyBeanReference方法会去判断当前这个Bean是否匹配任何切面规则,如果匹配,就提前用ProxyFactory生成代理对象。
关键点在于:这个判断和代理对象的生成,是发生在“某个Bean被循环依赖并索取引用”的时刻,而不是在容器的初始化阶段。这就是三级缓存配合AOP的精髓:代理对象需要生成时才生成,懒到不能再懒。
7.2 代理对象和原始对象,放进了哪个缓存
这里有一个容易混淆的细节。当getEarlyBeanReference返回了一个代理对象后,这个代理对象会被放进二级缓存,也就是earlySingletonObjects。而三级缓存里的ObjectFactory会被移除。
到了A完全创建完成后,在doCreateBean的末尾会做引用一致性检查。如果A没有被代理,exposedObject和原始bean相等,那早期暴露的引用和最终引用是同一个,直接用二级缓存里的对象作为最终结果。如果A在初始化阶段被另一个后置处理器替换成了别的代理对象,Spring会以最终生成的代理对象为准。
这段逻辑非常绕,但核心结论很简单:**在任何情况下,容器最终缓存到一级缓存里的对象,和已经注入给其他Bean的那个对象,必须保持一致。**这是Spring保证单例语义的地基。
7.3 @Async这类代理为什么有时会踩坑
我遇到过不少人在用@Async的时候遇到一个奇怪的问题:循环依赖加上@Async,结果Bean的代理没有生效,或者出现了一个“原始对象被注入”的现象。原因在于@Async使用的代理创建机制和事务AOP不太一样。
具体来说,@Async的代理是通过AsyncAnnotationBeanPostProcessor在postProcessAfterInitialization阶段生成的,它并没有实现SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法。这意味着如果循环依赖导致Bean提前暴露,早期引用的对象可能是原始对象,而不是带@Async代理的对象。后续即使执行了postProcessAfterInitialization生成了代理,但这个代理生成时机太晚,可能不会反映到已经被注入到其他Bean中的那个引用上。
这个问题的本质是:**三级缓存解决循环依赖的正常路径依赖SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference扩展点,如果你所用的代理机制没有实现这个扩展点,就会出现提前暴露的对象和最终对象不一致的问题。**我不是说遇到这种情况无解,但它确实说明了一个道理:循环依赖和AOP组合使用时要非常小心,能避免尽量避免。
8. 常见问题排查与源码阅读建议
8.1 循环依赖相关的报错排查清单
总结一下实际项目中常见的报错形态和排查方向:
| 报错信息 | 可能原因 | 处理思路 |
|---|---|---|
| BeanCurrentlyInCreationException | 构造器注入循环依赖 | 检查构造函数,改为字段注入或@Lazy,最好重构依赖关系 |
| The dependencies of some of the beans in the application context form a cycle | Spring Boot 2.6+默认禁止循环依赖 | 暂时开启allow-circular-references,但尽快清理循环 |
| BeanNotOfRequiredTypeException | AOP代理对象类型不匹配 | 检查是否混用了CGLIB代理和JDK动态代理,确认切面配置 |
| 循环依赖导致代理失效 | @Async等非SmartInstantiationAware后置处理器 | 避免循环依赖,或显式配置代理方式 |
8.2 看源码时怎么快速定位三级缓存相关代码
如果你想自己去看源码验证,我给你一条最小路径。直接在IDE里打开DefaultSingletonBeanRegistry,搜索singletonFactories和earlySingletonObjects这两个字段;然后看getSingleton方法,接着看doCreateBean中的addSingletonFactory调用;最后跳转到AbstractAutowireCapableBeanFactory,看getEarlyBeanReference。
这条路径短,变量少,逻辑集中,非常适合第一次读源码的人。不要一上来就打开ApplicationContext的大全家,那样容易被庞大的继承体系绕晕。记住一个思路:三级缓存是在DefaultSingletonBeanRegistry,代理判断是在AbstractAutoProxyCreator,关联逻辑在AbstractAutowireCapableBeanFactory的doCreateBean。
8.3 一个常见的误解:三级缓存只解决循环依赖
如果只看标题,很多人会以为三级缓存这个机制就是为了解决循环依赖而存在的。这个说法其实不够准确。三级缓存确实是循环依赖问题中最关键的一环,但ObjectFactory这种“延迟生成”的设计思路,给Spring带来的灵活性远超于循环依赖本身。它让Spring在“Bean尚未完全初始化”的时候就有了一个安全暴露引用的机制,AOP的早期代理、某些框架扩展点的处理,都依赖这套基础设施。
所以当你在看Spring源码、写框架扩展时,不要把三级缓存只当作一个“面试题答案”来记忆,它的核心价值在于提供了一种应对“依赖提前出现”问题的通用思路。
9. 从Spring基础到Spring AI,学习路径怎么规划
聊完三级缓存这个偏底层的主题,我想顺着热搜词里提到的Spring AI多说一句,因为这也是很多同学在问的“Spring学完之后该往哪儿走”。
9.1 Spring AI到底解决什么问题
Spring AI是Spring官方推出的人工智能应用开发框架,它做的事情可以简单理解为:把大模型API的调用封装成Spring风格的组件,让你用写Spring Boot业务代码的方式去接大模型。
如果你已经熟悉了Spring Boot的自动配置、依赖注入、starter机制,再来看Spring AI会非常顺手。它依然是用一个Starter引入依赖,用配置项配置模型API信息,然后用注入的方式拿到一个AI客户端,再调用它完成对话、生成、分析等任务。
Spring AI Alibaba则是阿里基于Spring AI做的适配和增强,降低了国内开发者使用国产大模型的门槛。它继承了一致的编程模型,同时在模型选择、服务部署上让国内用户用起来更顺手。
9.2 三级缓存的知识怎么迁移到Spring AI
有些人会有疑问:我学了三级缓存,对了解Spring AI有帮助吗?
我的观点是,短期看确实没什么直接关系。Spring AI的本质还是Spring Boot生态里的一个组件,你越熟悉Spring的自动配置原理、BeanFactory扩展机制、条件装配,就越能理解Spring AI的某个自动配置类为什么那么写,某个自定义配置为什么能覆盖默认逻辑。Spring Boot在做自动配置编排时,经常就是通过条件注解动态创建不同的Bean,这和容器的基础机制是一脉相承的。
换句话说,Spring容器原理就像地基,Spring AI是在地基上盖的一座新楼。地基打得越扎实,往上盖楼就越轻松。
9.3 给正在学Spring的同学一个路线建议
如果让我给一条清晰的路径,我会这样建议:
- 第一步:把IoC和AOP基础概念吃透,能自己写一个最简单的基于注解的项目;
- 第二步:深入Bean生命周期,掌握三级缓存和循环依赖原理;
- 第三步:理解Spring Boot的自动配置和条件装配机制;
- 第四步:去读Spring Data、Spring Security这类具体模块的源码,带着“容器是怎么管理这些Bean”的问题去读;
- 第五步:有余力再看Spring AI这类前沿扩展,你会发现前面的知识全都能用上。
这个路线不是必须严格按顺序走完,但“容器机制”这一环尽量不要跳过,它决定了你对整个Spring生态的理解深度。
10. 聊一点学习源码的心得
写到这里,我特别想分享一个学习上的体会。很多朋友看源码容易陷入一个误区,总想一次性看完一类BeanFactory的所有实现,从XmlBeanFactory一路看到AnnotationConfigApplicationContext,结果被巨大的类图劝退。
我自己用下来的有效方式是:先从一个具体的报错或者具体的现象入手,比如循环依赖报错、AOP代理不生效,然后沿着调用链去追一小段核心逻辑,只把那一段代码读透,其他的分支先不管。等积累了多个这样的小片段,再拼起来看整体,就会自然形成一张清晰的图。
三级缓存就是这样一个特别适合“单点突破”的主题。它的代码量不大,牵涉的类也不多,但设计思想极其丰富,能把你对Spring容器的理解拉升一个层次。把它搞明白之后,你会发现读Spring Boot里的很多源码,障碍都小了很多。
11. 最后分享一个小技巧
最后给你一个我在排查循环依赖时经常用的小技巧。如果你怀疑某个项目里存在循环依赖,但启动日志被刷得太多看不清楚,可以在启动参数里加一个调试项:
bash复制-Dspring.debug=true
这样可以打开Spring Boot的部分调试日志,同时把日志级别调整一下:
properties复制logging.level.org.springframework.beans.factory=DEBUG
logging.level.org.springframework.context.annotation=DEBUG
开启之后,你会看到类似“Bean 'xxx' is currently in creation”这样的提示,可以很直接地定位到是哪两个Bean在互相依赖。这个方法比在网上搜一堆报错案例要有效得多,推荐你试试。
Spring容器这套机制,说到底是围绕Bean的生命周期展开的。三级缓存只是其中一环,但这一环里浓缩了接口设计、延迟处理、并发控制、AOP扩展等多方面的智慧。把它真正理解了,再去看Spring其他模块,会有一种豁然开朗的感觉。
