DI 之 3.2 循环依赖 (伍)

本文介绍了Spring框架中循环依赖的概念及解决方法,详细分析了构造器循环依赖与setter循环依赖的区别,以及Spring如何处理不同作用域下的循环依赖问题。

3.2.1  什么是循环依赖

       循环依赖就是循环引用,就是两个或多个Bean相互之间的持有对方,比如CircleA引用CircleB,CircleB引用CircleC,CircleC引用CircleA,则它们最终反映为一个环。此处不是循环调用,循环调用是方法之间的环调用。如图3-5所示:

图3-5 循环引用

       循环调用是无法解决的,除非有终结条件,否则就是死循环,最终导致内存溢出错误。

       Spring容器循环依赖包括构造器循环依赖和setter循环依赖,那Spring容器如何解决循环依赖呢?首先让我们来定义循环引用类:

package cn.javass.spring.chapter3.bean;  
public class CircleA {  
    private CircleB circleB;  
    public CircleA() {  
    }  
    public CircleA(CircleB circleB) {  
        this.circleB = circleB;  
    }  
public void setCircleB(CircleB circleB)   
{  
        this.circleB = circleB;  
    }  
public void a() {  
   circleB.b();  
}  
} 
package cn.javass.spring.chapter3.bean;  
public class CircleB {  
    private CircleC circleC;  
    public CircleB() {  
    }  
    public CircleB(CircleC circleC) {  
        this.circleC = circleC;  
    }  
public void setCircleC(CircleC circleC)   
{  
        this.circleC = circleC;  
    }  
    public void b() {  
        circleC.c();  
    }  
}  
package cn.javass.spring.chapter3.bean;  
public class CircleC {  
    private CircleA circleA;  
    public CircleC() {  
    }  
    public CircleC(CircleA circleA) {  
        this.circleA = circleA;  
    }  
public void setCircleA(CircleA circleA)   
{  
        this.circleA = circleA;  
    }  
    public void c() {  
        circleA.a();  
    }  
}  

3.2.2        Spring如何解决循环依赖

一、构造器循环依赖:表示通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖。

如在创建CircleA类时,构造器需要CircleB类,那将去创建CircleB,在创建CircleB类时又发现需要CircleC类,则又去创建CircleC,最终在创建CircleC时发现又需要CircleA;从而形成一个环,没办法创建。

Spring容器将每一个正在创建的Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的Bean将从“当前创建Bean池”中清除掉。

       1)首先让我们看一下配置文件(chapter3/circleInjectByConstructor.xml):

<bean id="circleA" class="cn.javass.spring.chapter3.bean.CircleA">  
<constructor-arg index="0" ref="circleB"/>  
</bean>  
<bean id="circleB" class="cn.javass.spring.chapter3.bean.CircleB">  
<constructor-arg index="0" ref="circleC"/>  
</bean>  
<bean id="circleC" class="cn.javass.spring.chapter3.bean.CircleC">  
<constructor-arg index="0" ref="circleA"/>  
</bean> 

 2)写段测试代码(cn.javass.spring.chapter3.CircleTest)测试一下吧:

@Test(expected = BeanCurrentlyInCreationException.class)  
public void testCircleByConstructor() throws Throwable {  
try {  
      new ClassPathXmlApplicationContext("chapter3/circleInjectByConstructor.xml");  
    }  
    catch (Exception e) {  
      //因为要在创建circle3时抛出;  
      Throwable e1 = e.getCause().getCause().getCause();  
      throw e1;  
    }  
}  

 让我们分析一下吧:

       1、Spring容器创建“circleA” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleB”,并将“circleA” 标识符放到“当前创建Bean池”;

       2、Spring容器创建“circleB” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleC”,并将“circleB” 标识符放到“当前创建Bean池”;

3、Spring容器创建“circleC” Bean,首先去“当前创建Bean池”查找是否当前Bean正在创建,如果没发现,则继续准备其需要的构造器参数“circleA”,并将“circleC” 标识符放到“当前创建Bean池”;

4、到此为止Spring容器要去创建“circleA”Bean,发现该Bean 标识符在“当前创建Bean池”中,因为表示循环依赖,抛出BeanCurrentlyInCreationException。

  

二、setter循环依赖:表示通过setter注入方式构成的循环依赖。

对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(如setter注入)的Bean来完成的,而且只能解决单例作用域的Bean循环依赖。

       如下代码所示,通过提前暴露一个单例工厂方法,从而使其他Bean能引用到该Bean。

addSingletonFactory(beanName, new ObjectFactory() {  
    public Object getObject() throws BeansException {  
        return getEarlyBeanReference(beanName, mbd, bean);  
    }  
});

具体步骤如下:

       1、Spring容器创建单例“circleA” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleA” 标识符放到“当前创建Bean池”;然后进行setter注入“circleB”;

       2、Spring容器创建单例“circleB” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory”用于返回一个提前暴露一个创建中的Bean,并将“circleB” 标识符放到“当前创建Bean池”,然后进行setter注入“circleC”;

       3、Spring容器创建单例“circleC” Bean,首先根据无参构造器创建Bean,并暴露一个“ObjectFactory ”用于返回一个提前暴露一个创建中的Bean,并将“circleC” 标识符放到“当前创建Bean池”,然后进行setter注入“circleA”;进行注入“circleA”时由于提前暴露了“ObjectFactory”工厂从而使用它返回提前暴露一个创建中的Bean;

