依赖注入、循环依赖 - 你真的了解吗?

关注公众号【1024个为什么】,及时接收最新推送文章!   

本文观点可能颠覆你现有的认知,请坐稳扶好。

||  依赖注入

| 什么是依赖注入?

依赖注入(Dependency Injection,简称DI),先看看百度百科的结果,大家自己领会,我持保留意见。

我的理解:一种通过外部传值给类成员变量赋值的编码风格。

| 为什么会有依赖注入?

个人猜测应该和面向对象设计原则有关,CRP(Composite Reuse Principle),CRP讲究的是对象间的相互配合,优先使用组合而不是继承。

那么问题来了,组合的这些对象如何赋值呢?最直接的做法是new一个对象给属性赋值。 

public class Emailer {
    private SpellChecker spellChecker;
    public Emailer() {
        this.spellChecker = new SpellChecker();
    }
    public void send(String text) { .. }
}

这种赋值方式存在几个问题:

  • 每实例化一个Emailer对象,都会实例化一个SpellChecker对象,如果某个执行逻辑中没用到spellChecker对象,那岂不是即浪费时间又浪费空间。

  • 如果用到spellChecker对象时发现最开始赋的值不满足怎么办?

  • 如果引用的是一个ISpellChecker接口呢?在实例化Emailer的时候还不知道业务逻辑执行的时候用到ISpellChecker的哪个实现类‍。

于是就有了另外一个设计原则,DIP(Dependency Injection Principle),这个原则讲的是面向接口编程而不是面向实现编程,使用注入的方式完成依赖对象的赋值。说白了就是不管依赖类还是接口,都支持,谁用谁传值就行了。

外部传值,可以有多种形式,总结起来就两大类:构造方法传参,非构造方法传参(setter方法,方法名不重要,只是大多数习惯了叫setter,很多起名叫 init(Xxx xxx))。

| 依赖注入达到了什么目的?

解耦:我是一台豆浆机,要打出豆浆就需要水、豆子(依赖的2个对象), 谁使用谁就要负责加水、放豆子,我提供打豆浆的功能,但需要的东西(对象的值)要由使用方在使用的时候提供(注入)。就算不提供水、豆子,也不影响我正常的旋转。

易于扩展:主要是指依赖接口的情况,不管你放黄豆、绿豆还是黑豆,只要属于豆子(接口)都支持。

||  循环依赖

| 什么情况下会循环依赖?

两个及以上的类在实例化的过程中,相互需要各自实例化出的对象给自己的属性赋值,这种情况下就会产生循环依赖。

表现在代码上就是下面的两种情况:

public class A {
    B b = new B();
}
public class B {
    A a = new A();
}
public class Test {
    public static void main(String[] args) {
        new A(); // 或者 new B();
    }
}
public class A {
    B b;
    public A(B b){
        this.b = b;
    }
}
public class B {
    A a;
    public B(A a) {
        this.a = a;
    }
}

这种通过构造参数赋值,如果用无参的new(),在编译时就会报错,如果使用反射绕过编译器,真正执行的时候也会报错(找不到无参的构造)。

Spring里提到的唯一一种能产生循环依赖的场景,就是XML下注入方式为Constructor-based 的场景。后面会详细说明。

| 为什么会产生循环依赖?

要解释这个问题,就要了解对象初始化的过程,这里说的初始化是指类加载过程的最后一步 -- 赋值环节。这个环节主要会对下面的属性赋值:

  • static 修饰的属性、代码块,JVM会自动生成<clinit>方法,优先执行,而且只执行一次。final static 修饰的常量值(基础数据类型),在类加载时已经放在了class的常量池中了。

  • 采用 new 关键字赋值的属性,因为new、.newInstanse()等关键字会触发JVM的初始化动作,所以为了保证这个属性对应的类型存在且正确,要对此类型初始化后赋值给该属性。

问题的关键来了,赋值没结束,初始化的过程就算没结束,除非正常赋值结束,或者赋值过程中出现了异常。像常见的堆溢出、栈溢出都会导致初始化失败。循环依赖就属于赋值过程中产生了异常,导致初始化失败。

拓展一下:发生了循环依赖,是栈先溢出,还是堆先溢出?

答案是不确定,要看类的结构。

如果只有单纯的几个引用类型的属性,那就是先报 java.lang.StackOverflowError,因为默认的栈帧深度是1024,而几千个引用对象占用堆的空间几乎可以忽略不计;

如果有占用大量内存空间的 static 属性(比如一个10M的byte[]),按照上面初始化赋值的过程,那就有可能先报 java.lang.OutOfMemoryError。

||  Spring 是如何处理循环依赖问题的?

| 直接抛异常

Spring在这一小节中提到了循环依赖:

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-dependency-resolution

文档中指出,使用构造方法注入,可能会产生循环依赖,bean解析依赖的过程中如果发现循环依赖,处理手段很干脆,直接抛异常 BeanCurrentlyInCreationException ,并且建议你使用setter注入。 

The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  a defined in file [D:\yunsc\project\demo\target\classes\com\临时\A.class]
↑     ↓
|  b defined in file [D:\yunsc\project\demo\target\classes\com\临时\B.class]
└─────┘
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)
  at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1251)
  ......

| 那我们熟知的@Autowired又作何解释?

这个很简单,回忆一下 @Autowired 的使用场景,是不是只声明了属性,没有赋值。所以JVM在初始化类的过程中就没有为这些属性赋值的环节,也就是说循环依赖的必要条件被破坏了,所以就不可能再发生循环依赖了。

至于我们使用时,这个属性为什么不是null,那是因为在JVM实例化结束后,Spring 又从自己的 bean 工厂中拿到待赋值的属性对应的对象(就算重新new一个也不会产生循环依赖),通过反射给赋上值的。

所以 @Autowired 本质上还是 setter 注入。

||  总结

1、依赖注入和循环依赖没啥关系,依赖注入更侧重于由外部触发的赋值动作,而循环依赖是编码问题的一种表象。

2、如果真的发生循环依赖了,就只能等着堆栈溢出报错结束了。

3、Spring 只是用自己的注入方式避免了循环依赖,而不是解决了循环依赖。

4、就像死锁,一旦发生,就是无解的,只能说是编码过程中如何避免死锁。

参考资料:

《Dependency injection》 -- Dhanji R. Prasanna

《The Java Virtual Machine Specification(Java SE 8 Edition)》 -- Tim Lindholm、Frank Yellin、Gilad Bracha、Alex Buckley [5.5]

《深入理解Java虚拟机》 -- 周志明 [7.3]

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-dependencies

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值