Spring中Bean的循环依赖
# Spring中Bean的循环依赖
在上文的启动流程探索中,我们知道ioc跟bean的创建是在SpringApplication对象构造好后,执行run()
方法时,再具体一点就是refreshContext()
方法执行时
// 创建`IOC`容器
context = this.createApplicationContext();
// 设置一个启动器,设置应用程序启动
context.setApplicationStartup(this.applicationStartup);
// 准备IOC容器的基本信息
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 刷新IOC容器
this.refreshContext(context);
// 执行刷新后的处理
this.afterRefresh(context, applicationArguments);
2
3
4
5
6
7
8
9
10
那Bean执行过程中经历了哪些,它们之间的循环依赖又是什么?
# Bean的创建过程
一个bean
的创建大概分为5个步骤:
- 【实例化】createInstance——调用构造函数,创建实例
- 【属性赋值】注入依赖,property赋值
- 【初始化】回调Aware、postProcessor..
- 【使用】
- 【销毁】执行顺序为:@PreDestroy → DisposableBean.destroy() → 自定义destroy-method。,Bean被移除,资源被释放,Bean从容器和JVM内存中销毁
# 循环依赖
那所谓的循环依赖是什么意思呢?
是指在创建Bean的第二阶段——注入依赖
时,发现该Bean依赖另一个Bean,而另一个Bean的创建又依赖前一个Bean。从而导致彼此都无法创建的情况。
@Service
public class AService {
@Autowired
private BService bService;
}
2
3
4
5
6
7
@Service
public class BService {
@Autowired
private AService aService;
}
2
3
4
5
6
以上代码就是一种典型的情况,当然循环依赖也不只发生在两个Bean之间,多个Bean也可能发生,即使是单个Bean也可能发生————自己需要自己。
@Service
public class CService {
@Autowired
private CService cService;
}
2
3
4
5
6
# 缓存结构
对于循环依赖,Spring之所以能解决是因为独特的缓存结构。Spring中的缓存一共有三层:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 三级缓存
2
3
4
5
6
Spring 使用三个 Map(缓存)管理 Bean 的作用:
缓存层级 | 数据结构 | 作用 |
---|---|---|
一级缓存(singletonObjects ) | ConcurrentHashMap<String, Object> | 存储 完全初始化完成的 Bean(最终可用的实例)。 |
二级缓存(earlySingletonObjects ) | ConcurrentHashMap<String, Object> | 存储 半成品 Bean(已实例化但未完成属性注入或初始化的 Bean)。 |
三级缓存(singletonFactories ) | ConcurrentHashMap<String, ObjectFactory<?>> | 存储 对象工厂(ObjectFactory),用于按需生成半成品 Bean(支持 AOP 代理的延迟创建)。 |
但请记住,
Spring 通过 三级缓存机制(singletonObjects
、earlySingletonObjects
、singletonFactories
)只解决部分了循环依赖问题,但 并非所有循环依赖都能被解决。
# 使用缓存解决循环依赖
清楚了缓存结构,那么接下来看看它是如何解决bean创建过程中的循环依赖的。就用上面的AService跟BService例子
@Service
public class AService {
@Autowired
private BService bService;
}
2
3
4
5
6
7
以 Bean A 依赖 Bean B,Bean B 依赖 Bean A 为例:
# 1.创建 Bean A
步骤 1:实例化 A
Spring 调用 A 的构造函数生成原始对象(此时
b
属性为null
)步骤 2:提前暴露 A 的引用
将 A 的
ObjectFactory
存入三级缓存(singletonFactories
)步骤 3:填充 A 的属性
发现需要注入
B
,触发 B 的创建。
# 2.创建 Bean B
步骤 4:实例化 B
调用 B 的构造函数生成原始对象(此时
a
属性为null
)。步骤 5:提前暴露 B 的引用
将 B 的
ObjectFactory
存入三级缓存。步骤 6:填充 B 的属性
发现需要注入
A
,尝试从缓存获取 A:- 一级缓存:无 A(未初始化完成)❌
- 二级缓存:无 A(尚未放入)❌
- 三级缓存:找到 A 的
ObjectFactory
✅ → 调用ObjectFactory.getObject()
生成 A 的早期引用(可能是原始对象或代理对象)→ 将 A 的早期引用存入二级缓存(earlySingletonObjects
),并从三级缓存移除。
步骤 7:完成 B 的初始化
将 A 的早期引用注入 B,B 变为完整 Bean,存入一级缓存。
# 3. 完成 Bean A 的初始化
步骤 8:注入 B 到 A
此时一级缓存已有 B,将 B 注入 A。
步骤 9:初始化 A
执行 A 的初始化方法(如
@PostConstruct
)。步骤 10:A 存入一级缓存
将完全初始化的 A 从二级缓存移除,存入一级缓存。至此 A 和 B 均可正常使用
# 之所以要三级的原因
- 三级缓存的作用:
- 三级缓存允许通过 工厂方法 动态生成 Bean 的早期引用(支持 AOP 代理对象的创建)。
- 二级缓存 存储半成品 Bean,避免重复生成。
- 核心思想:
- 将 Bean 的实例化 与 依赖注入 分离,通过提前暴露 Bean 的早期引用解决循环依赖。
# 作用范围与局限
循环依赖类型 | 是否能解决 | 说明 |
---|---|---|
Setter 注入 | ✅ | 通过三级缓存机制解决。 |
字段注入 | ✅ | 通过三级缓存机制解决。 |
构造器注入 | ❌ | 无法提前暴露半成品 Bean,导致死循环。 |
原型作用域(Prototype) | ❌ | 每次创建新实例,无法使用缓存。 |
AOP 代理对象 | ❌ | 代理对象的创建时机可能导致失败。 |
# 为什么需要三级缓存、二级缓存
看了上面的流程,好像只用二级缓存也能解决循环依赖问题,因为只需要一个存放ObjectFactory
的缓存就可以解决循环依赖问题,或者说即使只有一级缓存也能解决循环依赖问题。为什么要设计三级缓存呢?
这是因为最终存放在一级缓存中的bean可能不是第一步中创建的原始实例,有可能是AOP中的代理对象,比如以下这种情况
@Service
public class AService{
@Transactional
public void test(){
}
}
2
3
4
5
6
7
8
因为加了@Transactional
所以在对象工厂中生成的就是代理对象,而非第一步中生成的原始对象。所以需要三级缓存,这也是三级缓存
存在的主要原因。
那如果把ObjectFactory
放入二级缓存呢?
放到三级。这是为了延迟加载,因为不是每个bean的生成会导致循环依赖,Spring它考虑的是只有在循环依赖发生的时候,才缓存半成品bean。
同时也是为了提高效率,比如以下这种情况
@Service
public class AService{
@Autowired
private BService bService;
}
@Service
public class BService{
@Autowired
private AService aService;
@Autowired
private CService cService;
}
@Service
public class CService{
@AutowireA
private AService aService;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 先创建A发现需要B
- 接着创建B,发现需要A,于是创建A对象工厂并放入三级,并生成半成品A放入二级
- 继续注入依赖C,发现没有C
- 在创建C的过程中,发现它也依赖A,这个时候之前存的二级缓存就起到了作用,可以直接创建
总结一下:
- 一级缓存则是存放的是实例化完成的bean
- 二级缓存中的bean有可能是半成品对象,也有可能是代理半成品对象。
- 三级缓存存的是ObjectFactory,主要是为了AOP的实现
# 为什么不能只用一级缓存?
一级缓存(singletonObjects
) 存储的是 完全初始化完成的 Bean(已实例化、属性注入、初始化完成)。
问题:
- 无法解决循环依赖: 在循环依赖场景中(如 A 依赖 B,B 依赖 A),Bean A 在完成初始化前需要注入 B,而 B 也需要注入 A。如果仅使用一级缓存,此时 A 还未初始化完成,无法放入一级缓存,B 也无法从一级缓存中获取 A,导致死锁。
- 无法支持 AOP 代理的延迟生成:
AOP 代理通常在 Bean 初始化阶段(
initializeBean
)生成。如果直接通过一级缓存暴露 Bean,可能导致代理对象过早生成,干扰初始化流程。
如果存储的是 ObjectFactory,
- 当
ObjectFactory.getObject()
被多次调用时(如多级循环依赖),每次都会生成新的代理对象- 违反单例原则:A 的代理对象被注入 B,但最终 A 初始化完成后又会生成另一个代理对象
- 结果:容器中存在 多个不同的 A 代理实例
为什么只能适用于单例模式,这是因为?
A[创建 ServiceA 实例1] --> B[注入 ServiceB] B --> C[创建 ServiceB 实例1] C --> D[注入 ServiceA] D --> E[创建 ServiceA 实例2] E --> F[注入 ServiceB] F --> G[创建 ServiceB 实例2] G --> H[...无限循环]
会导致堆栈溢出。
# 为什么不能只用二级缓存?
二级缓存(earlySingletonObjects
) 存储的是 半成品 Bean(已实例化但未完成属性注入和初始化)。
问题:
- 无法避免重复生成实例: 如果多个 Bean 依赖同一个未初始化的 Bean(如 B 依赖 A,C 也依赖 A),每次从二级缓存获取时都会调用工厂方法生成新的实例,导致性能浪费。
- 无法支持 AOP 代理的提前暴露:
二级缓存中存储的是原始 Bean 实例,而非代理对象。如果某个 Bean 需要 AOP 代理(如
@Transactional
),直接暴露原始实例会导致依赖方获取到未增强的对象,而后续生成的代理对象会覆盖原始实例,破坏依赖链的一致性。
# 为什么不能只用一级和二级缓存?
# 反面例子2:只有一级和二级缓存(只给毛坯房钥匙)
现在物业改规定了:毛坯房也可以先登记房号(二级缓存),钥匙可以先给邻居。
场景:你的房子(A)是毛坯房,但你知道以后要装智能窗帘(这需要特殊的电路接口和网络模块,相当于AOP代理)。但现在还没装,你就先把毛坯房的钥匙给了邻居(B)。
邻居很高兴,按照当前毛坯房的线路(原始对象),把他家的网线接口装好了。
几天后,你开始装修了,才把智能窗帘的系统装上。这个系统需要改造电路和网络接口。装修完后,你的房子变成了一个带智能家居的豪华版房子(Proxy A)。
这时问题来了:
- 你手里的房子:是豪华版(Proxy A)。
- 邻居手里的网线:还接在原来那个旧版毛坯房的接口(Original A)上。
- 物业登记册:登记的是你的豪华版房子(Proxy A)。
后果:
- 系统中同时存在了两个“A”:一个豪华版(你家和物业那里),一个毛坯版(邻居家)。
- 这严重违反了单例原则,本该只有一个Bean(A)的。
- 当邻居(B)想通过网络接口控制你的智能窗帘时,发现根本控制不了,因为他的线接错了地方!行为不一致了。
# 为什么三级缓存能解决?
三级缓存存的不是毛坯房,而是“施工队”(ObjectFactory)!
还是上面那个场景,但这次流程变了:
- 当邻居(B)来要房号时,物业没有直接给他毛坯房钥匙。
- 而是叫来了你的施工队(ObjectFactory),说:“现在邻居要接网线,而且我们知道你以后要装智能窗帘,你现在就按最终会装智能窗帘的标准,把网络接口提前给装好!”
- 施工队立刻动手,把毛坯房改造成了一个已经预埋好智能家居线路的半成品房(Early Proxy A),然后交给邻居。
- 邻居接入这个已经改造好的接口。
- 你后期装修时,只是在已经预埋好的线上安装具体的智能窗帘设备而已,不再改变线路结构。
结果:
- 邻居(B)拿到的,是和你最终完工的房子完全兼容的一个早期版本。
- 最终只有一个Bean,状态和行为都是一致的。
总结下问题就是:
代理对象不一致:
- B 持有 A 的原始对象
- 容器最终存储 A 的代理对象
- 违反单例原则:系统中存在两个不同的 A 实例
# 三级缓存(工厂)-核心中的核心!
它能在需要时(即发生循环依赖时)动态地、有预见性地创建Bean的早期引用(尤其是代理对象),确保这个早期引用和最终Bean是兼容的、一致的。它解决了“半成品如何加工”的问题。
为什么ObjectFactory创建后的bean可以解决aop的问题?
ObjectFactory
的智能,体现在它的核心任务——getEarlyBeanReference()
方法。
// 这就是“万能施工队”的施工图纸!
protected Object getEarlyBeanReference(String beanName, Object bean) {
Object exposedObject = bean;
// 拿着原始Bean,去问所有的“监理方”(BeanPostProcessor):
// “我们要提前暴露这个Bean了,你们谁要给它做一下包装吗?”
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 比如,这里会调用到 AbstractAutoProxyCreator 这个“AOP总监理”
// 它会判断这个Bean是否需要代理,如果需要,就创建一个代理对象返回。
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
return exposedObject; // 返回的可能是原始Bean,也可能是包装好的代理Bean
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个过程的核心优势:
- 决策延迟(按需代理):不是一上来就创建代理,而是等到循环依赖真正发生、其他Bean真正需要注入它的时候,才决定是否创建代理以及创建什么样的代理。这非常高效和精确。
- 保证一致性:所有依赖方,通过这个统一的“施工队”入口,拿到的都是同一个、最终形态的代理对象。彻底避免了前面例子中“毛坯房”和“精装房”并存的地狱场景。
- 尊重生命周期:虽然代理被提前创建了,但Bean的初始化方法(如
@PostConstruct
)依然会在其生命周期的正确阶段被执行。这个代理对象只是一个“空壳”,内部目标对象的状态变化(比如初始化完成的属性)会通过代理机制最终被应用到。
# 只用二级缓存,提前造好代理,是否可行呢?
从技术上讲,完全可以交给二级缓存来做。 但Spring选择不这样做的原因,不是技术不可行,而是出于性能和设计纯度的深度考量。
维度 | 二级缓存方案(提前处理) | 三级缓存方案(延迟按需处理) |
---|---|---|
性能 | 差。所有Bean都要提前承担代理检查的开销。 | 极佳。仅真正发生循环依赖的Bean承担此开销。 |
设计目标 | 优先保证功能实现。 | 优先保证极致性能和设计优雅。 |
模拟代码 | earlyProxy = **immediately** checkAndCreateProxy(bean); earlyCache.put(name, earlyProxy); | factoryCache.put(name, () -> checkAndCreateProxy(bean)); // 先存个函数 // 等需要时再调用这个函数() |
哲学 | “预先计算”(Eager Evaluation) | “延迟计算”或“惰性求值”(Lazy Evaluation) |
# 没循环依赖的情况下,不会用到三级缓存
对于绝大多数没有循环依赖的、安分守己的 Bean 来说,三级缓存就像一个不存在的身影,整个流程会变得非常简洁和高效。
让我们来完整跟踪一个普通 Bean(无循环依赖) 在缓存中的“一生”。
- 实例化 (
实例化
)
- 动作:Spring 调用 Bean 的构造函数,创建一个原始对象。此时对象内部的属性都是
null
或默认值。 - 代码象征:
Object beanInstance = constructor.newInstance(...);
- 暴露“备胎” (
三级缓存
)
- 动作:Spring 会习惯性地、有备无患地将一个能生成该 Bean 早期引用的
ObjectFactory
对象放入 三级缓存 (singletonFactories
)。
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean));
- 关键:对于普通 Bean,这个
ObjectFactory
永远也不会被调用。它就像一个上了膛但永远不会击发的子弹。
- 属性填充 (
populateBean
)
- 动作:Spring 扫描
@Autowired
等注解,为刚刚实例化的对象注入它所需要的依赖(其他 Bean)。 - 过程:如果依赖的 Bean 还没创建,Spring 会先去创建那些 Bean。因为没有循环依赖,这个过程会顺利递归完成,然后返回。
- 状态:此时,Bean 的属性已经被全部正确赋值,但它还没有执行初始化方法。
- 初始化 (
initializeBean
)
- 动作:Spring 执行 Bean 的生命周期回调方法。
- 执行
@PostConstruct
注解的方法。 - 执行
InitializingBean
接口的afterPropertiesSet()
方法。 - 执行自定义的
init-method
。
- 执行
- 初始化后 (& AOP 代理) (
三级缓存
)
- 动作:这是 AOP 代理正常创建的时机!Spring 会遍历所有的
BeanPostProcessor
。 - 过程:
AbstractAutoProxyCreator
这个后处理器会检查当前 Bean 是否需要被代理。如果需要,它就会在这里生成一个代理对象作为最终 Bean;如果不需要,则返回原始对象。 - 重要区别:此时创建的代理是“正常流程”的代理,与之前三级缓存里那个
ObjectFactory
准备的“应急方案”无关。
- 功德圆满 (
一级缓存
)
- 动作:将完全初始化好的 Bean(可能是代理对象,也可能是原始对象)放入 一级缓存 (
singletonObjects
)。从这里开始,程序通过applicationContext.getBean()
获取到的就是这个最终版本。 - 代码象征:
singletonObjects.put(beanName, fullyInitializedBean);
- 清理战场
- 动作:Spring 会将该 Bean 从 二级缓存 (
earlySingletonObjects
) 和 三级缓存 (singletonFactories
) 中移除。 - 原因:因为 Bean 已经完美创建完毕,不再需要那些为循环依赖准备的“备胎”机制了。清除它们可以释放内存。
# 三级缓存各层的作用
- 一级缓存:存储完整的Bean
- 二级缓存:避免多重循环依赖的情况 重复创建动态代理。
- 三级缓存:
- 缓存是函数接口:通过lambda 把方法传进去(把Beant实例和Bean名字传进去(aop创建))
- 不会立即调
- 会在 ABA(第二次getBean(A)才会去调用三级缓存(如果实现了aop才会创建动态代理,如果没有实现依然返回的Bean的实例))
- 放入二级缓存(避免重复创建)
# 总结
Spring 的 三级缓存机制(singletonObjects
、earlySingletonObjects
、singletonFactories
)是为了解决 单例 Bean 的循环依赖问题,同时支持 AOP 代理的延迟生成 和 性能优化。如果仅使用一级或二级缓存,无法满足这些复杂需求。
那个“咨询BeanPostProcessor”的动作看起来是独立的,为什么必须放在ObjectFactory里延迟执行,而不是在放入二级缓存前就做完。
其实这个问题抓住了Spring设计最精妙的核心——时机。关键点在于:“咨询BeanPostProcessor”这个操作本身是有副作用且成本高昂的,绝对不能无条件地为所有Bean都执行。
如果交给二级缓存来做,意味着每个Bean实例化后,不管有没有循环依赖,都必须立即执行所有SmartInstantiationAwareBeanPostProcessor来判定是否需要代理。这会产生两个致命问题:
- 性能灾难:很多Bean根本不会有循环依赖,但也要提前付出咨询所有后处理器的代价。Spring应用启动时会有成千上万个Bean,这个开销是巨大的。
- 状态冲突风险:在属性填充和初始化之前就创建代理,相当于给一个“半成品”穿上了代理的外衣。后续的初始化过程是在代理内部的目标对象上进行的,这可能会导致一些生命周期回调或依赖注入在代理环境下出现意外行为。
而三级缓存的ObjectFactory方案的精妙之处就在于按需触发。只有真正发生循环依赖、有其他Bean来索取引用时,才会调用ObjectFactory.getObject(),才会去执行那套昂贵的咨询流程。没有循环依赖的Bean就完全不会走这个路径,节省了大量开销。
# 依赖注入
依赖注入就是将该类所需的属性(其他 Bean 或配置值)初始化。
# 依赖注入的方式
Spring 支持多种依赖注入的方式,包括:
- 构造函数注入
- setter 方法注入
- 字段注入
我们在日常开发中常用的是字段注入,也就是直接在属性字段上加对应的注解;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 其他方法
}
2
3
4
5
6
7
8
9
10
11
# 官方不推荐使用@Autowired属性注入
官方推荐使用构造器方式
进行注入,这样可提升项目启动性能。属性注入可能需要在对象创建后进行多次注入操作,性能稍逊。
同时构造器的方式还可以使用final关键字,来保证属性值的不变,提高安全性。
而若是对属性值加上final修饰符,则无法通过属性进行注入。
原因如下:
属性注入(Field Injection)
在 Spring 框架中是通过反射(Reflection)
实现的。Spring 容器使用反射机制来访问和设置类的私有字段,从而完成依赖注入。
但加了 final
的属性字段必须在对象创建时被初始化。否则编译器会报错,提示 该属性 字段没有被初始化。即使你通过某种方式使代码编译通过,final
字段的不可变性仍然存在。Spring 框架在进行属性注入时,会尝试使用反射来设置字段的值。然而,由于 final
字段的特殊性,反射并不能可靠地更改其值。
所以,既要实现属性注入又要实现属性不可变性,是不可能的。因此,官方推荐构造器的方式注入。
不过,如今的开发中,使用属性注入的方式仍然很多,其原因在于它的便捷性和对代码量的简化。
而关于其他的问题,比如属性值的安全性等,应该是可以通过开发过程中来避免的。所以关于建议,还是要根据情况来看。
当然也有人认为,之所以这样建议是设计者认为依赖应该在初始化阶段就定义了而不是之后再定义,且任何依赖都不应为null。如果真的想传入null也应该传入null object pattern的实现而不是真的null值。
# @Autowired 和 @Resource 的区别
@Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。它们都提供了将依赖对象注入到当前bean的功能
- 来源不同:@Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解
- 查找顺序不同:
- @Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找
- @Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找
- 支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数
- 依赖注入的支持不同
- @Autowired 既支持
构造方法注入
,又支持属性注入
和Setter注入
- @Resource 只支持
属性注入
和Setter注入
- @Autowired 既支持
# 指定构造方法
bean的第一步是对象的构造,spring中bean的默认创建是调用无参构造方法
。
当然你可以指定构造方法。大家可以用一下简单代码进行实验:
@Service
public class AService {
private UserService userService;
public AService() {
System.out.println("bean Aservice 无参构造");
}
@Autowired
public AService(UserService userService) {
System.out.println("执行有参构造");
this.userService = userService;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
以上代码就是通过@Autowired
来指定使用有参构造来创建对象。项目启动时,在控制台便会打印有参构造对应的语句,而不会执行无参构造。
# 注解@PostConstruct
完成依赖注入后,就进入了第三阶段,执行各种回调。在依赖注入完成后,你如果想对bean做一些操作,比如缓存一些值,或者其他。你可以通过@PostConstruct
注解来实现:
@Service
public class AService {
private UserService userService;
public AService() {
System.out.println("bean Aservice 无参构造");
}
@PostConstruct
public void Test(){
System.out.println("PostConstruct");
}
@Autowired
public AService(UserService userService) {
System.out.println("执行有参构造");
this.userService = userService;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
使用该注解的方法会在依赖注入
完成后被自动调用。它的作用是:对当前bean进行一些初始化操作。调用顺序如下:
- Constructor >> @Autowired >> @PostConstruct
# InitializingBean接口
除了通过注解的方式可以实现:自定义方法,在依赖注入
完成后自动调用外,还可以用实现接口的方式。在bean类中实现接口InitializingBean
,并重写afterPropertiesSet()
方法:
@Service
public class AService implements InitializingBean {
private UserService userService;
public AService() {
System.out.println("bean Aservice 无参构造");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("initializing ---");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时在AService
完成依赖注入后,便会执行afterPropertiesSet
方法
# 注意
Spring Boot 2.6.0之后,如果程序中存在循环依赖问题,启动上就会失败,报错
如果要启动,需要进行一些设置
spring.main.allow-circular-references = true