4、最后在依赖注入“circleB”和“circleA”,完成setter注入。

 

       对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean。

<!-- 定义Bean配置文件,注意scope都是“prototype”-->  
<bean id="circleA" class="cn.javass.spring.chapter3.bean.CircleA" scope="prototype">  
        <property name="circleB" ref="circleB"/>  
   </bean>  
   <bean id="circleB" class="cn.javass.spring.chapter3.bean.CircleB" scope="prototype">  
       <property name="circleC" ref="circleC"/>  
   </bean>  
   <bean id="circleC" class="cn.javass.spring.chapter3.bean.CircleC" scope="prototype">  
       <property name="circleA" ref="circleA"/>  
   </bean>  
//测试代码cn.javass.spring.chapter3.CircleTest  
@Test(expected = BeanCurrentlyInCreationException.class)  
public void testCircleBySetterAndPrototype () throws Throwable {  
    try {  
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(  
"chapter3/circleInjectBySetterAndPrototype.xml");  
        System.out.println(ctx.getBean("circleA"));  
    }  
    catch (Exception e) {  
        Throwable e1 = e.getCause().getCause().getCause();  
        throw e1;  
    }  
}  

 对于“singleton”作用域Bean,可以通过“setAllowCircularReferences(false);”来禁用循环引用:

@Test(expected = BeanCurrentlyInCreationException.class)  
public void testCircleBySetterAndSingleton2() throws Throwable {  
    try {  
        ClassPathXmlApplicationContext ctx =  
new ClassPathXmlApplicationContext();  
        ctx.setConfigLocation("chapter3/circleInjectBySetterAndSingleton.xml");  
        ctx.refresh();  
    }  
    catch (Exception e) {  
        Throwable e1 = e.getCause().getCause().getCause();  
        throw e1;  
    }  
}  

补充:出现循环依赖是设计上的问题,一定要避免!

包之间的依赖结构必须是一个直接的无环图形(DAG)。也就是说,在依赖结构中不允许出现环(循环依赖)。

 

