1:前言
最近在項目中遇到了一次循環依賴報錯的問題,雖然解決的很快,但是有些不明白的地方,特此記錄。
在此我把 bean 的結構和 注入方式單獨拎出來進行演示
1.1:報錯提示
1.2:錯誤日志
Exception encountered during context initialization - cancelling refresh attempt:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'brokenComponent': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'tubunComponent' defined in class path resource [com/dev/config/sprngcache3/TwConfig.class]: Unsatisfied dependency expressed through method 'tubunComponent' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'denpaComponent': Unsatisfied dependency expressed through field 'tolenComponent'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tolenComponent': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'tubunComponent':
Requested bean is currently in creation:Is there an unresolvable circular reference?
1.3:stack overflow問題描述
Requested bean is currently in creation: Is there an unresolvable circular reference?
1.4:bean 依賴結構
BrokenComponent:
@Component
public class BrokenComponent {
@Autowired
private TolenComponent tolenComponent;
@Resource
private TubunComponent tubunComponent;
}
TolenComponent:
@Transactional
@Component
public class TolenComponent {
@Resource
private TubunComponent tubunComponent;
}
TubunComponent:
public class TubunComponent {
private DenpaComponent denpaComponent;
public TubunComponent(DenpaComponent denpaComponent) {
this.denpaComponent = denpaComponent;
}
}
TwConfig:
@Configuration
public class TwConfig {
@Bean
public TubunComponent tubunComponent(DenpaComponent denpaComponent) {
return new TubunComponent(denpaComponent);
}
}
DenpaComponent:
@Component
public class DenpaComponent {
@Autowired
private TolenComponent tolenComponent;
}
除了 tubunComponent 是構造器注入,其他的bean 都是set 注入。
分析bean依賴圖可以發現確實是循環依賴的問題。
2:問題分析
由於本人對spring bean 的加載機制不是很清晰,這次特意花了幾天時間做了梳理。
2.1:spring 的 bean 加載過程
2.2:spring 的三級緩存
解決了什么問題?
推薦博客:https://cloud.tencent.com/developer/article/1497692
2.3:Spring 容器加載過程中比較重要的類/屬性
類名稱 | 方法 | 作用 |
---|---|---|
DefaultListableBeanFactory | preInstantiateSingletons | Trigger initialization of all non-lazy singleton beans.../ 觸發所有非懶加載的單例bean進行初始化 |
AbstractBeanFactory | getBean/doGetBean | 從容器中獲取bean |
DefaultSingletonBeanRegistry | getSingleton | 從緩存中獲取單例實例,沒有則走單例的創建流程 |
DefaultSingletonBeanRegistry | beforeSingletonCreation | 創建單例之前會將單例放在set集合(singletonsCurrentlyInCreation)中,有檢查bean是否被循環依賴的作用 |
DefaultSingletonBeanRegistry | beforeSingletonCreation | 創建單例之后會將單例從set集合(singletonsCurrentlyInCreation)中移除 |
AbstractAutowireCapableBeanFactory | createBean/doCreateBean | 創建單例bean |
AbstractAutowireCapableBeanFactory | populateBean | 對bean進行屬性賦值(往往是二級緩存中的bean) |
AbstractAutowireCapableBeanFactory | initializeBean | 對屬性賦值之后的bean進行初始化,加載RootBeanDefinition信息,這一步做完才是一個完整的可用的bean |
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/** Cache of singleton objects: bean name to bean instance.(一級緩存,存放已初始化完畢的bean) */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory.(三級緩存,存放未進行過初始化的beanFactory) */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. (二級緩存,存放已實例化,但未進行裝配過屬性的bean)*/
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
/** Set of registered singletons, containing the bean names in registration order.(存放已注冊了的單例集合 = singletonObjects.size + earlySingletonObjects)*/
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);
/** Names of beans that are currently in creation.(存放正在被創建的實例,用於檢查是否有循環依賴) */
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
....
}
2.4:注冊后處理器->BeanPostProcessor
作用:對通過 BeanFactory 創建的 bean 進行屬性填充。
這是AbstractAutowireCapableBeanFactory#populateBean 方法中的某一段代碼:
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
我打出了所有要進行輪詢校驗的 BeanPostProcessor
brokenComponent 中有兩個bean:
一個是通過@Resource 注解注入的,一個是通過@Autowire 進行注入的,奉勸各位同學千萬不要兩種混用啊!!!
2.4.1:CommonAnnotationBeanPostProcessor
/**
* ...
*<p>The central element is the {@link javax.annotation.Resource} annotation
* for annotation-driven injection of named beans, by default from the containing
* Spring BeanFactory, with only {@code mappedName} references resolved in JNDI.
* The {@link #setAlwaysUseJndiLookup "alwaysUseJndiLookup" flag} enforces JNDI lookups
* equivalent to standard Java EE 5 resource injection for {@code name} references
* and default names as well. The target beans can be simple POJOs, with no special
* requirements other than the type having to match.
*
* <p>The JAX-WS {@link javax.xml.ws.WebServiceRef} annotation is supported too,
* analogous to {@link javax.annotation.Resource} but with the capability of creating
* specific JAX-WS service endpoints. This may either point to an explicitly defined
* resource by name or operate on a locally specified JAX-WS service class. Finally,
* this post-processor also supports the EJB 3 {@link javax.ejb.EJB} annotation,
* analogous to {@link javax.annotation.Resource} as well, with the capability to
* specify both a local bean name and a global JNDI name for fallback retrieval.
* The target beans can be plain POJOs as well as EJB 3 Session Beans in this case.
* ...
*/
public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor
implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable {
...
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findResourceMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of resource dependencies failed", ex);
}
return pvs;
}
}
通過翻譯及反復debug驗證,這個類將對某個bean中所有被 @Resource 注解修飾的屬性進行填充。
2.4.2:AutowiredAnnotationBeanPostProcessor
/**
* {@link org.springframework.beans.factory.config.BeanPostProcessor} implementation
* that autowires annotated fields, setter methods and arbitrary config methods.
* Such members to be injected are detected through a Java 5 annotation: by default,
* Spring's {@link Autowired @Autowired} and {@link Value @Value} annotations.
*
* <p>Also supports JSR-330's {@link javax.inject.Inject @Inject} annotation,
* if available, as a direct alternative to Spring's own {@code @Autowired}.
* ...
* @see Autowired
* @see Value
*/
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
...
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
try {
metadata.inject(bean, beanName, pvs);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
}
return pvs;
}
}
這個類將對某個bean中所有被 @Autowire@Value 注解修飾的屬性進行填充。
2.4.3:@Autowire VS @Resource
1:無論使用@Resource 還是 @Resource,都不影響bean的初始化順序。
2:@Resource修飾的屬性 要優先於 @Autowire修飾的屬性 進行初始化。
2.5:報錯流程
1: getBean("brokenComponent") -> getSingleton("brokenComponent") -> beforeSingletonCreation("brokenComponent") -> createBean("brokenComponent") ->
addSingletonFactory("brokenComponent") -> inject() ->
2: getBean("tubunComponent",TubunComponent.class) -> getSingleton("tubunComponent") -> beforeSingletonCreation("tubunComponent") ->
3:getBean("twConfig") -> getSingleton("twConfig") -> beforeSingletonCreation("twconfig") -> addSingletonFactory("twConfig") -> afterSingletonCreation("twConfig")
4:getBean("denpaComponent") -> getSingleton("denpaComponent") -> beforeSingletonCreation("denpaComponent") -> createBean("denpaComponent") ->
addSingletonFactory("denpaComponent") -> inject() ->
5:getBean("tolenComponent") -> getSingleton("tolenComponent") -> beforeSingletonCreation("tolenComponent") -> createBean("tolenComponent") ->
addSingletonFactory("tolenComponent") -> inject() ->
6:getBean("tubunComponent",TubunComponent.class) -> getSingleton("tubunComponent") -> beforeSingletonCreation("tubunComponent") -> 報錯
7:afterSingletonCreation("tolenComponent") -> afterSingletonCreation("denpaComponent") -> afterSingletonCreation("tubunComponent") -> afterSingletonCreation("brokenComponent")
可以看到 tubunComponent 進行了兩次 getSingleton,經過循環依賴檢查的時候報錯了。
2.6:錯誤流程分析歸納
1:Spring 容器先加載 brokenComponent 這個 bean。
2:brokenComponent依賴了tubunComponent(@Resource修飾),因此優先填充 tubunComponent,因此去初始化tubunComponent 這個bean,並將tubunComponent 存放在 DefaultSingletonBeanRegistry.singletonsCurrentlyInCreation 集合中。
3:tubunComponent依賴了depenComponent,因此去初始化depenComponent 這個bean,並將depenComponent 存放在 DefaultSingletonBeanRegistry.singletonsCurrentlyInCreation 集合中。
4:depenComponent依賴了tolenComponent,因此去初始化depenComponent 這個bean,並將tolenComponent 存放在 DefaultSingletonBeanRegistry.singletonsCurrentlyInCreation 集合中。
5:tolenComponent依賴了tubunComponent,因此去初始化tubunComponent 這個bean,然而當將tubunComponent 存放在 DefaultSingletonBeanRegistry.singletonsCurrentlyInCreation 集合中時發現 tubunComponent 已經存在了,
從而判斷這幾個bean 產生了循環依賴並跑出異常。
beforeSingletonCreation(String beanName) 方法源碼:
/**
* Callback before singleton creation.
* <p>The default implementation register the singleton as currently in creation.
* @param beanName the name of the singleton about to be created
* @see #isSingletonCurrentlyInCreation
*/
protected void beforeSingletonCreation(String beanName) {
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}
2.7:解決方式
1:使用@Lazy 修飾。
2:這其實是一個操作不當造成的問題,因為spring的三級緩存已經解決了set注入導致的循環依賴,而這幾個bean並不是全部都是使用構造器注入。
在 brokeComponent 中使用@Autowired修飾了tolenComponent,有用@Resource 修飾了tubunComponent,
導致tubunComponent要優先於tolenComponent優先加載,而tolenComponent 又依賴了tubunComponent,因此報錯。
我們只需要讓tolenComponent 優先於 tubunComponent 優先加載就可以了。
實驗證明:
1:@Resource 修飾 tolenComponent + @Resource 修飾 tubunComponent 的組合不會報錯。
2:@Resource 修飾 tolenComponent + @Autowired 修飾 tubunComponent 的組合不會報錯。
3:@Autowired 修飾 tolenComponent + @Autowired 修飾 tubunComponent 的組合不會報錯。
偏偏使用了會報錯的一種組合......