一、背 景
预发环境一个后台服务admin突然启动失败,异常如下:
css
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:598)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:376)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1404)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:847)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:744)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:391)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1215)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1204)
at com.shizhuang.duapp.commodity.interfaces.admin.CommodityAdminApplication.main(CommodityAdminApplication.java:100)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:578)
错误日志中明确写道:"Bean has been injected into other beans ... in its raw version as part of a circular reference, but has eventually been wrapped. "这不仅仅是一个简单的循环依赖错误。它揭示了一个更深层次的问题:当循环依赖遇上Spring的AOP代理(如@Transactional事务、自定义切面等),Spring在解决依赖的时,不得已将一个"半成品"(原始Bean)注入给了其他30多个Bean。而当这个"半成品"最终被"包装"(代理)成"成品"时,先前那些持有"半成品"引用的Bean们,使用的却是一个错误的版本。
这就像在组装一个精密机器时,你把一个未经质检的零件提前装了进去,等质检完成后,机器里混用着新旧版本的零件,最终的崩溃也就不可避免。
本篇文章将带你一起:
- 熟悉spring容器的循环依赖以及Spring容器如何解决循环依赖,创建bean相关的流程。
- 深入解读这条复杂错误日志背后的每一个关键线索;
- 提供紧急止血方案;
- 分享如何从架构设计上避免此类问题的实践心得。
二、相关知识点简介
2.1 循环依赖
什么是Bean循环依赖?
循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用,主要有如下几种情况。
第一种情况:自己依赖自己的直接依赖
第二种情况:两个对象之间的直接依赖
前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。
循环依赖场景
构造器注入循环依赖:
typescript
@Service
public class A {public A(B b) {}}
@Service
public class B {public B(A a) {}}
结果:项目启动失败抛出异常BeanCurrentlyInCreationException
dart
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:339)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:215)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
构造器注入构成的循环依赖,此种循环依赖方式无论是Singleton模式还是prototype模式都是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。原因是Spring解决循环依赖依靠的是Bean的"中间态"这个概念,而中间态指的是已经实例化,但还没初始化的状态。而完成实例化需要调用构造器,所以构造器的循环依赖无法解决。
Singleton模式field属性注入(setter方法注入)循环依赖:
这种方式是我们最为常用的依赖注入方式:
less
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
结果:项目启动成功,正常运行
prototype field属性注入循环依赖:
prototype在平时使用情况较少,但是也并不是不会使用到,因此此种方式也需要引起重视。
less
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class A {
@Autowired
private B b;
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class B {
@Autowired
private A a;
}
结果:需要注意的是本例中启动时是不会报错的(因为非单例Bean默认不会初始化,而是使用时才会初始化),所以很简单咱们只需要手动getBean()或者在一个单例Bean内@Autowired一下它即可。
java
// 在单例Bean内注入
@Autowired
private A a;
这样子启动就报错:
dart
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'mytest.TestSpringBean': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'a': Unsatisfied dependency expressed through field 'b'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'b': Unsatisfied dependency expressed through field 'a'; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:596)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374)
如何解决?可能有的小伙伴看到网上有说使用@Lazy注解解决:
less
@Lazy
@Autowired
private A a;
此处负责任的告诉你这样是解决不了问题的(可能会掩盖问题),@Lazy只是延迟初始化而已,当你真正使用到它(初始化)的时候,依旧会报如上异常。
对于Spring循环依赖的情况总结如下:
- 不能解决的情况:构造器注入循环依赖,prototype field属性注入循环依赖
- 能解决的情况:field属性注入(setter方法注入)循环依赖
Spring如何解决循环依赖
Spring 是通过三级缓存和提前曝光的机制来解决循环依赖的问题。
三级缓存
三级缓存其实就是用三个 Map 来存储不同阶段 Bean 对象。
typescript
一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
二级缓存
private final Map<String, ObjectearlySingletonObjects = new HashMap<>(16);
//三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16)
- singletonObjects:用于存放完全初始化好的 bean,从该缓存中取出的 bean 可以直接使用。
- earlySingletonObjects:提前曝光的单例对象的cache,存放原始的 bean 对象(尚未填充属性),用于解决循环依赖。
- singletonFactories:单例对象工厂的cache,存放 bean 工厂对象,用于解决循环依赖。
三级缓存解决循环依赖过程
假设现在我们有ServiceA和ServiceB两个类,这两个类相互依赖,代码如下:
less
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA ;
}
下面的时序图说明了spring用三级缓存解决循环依赖的主要流程:
为什么需要三级缓存?
这是一个理解Spring容器如何解决循环依赖的核心概念。三级缓存 是Spring为了解决循环依赖 的同时,又能保证AOP代理的正确性而设计的精妙机制。
为了理解为什么需要三级缓存,我们一步步来看。
如果没有缓存(Level 0)
假设有两个Bean:ServiceA 和 ServiceB,它们相互依赖。
Java
less
@Component
public class ServiceA {
@Autowired
private ServiceB serviceB;
}
@Component
public class ServiceB {
@Autowired
private ServiceA serviceA;
}
创建过程(无缓存) :
- 开始创建 ServiceA -> 发现 ServiceA 需要 ServiceB -> 开始创建 ServiceB
- 开始创建 ServiceB -> 发现 ServiceB 需要 ServiceA -> 开始创建 ServiceA
- 开始创建 ServiceA -> 发现 ServiceA 需要 ServiceB -> ... 无限循环,StackOverflowError
结论:无法解决循环依赖,直接死循环。
如果只有一级缓存(Singleton Objects)
一级缓存存放的是已经完全创建好、初始化完毕的Bean。
问题:在Bean的创建过程中(比如在填充属性 populateBean 时),ServiceA还没创建完,它本身不应该被放入"已完成"的一级缓存。但如果ServiceB需要ServiceA,而一级缓存里又没有ServiceA的半成品,ServiceB就无法完成创建。这就回到了上面的死循环问题。
结论:一级缓存无法解决循环依赖。
如果使用二级缓存
二级缓存的核心思路是:将尚未完全初始化好的"早期引用"暴露出来。
现在我们有:
- 一级缓存(成品库) :存放完全准备好的Bean。
- 二级缓存(半成品库) :存放刚刚实例化(调用了构造方法),但还未填充属性和初始化的Bean的早期引用。
创建过程(二级缓存):
开始创建ServiceA:
- 实例化ServiceA(调用ServiceA的构造方法),得到一个ServiceA的原始对象。
- 将ServiceA的原始对象放入二级缓存(半成品库)。
- 开始为ServiceA填充属性 -> 发现需要ServiceB。
开始创建ServiceB:
- 实例化ServiceB(调用B的构造方法),得到一个ServiceB的原始对象。
- 将ServiceB的原始对象放入二级缓存。
- 开始为ServiceB填充属性 -> 发现需要ServiceA。
ServiceB从二级缓存中获取A:
- ServiceB成功从二级缓存中拿到了ServiceA的早期引用(原始对象)。
- ServiceB顺利完成了属性填充、初始化等后续步骤,成为一个完整的Bean。
- 将完整的ServiceB放入一级缓存(成品库),并从二级缓存移除ServiceB。
ServiceA继续创建:
- ServiceA拿到了创建好的ServiceB,完成了自己的属性填充和初始化。
- 将完整的ServiceA放入一级缓存(成品库),并从二级缓存移除ServiceA。
问题来了:如果ServiceA需要被AOP代理怎么办?
如果A类上加了 @Transactional 等需要创建代理的注解,那么最终需要暴露给其他Bean的应该是ServiceA的代理对象,而不是ServiceA的原始对象。
在二级缓存方案中,ServiceB拿到的是A的原始对象 。但最终ServiceA 完成后,放入一级缓存的是ServiceA的代理对象。这就导致了:
- ServiceB里面持有的ServiceA是原始对象。
- 而其他地方注入的ServiceA是代理对象。
- 这就造成了不一致!如果通过ServiceB的ServiceA去调用事务方法,事务会失效,因为那是一个没有被代理的原始对象。
结论:二级缓存可以解决循环依赖问题,但无法正确处理需要AOP代理的Bean。
三级缓存的登场(Spring的终极方案)
为了解决代理问题,Spring引入了第三级缓存。它的核心不是一个直接存放对象(Object)的缓存,而是一个存放 ObjectFactory(对象工厂) 的缓存。
三级缓存的结构是:Map<String, ObjectFactory<?>> singletonFactories
创建过程(三级缓存,以ServiceA需要代理为例):
- 开始创建ServiceA:
- 实例化ServiceA,得到ServiceA的原始对象。
- 向三级缓存 添加一个ObjectFactory。这个工厂的getObject()方法有能力判断ServiceA是否需要代理,并返回相应的对象(原始对象或代理对象) 。
- 开始为ServiceA填充属性 -> 发现需要ServiceB。
- 开始创建B:
- 实例化ServiceB。
- 同样向三级缓存添加一个ServiceB的ObjectFactory。
- 开始为ServiceB填充属性 -> 发现需要ServiceA。
- ServiceB从缓存中获取ServiceA:
- ServiceB发现一级缓存没有ServiceA,二级缓存也没有ServiceA。
- ServiceB发现三级缓存有A的ObjectFactory。
- B调用这个工厂的getObject()方法。此时,Spring会执行一个关键逻辑:
- 如果ServiceA需要被代理,工厂会提前生成ServiceA的代理对象并返回。
- 如果ServiceA不需要代理,工厂则返回A的原始对象。
- 将这个早期引用(可能是原始对象,也可能是代理对象) 放入二级缓存,同时从三级缓存移除A的工厂。
- ServiceB拿到了ServiceA的正确版本的早期引用。
后续步骤:
- ServiceB完成创建,放入一级缓存。
- ServiceA继续用ServiceB完成创建。在ServiceA初始化的最后,Spring会再次检查:如果ServiceA已经被提前代理了(即在第3步中),那么就直接返回这个代理对象;如果没有,则可能在此处创建代理(对于不需要解决循环依赖的Bean)。
- 最终,将完整的ServiceA(代理对象)放入一级缓存,并清理二级缓存。
总结:为什么需要三级缓存?
需要三级缓存,是因为Spring要解决一个复杂问题:在存在循环依赖的情况下,如何确保所有Bean都能拿到最终形态(可能被AOP代理)的依赖对象,而不是原始的、未代理的对象。 三级缓存通过一个ObjectFactory将代理的时机提前,完美地解决了这个问题。二级缓存主要是为了性能优化而存在的。
spring三级缓存为什么不能解决
@Async注解的循环依赖问题
这触及了 Spring 代理机制的一个深层次区别。@Async注解的循环依赖问题确实比@Transactional 更复杂,三级缓存无法完全解决。让我们深入分析原因。
2.2 Spring创建Bean主要流程
为了容易理解 Spring 解决循环依赖过程,我们先简单温习下 Spring 容器创建 Bean 的主要流程。
从代码看Spring对于Bean的生成过程,步骤还是很多的,我把一些扩展业务代码省略掉:
scss
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// Bean初始化第一步:默认调用无参构造实例化Bean
// 如果是只有带参数的构造方法,构造方法里的参数依赖注入,就是发生在这一步
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//判断Bean是否需要提前暴露对象用来解决循环依赖,需要则启动spring三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// bean创建第二步:填充属性(DI依赖注入发生在此步骤)
populateBean(beanName, mbd, instanceWrapper);
// bean创建第三步:调用初始化方法,完成bean的初始化操作(AOP的第三个入口)
// AOP是通过自动代理创建器AbstractAutoProxyCreator的postProcessAfterInitialization()
//方法的执行进行代理对象的创建的,AbstractAutoProxyCreator是BeanPostProcessor接口的实现
exposedObject = initializeBean(beanName, exposedObject, mbd);
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
} catch (Throwable ex) {
// ...
}
// ...
return exposedObject;
}
从上述代码看出,整体脉络可以归纳成 3 个核心步骤:
- 实例化Bean:主要是通过反射调用默认构造函数创建 Bean 实例,此时Bean的属性都还是默认值null。被注解@Bean标记的方法就是此阶段被调用的。
- 填充Bean属性:这一步主要是对Bean的依赖属性进行填充,对@Value、@Autowired、@Resource注解标注的属性注入对象引用。
- 调用Bean初始化方法:调用配置指定中的init方法,如 xml文件指定Bean的init-method方法或注解 @Bean(initMethod = "initMethod")指定的方法。
三、案例分析
3.1 代码分析
以下是我简化后的类之间大体的依赖关系,工程内实际的依赖情况会比这个简化版本复杂一些。
less
@RestController
public class OldCenterSpuController {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
@RestController
public class TimeoutNotifyController {
@Resource
private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
public class NewSpuApplyCheckServiceImpl {
@Resource
private SpuCheckDomainServiceImpl spuCheckDomainServiceImpl;
}
@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
从代码看,主要是SpuCheckDomainServiceImpl和NewSpuApplyCheckServiceImpl 构成了一个依赖环。而我们从正常启动的bean加载顺序发现首先是从OldCenterSpuController开始加载的,具体情况如下所示:
scss
OldCenterSpuController
↓ (依赖)
NewSpuApplyCheckServiceImpl
↓ (依赖)
SpuCheckDomainServiceImpl
↓ (依赖)
NewSpuApplyCheckServiceImpl
异常启动的情况bean加载是从TimeoutNotifyController开始加载的,具体情况如下所示:
scss
TimeoutNotifyController
↓ (依赖)
SpuCheckDomainServiceImpl
↓ (依赖)
NewSpuApplyCheckServiceImpl
↓ (依赖)
SpuCheckDomainServiceImpl
同一个依赖环,为什么从OldCenterSpuController 开始加载就可以正常启动,而从TimeoutNotifyController 启动就会启动异常呢?下面我们会从现场debug的角度来分析解释这个问题。
3.2 问题分析
在相关知识点简介里面知悉到spring用三级缓存解决了循环依赖问题。为什么后台服务admin启动还会报循环依赖的问题呢?
要得到问题的答案,还是需要回到源码本身,前面我们分析了spring的创建Bean的主要流程,这里为了更好的分析问题,补充下通过容器获取Bean的。
在通过spring容器获取bean时,底层统一会调用doGetBean方法,大体如下:
less
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 从三级缓存获取bean
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}else {
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
//如果是单例Bean,从三级缓存没有获取到bean,则执行创建bean逻辑
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
从doGetBean方法逻辑看,在spring从一二三级缓存获取bean返回空时,会调用createBean方法去场景bean,createBean方法底层主要是调用前面我们提到的创建Bean流程的doCreateBean方法。
注意:doGetBean方法里面getSingleton方法的逻辑是先从一级缓存拿,拿到为空并且bean在创建中则又从二级缓存拿,二级缓存拿到为空 并且当前容器允许有循环依赖则从三级缓存拿。并且将对象工厂移到二级缓存,删除三级缓存
doCreateBean方法如下:
scss
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// Bean初始化第一步:默认调用无参构造实例化Bean
// 如果是只有带参数的构造方法,构造方法里的参数依赖注入,就是发生在这一步
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
//判断Bean是否需要提前暴露对象用来解决循环依赖,需要则启动spring三级缓存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// bean创建第二步:填充属性(DI依赖注入发生在此步骤)
populateBean(beanName, mbd, instanceWrapper);
// bean创建第三步:调用初始化方法,完成bean的初始化操作(AOP的第三个入口)
// AOP是通过自动代理创建器AbstractAutoProxyCreator的postProcessAfterInitialization()
//方法的执行进行代理对象的创建的,AbstractAutoProxyCreator是BeanPostProcessor接口的实现
exposedObject = initializeBean(beanName, exposedObject, mbd);
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
} catch (Throwable ex) {
// ...
}
// ...
return exposedObject;
}
将doGetBean和doCreateBean的逻辑转换成流程图如下:
从流程图可以看出,后台服务admin启动失败抛出UnsatisfiedDependencyException异常的必要条件是存在循环依赖,因为不存在循环依赖的情况bean只会存在单次加载,单次加载的情况bean只会被放进spring的第三级缓存。
而触发UnsatisfiedDependencyException异常的先决条件是需要spring的第一二级缓存有当前的bean。所以可以知道当前bean肯定存在循环依赖。在存在循环依赖的情况下,当前bean被第一次获取(即调用doGetBean方法)会缓存进spring的第三级缓存,然后会注入当前bean的依赖(即调用populateBean方法),在当前bean所在依赖环内其他bean都不在一二级缓存的情况下,会触发当前bean的第二次获取(即调用doGetBean方法),由于第一次获取已经将Bean放进了第三级缓存,spring会将Bean从第三级缓存移到二级缓存并删除第三级缓存。
最终会回到第一次获取的流程,调用初始化方法做初始化。最终在初始化有对当前bean做代理增强的并且提前暴露到二级缓存的对象有被其他依赖引用到,而且allowRawInjectionDespiteWrapping=false的情况下,会导致抛出UnsatisfiedDependencyException,进而导致启动异常。
注意:在注入当前bean的依赖时,这里spring将Bean从第三级缓存移到二级缓存并删除第三级缓存后,当前bean的依赖的其他bean会从二级缓存拿到当前bean做依赖。这也是后续抛异常的先决条件
结合admin有时候启动正常,有时候启动异常的情况,这里猜测启动正常和启动异常时bean加载顺序不一致,进而导致启动正常时当前Bean只会被获取一次,启动异常时当前bean会被获取两次。为了验证猜想,我们分别针对启动异常和启动正常的bean获取做了debug。
debug分析
首先我们从启动异常提取到以下关键信息,从这些信息可以知道是spuCheckDomainServiceImpl的加载触发的启动异常。所以我们这里以spuCheckDomainServiceImpl作为前面流程分析的当前bean。
dart
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'timeoutNotifyController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'spuCheckDomainServiceImpl': Bean with name 'spuCheckDomainServiceImpl' has been injected into other beans [...] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.
然后提前我们在doCreateBean方法设置好spuCheckDomainServiceImpl加载时的条件断点。我们先debug启动异常的情况。最终断点信息如下:
从红框1里面的两个引用看,很明显调initializeBean方法时spring有对spuCheckDomainServiceImpl做代理增强。导致initializeBean后返回的引用和提前暴露到二级缓存的引用是不一致的。这里spuCheckDomainServiceImpl有二级缓存是跟我们前面分析的吻合,是因为spuCheckDomainServiceImpl被获取了两次,即调了两次doGetBean。
从红框2里面的actualDependentBeans的set集合知道提前暴露到二级缓存的引用有被其他33个bean引用到,也是跟异常提示的bean列表保持一致的。
这里spuCheckDomainServiceImpl的加载为什么会调用两次doGetBean方法呢?
从调用栈分析到该加载链如下:
rust
TimeoutNotifyController ->spuCheckDomainServiceImpl-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl
TimeoutNotifyController注入依赖时第一次调用doGetBean获取spuCheckDomainServiceImpl时,从一二三级缓存获取不到,会调用doCreateBean方法创建spuCheckDomainServiceImpl。
首先会将spuDomainServiceImpl放进spring的第三级缓存,然后开始调populateBean方法注入依赖,由于在循环中间的newSpuApplyCheckServiceImpl是第一次获取,一二三级缓存都获取不到,会调用doCreateBean去创建对应的bean,然后会第二次调用doGetBean获取spuCheckDomainServiceImpl,这时spuCheckDomainServiceImpl在第一次获取已经将bean加载到第三级缓存,所以这次spring会将bean从第三级缓存直接移到第二级缓存,并将第三级缓存里面的spuCheckDomainServiceImpl对应的bean删除,并直接返回二级缓存里面的bean,不会再调doCreateBean去创建spuCheckDomainServiceImpl。最终完成了循环中间的bean的初始化后(这里循环中间的bean初始化时依赖到的bean如果有引用到spuCheckDomainServiceImpl会调用doGetBean方法从二级缓存拿到spuCheckDomainServiceImpl提前暴露的引用),会回到第一次调用doGetBean获取spuCheckDomainServiceImpl时调用的doCreateBean方法的流程。继续调initializeBean方法完成初始化,然后将初始化完成的bean返回。最终拿初始化返回的bean引用跟二级缓存拿到的bean引用做对比,发现不一致,导致抛出UnsatisfiedDependencyException异常。
那么这里为什么spuCheckDomainServiceImpl调用initializeBean方法完成初始化后与提前暴露到二级缓存的bean会不一致呢?
看spuCheckDomainServiceImpl的代码如下:
less
@Component
@Slf4j
@Validated
public class SpuCheckDomainServiceImpl {
@Resource
private NewSpuApplyCheckServiceImpl newSpuApplyCheckServiceImpl;
}
发现SpuCheckDomainServiceImpl类有使用到 @Validated 注解。查阅资料发现 @Validated的实现是通过在initializeBean方法里面执行一个org.springframework.validation.beanvalidation.MethodValidationPostProcessor后置处理器实现的,MethodValidationPostProcessor会对SpuCheckDomainServiceImpl做一层代理。导致initializeBean方法返回的spuCheckDomainServiceImpl是一个新的代理对象,从而最终导致跟二级缓存的不一致。
debug视图如下:
那为什么有时候能启动成功呢?什么情况下能启动成功?
我们继续debug启动成功的情况。最终观察到spuCheckDomainServiceImpl只会调用一次doGetBean,而且从一二级缓存拿到的spuCheckDomainServiceImpl提前暴露的引用为null,如下图:
这里为什么spuCheckDomainServiceImpl只会调用一次doGetBean呢?
首先我们根据调用栈整理到当前加载的引用栈:
rust
oldCenterSpuController-> newSpuApplyCheckServiceImpl-> ... ->spuCheckDomainServiceImpl -> newSpuApplyCheckServiceImpl
根据前面启动失败的信息我们可以知道,spuCheckDomainServiceImpl处理依赖的环是:
rust
spuCheckDomainServiceImpl ->newSpuApplyCommandServiceImpl-> ... ->spuCheckDomainServiceImpl
失败的情况我们发现是从spuCheckDomainServiceImpl开始创建的,现在启动正常的情况是从newSpuApplyCheckServiceImpl开始创建的。
创建 newSpuApplyCheckServiceImpl时,发现它依赖环中间这些bean会依次调用doCreateBean方法去创建对应的bean。
调用到spuCheckDomainServiceImpl时,由于是第一次获取bean,也会调用doCreateBean方法创建bean,然后回到创建spuCheckDomainServiceImpl的doCreateBean流程,这里由于没有将spuCheckDomainServiceImpl的三级缓存移到二级缓存,所以不会导致抛出UnsatisfiedDependencyException异常,最终回到newSpuApplyCheckServiceImpl的doCreateBean流程,由于newSpuApplyCheckServiceImpl在调用initializeBean方法没有做代理增强,所以也不会导致抛出UnsatisfiedDependencyException异常。因此最后可以正常启动。
这里我们会有疑问?类的创建顺序由什么决定的呢?
通常不同环境下,代码打包后的jar/war结构、@ComponentScan的basePackages配置细微差别,都可能导致Spring扫描和注册Bean定义的顺序不同。Java ClassLoader加载类的顺序本身也有一定不确定性。如果Bean定义是通过不同的配置类引入的,配置类的加载顺序会影响其中所定义Bean的注册顺序。
那是不是所有的类增强在有循环依赖时都会触发UnsatisfiedDependencyException异常呢?
并不是,比如@Transactional就不会导致触发UnsatisfiedDependencyException异常。让我们深入分析原因。
核心区别在于代理创建时机不同。
@Transactional的代理时机如下:
markdown
// Spring 为 @Transactional 创建代理的流程1. 实例化原始 Bean
2. 放入三级缓存(ObjectFactory)
3. 当发生循环依赖时,调用 ObjectFactory.getObject()
4. 此时判断是否需要事务代理,如果需要则提前创建代理
5. 将代理对象放入二级缓存,供其他 Bean 使用
@Validated的代理时机:
markdown
// @Validated 的代理创建在生命周期更晚的阶段1. 实例化原始 Bean
2. 放入三级缓存(ObjectFactory)
3. 当发生循环依赖时,调用 ObjectFactory.getObject()
4. ❌ 问题:此时 @Validated 的代理还未创建!
5. 其他 Bean 拿到的是原始对象,而不是异步代理对象
问题根源:@Transactional的代理增强是在三层缓存生成时触发的, @Validated的增强是在初始化bean后通过后置处理器做的代理增强。
3.3 解决方案
短期方案
- 移除SpuCheckDomainServiceImpl类上的Validated注解
- @lazy 解耦
-
- 原理是发现有@lazy 注解的依赖为其生成代理类,依赖代理类,只有在真正需要用到对象时,再通过getBean的逻辑去获取对象,从而实现了解耦。
长期方案
严格执行DDD代码规范
这里是违反DDD分层规范导致的循环依赖。
梳理解决历史依赖环
通过梳理修改代码解决历史存在的依赖环。我们内部实现了一个能检测依赖环的工具,这里简单介绍一下实现思路,详情如下。
日常循环依赖环:实战检测工具类解析
在实际项目中,即使遵循了DDD分层规范和注入最佳实践,仍有可能因业务复杂或团队协作不充分而引入循环依赖。为了在开发阶段尽早发现这类问题,我们可以借助自定义的循环依赖检测工具类,在Spring容器启动后自动分析并报告依赖环。
功能概述:
- 条件启用:通过配置circular.dependecy.analysis.enabled=true开启检测;
- 依赖图构建:扫描所有单例Bean,分析其构造函数、字段、方法注入及depends-on声明的依赖;
- 循环检测算法:使用DFS遍历依赖图,识别所有循环依赖路径;
- 通知上报:检测结果通过飞书机器人发送至指定接收人(targetId)。
简洁代码结构如下:
typescript
@Component
@ConditionalOnProperty(value = "circular.dependency.analysis.enabled", havingValue = "true")
public class TimingCircularDependencyHandler extends AbstractNotifyHandler<NotifyData>
implements ApplicationContextAware, BeanFactoryAware {
@Override
public Boolean handler(NotifyData data) {
dependencyGraph = new HashMap<>();
handleContextRefresh(); // 触发依赖图构建与检测
return Boolean.TRUE;
}
private void buildDependencyGraph() {
// 遍历所有Bean,解析其依赖关系
// 支持:构造器、字段、方法、depends-on
}
private void detectCircularDependencies() {
// 使用DFS检测环,记录所有循环路径
// 输出示例:循环依赖1: A -> B -> C -> A
}
}
四、总结
循环依赖暴露了代码结构的设计缺陷。理论上应通过分层和抽象来避免,但在复杂的业务交互中仍难以杜绝。虽然Spring利用三级缓存等机制默默解决了这一问题,使程序得以运行,但这绝不应是懈怠设计的借口。我们更应恪守设计原则,从源头规避循环依赖,构建清晰、健康的架构。
往期回顾
-
Apex AI辅助编码助手的设计和实践|得物技术
-
从 JSON 字符串到 Java 对象:Fastjson 1.2.83 全程解析|得物技术
-
用好 TTL Agent 不踩雷:避开内存泄露与CPU 100%两大核心坑|得物技术
-
线程池ThreadPoolExecutor源码深度解析|得物技术
-
基于浏览器扩展 API Mock 工具开发探索|得物技术
文 /鲁班
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。