<think>好的,我现在要仔细分析用户的问题,他想要深入理解Spring依赖注入(DI)的实现原理,特别是基于源码的解析。用户之前已经得到过关于Spring DI的详细回答,现在需要结合提供的站内引用进一步深化。 首先,我需要回顾之前的回答,确保这次的内容不会重复,而是补充更多源码层面的细节。用户提供的引用包括CommonAnnotationBeanPostProcessor、SpringApplication启动过程、IoC容器原理以及构造器注入的循环依赖问题。这些引用需要被整合到新的回答中,特别是关于源码的部分。 用户特别提到要基于Spring源码进行解析,所以需要定位到具体的类和方法。例如,引用[1]提到了CommonAnnotationBeanPostProcessor的ResourceElement#getResourceToInject,这可能与@Resource注解的处理有关。而引用[4]中的例子展示了构造器注入导致的循环依赖问题,这需要结合Spring的三级缓存机制来解释。 接下来,需要明确回答的结构。用户可能需要从DI的核心机制、注解处理、循环依赖的源码实现等方面展开。同时,用户可能对Bean的实例化过程、依赖查找、注入方式(字段、构造器、setter)的具体实现感兴趣。 要注意到引用[3]中提到的IoC容器的核心概念,需要将DI的实现与IoC容器的工作流程结合起来,比如BeanDefinition的加载、BeanFactory的作用等。引用[2]提到的SpringApplication.run方法可能与容器的启动过程有关,但用户的问题更关注DI的实现,所以可能需要简要提及,重点放在DI部分。 在解析源码时,需要挑选关键类,如DefaultListableBeanFactory、AbstractAutowireCapableBeanFactory,以及处理注解的后处理器,如AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor。需要解释这些类在DI过程中的角色,以及它们如何协作完成依赖注入。 关于循环依赖,引用[4]中的代码示例展示了构造器注入无法解决循环依赖的问题,这与Spring的三级缓存机制有关。需要详细说明在构造器注入的情况下,Spring是如何检测并抛出异常的,而在字段或setter注入时如何通过三级缓存解决。 还需要考虑用户可能的疑问点,例如不同注解(@Autowired、@Resource)的处理差异,后处理器的作用顺序,以及Spring Boot在自动配置中对DI的扩展。这些问题可以作为相关问题提出,帮助用户进一步探索。 最后,确保回答符合用户要求的格式:使用中文,行内公式用$...$,独立公式用$$...$$,正确引用站内文献,并生成相关问题。需要避免使用Markdown,保持自然的结构,分步骤详细解释源码的关键部分,并结合示例代码说明。</think>#### Spring依赖注入(DI)源码级实现原理剖析 --- ### 一、DI核心实现机制 Spring DI的核心实现集中在`DefaultListableBeanFactory`类中,其核心逻辑通过**Bean后处理器**实现。关键源码路径如下: ``` org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean -> populateBean(依赖注入) -> initializeBean(初始化) ``` #### 1.1 依赖解析流程(以字段注入为例) ```java // AutowiredAnnotationBeanPostProcessor核心方法 public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { // 扫描@Autowired注解字段 InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs); try { metadata.inject(bean, beanName, pvs); // 执行注入 } // ... } ``` 具体注入实现: ```java // 字段注入核心方法(反射实现) field.setAccessible(true); field.set(bean, getBean(field.getType())); // 触发依赖查找[^3] ``` --- ### 二、注解驱动实现源码解析 #### 2.1 @Autowired处理机制 由`AutowiredAnnotationBeanPostProcessor`实现,关键类关系: ```mermaid classDiagram AutowiredAnnotationBeanPostProcessor --|> InstantiationAwareBeanPostProcessorAdapter AutowiredAnnotationBeanPostProcessor : +postProcessProperties() AutowiredAnnotationBeanPostProcessor : +determineCandidateConstructors() ``` **构造器注入选择算法**: ```java // 构造器优先级判断逻辑(源码截取) if (candidates.size() == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) { return new Constructor<?>[] { uniqueCandidate }; // 唯一构造器直接选择 } else if (candidates.size() > 1) { // 按@Autowired(required=true)优先原则排序 } ``` #### 2.2 @Resource特殊处理 引用[1]中提到的`CommonAnnotationBeanPostProcessor`处理流程: ```java // ResourceElement注入实现(源码关键片段) protected Object getResourceToInject(Object target, String requestingBeanName) { return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : getResource(this, requestingBeanName)); // 实际依赖查找入口[^1] } ``` 与@Autowired的主要差异: - 依赖查找策略:按名称(JNDI)->按类型 - 支持`@PostConstruct`/`@PreDestroy`生命周期回调 --- ### 三、循环依赖处理源码探秘 #### 3.1 三级缓存实现 ```java // DefaultSingletonBeanRegistry源码结构 public class DefaultSingletonBeanRegistry ... { private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存 private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 二级缓存 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存 } ``` #### 3.2 构造器注入限制验证 引用[4]中的案例对应源码逻辑: ```java // AbstractAutowireCapableBeanFactory.doCreateBean() if (earlySingletonExposure) { // 将刚实例化的Bean加入三级缓存 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } // 当检测到构造器循环依赖时抛出异常 if (isPrototypeCurrentlyInCreation(beanName)) { throw new BeanCurrentlyInCreationException(beanName); // 构造器注入无法解决循环依赖[^4] } ``` $$ \text{允许循环依赖条件} = \begin{cases} \text{非构造器注入} \\ \text{单例作用域} \\ \text{允许早期引用暴露} \end{cases} $$ --- ### 四、依赖注入类型处理 #### 4.1 泛型依赖解析 通过`ResolvableType`处理泛型: ```java ResolvableType resolvableType = ResolvableType.forField(field); BeanFactory beanFactory = getBeanFactory(); Object value = beanFactory.getBean(resolvableType); ``` #### 4.2 集合类型注入 特殊处理逻辑: ```java // DependencyDescriptor解析集合元素类型 if (type.isCollection() || type.isArray()) { Class<?> elementType = type.getComponentType(); return beanFactory.getBeansOfType(elementType); } ``` --- ### 五、现代Spring的优化演进 #### 5.1 条件化注入实现 `@Conditional`底层通过`ConditionEvaluator`实现: ```java // ConditionEvaluator.shouldSkip() public boolean shouldSkip(...) { ConditionContext context = new ConditionContextImpl(...); for (Condition condition : conditions) { if (!condition.matches(context)) return true; } return false; } ``` #### 5.2 Spring Boot自动装配增强 引用[2]中提到的启动流程关键扩展点: ```java // SpringApplication.run() public ConfigurableApplicationContext run(String... args) { // ... refreshContext(context); // 触发BeanFactory初始化 // 通过AutoConfigurationImportSelector加载自动配置类 } ``` --- ### 六、性能优化关键点 1. **缓存机制**:`MergedBeanDefinitionPostProcessor`缓存处理后的Bean定义 2. **快速失败策略**:提前校验@Autowired的必要依赖是否存在 3. **延迟查找**:使用`ObjectProvider`实现按需加载 --- ### 相关问题 1. 为什么Spring无法解决构造器注入的循环依赖?源码中如何实现这种限制? 2. `@Autowired`和`@Resource`在依赖查找算法上有何本质区别? 3. 如何通过扩展`BeanPostProcessor`实现自定义依赖注入逻辑? 4. Spring 5.x在DI实现上有哪些重大改进? [^1]: CommonAnnotationBeanPostProcessor源码解析 [^2]: Spring Boot启动流程核心机制 [^3]: Spring IoC容器依赖查找原理 : 构造器注入循环依赖检测实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值