摘要:这篇文章主要是总结了一下Java依赖注入的相关知识点。
前言:依赖注入是用来做对象管理的,为啥要管理呢?面向对象本质上是要多个不同职责相互对立的Object互相通过发消息来进行交互的。但是这个思想落地时,因为性能的原因,Object之间并不像网络那样真的发消息,而是互相调用对方的方法(C++,Java,C#等都是这么做的)。如果想调用某个Object的方法,就得拿到那个Object的引用。那么怎么拿到Object的引用呢?new一个不就可以了嘛。但现实当中比这个更复杂。比如一个系统需要一个“paymentService”的对象做支付,也许整个系统只需要一个paymentService的实例,所以必须得判断那个要用的Object有没有,没有才创建。但有时,对于一些Object可能每次都需要一个新的实例。两种策略之间,也许是某种“池化Object“的特定的逻辑,比如连接池只能有100个Connection Object。每个对象要初始化,获取一些资源,也可能某些时候释放一些资源。这些都是对Object生命周期管理的逻辑。如果没有注入机制,那么这些逻辑会散播到整个系统每一个角落,带来巨大的维护问题。
依赖注入的方法
注:下面例子主要以spring为例。
1、setter注入
1 |
|
这种注入方式是在Spring3.x刚推出的时候,推荐使用的注入方式。
2、field注入
1 |
|
这种注入方式非常简单,只要加入要注入的字段,附上@Autowired,即可完成。
3、构造器注入
1 |
|
这种注入方式是在Spring4.x版本中推荐使用的注入方式。
使用构造器注入的优点:
- 能够保证注入的组件不可变。
- 确保需要的依赖不为空。
- 构造器注入的依赖总是能够保证完全初始化的状态。
为什么推荐使用构造器注入?
field注入
优点:注入简单。简洁明了。
缺点:循环依赖、重名依赖、依赖为空。构造器注入
优点:初始化、不可变性、数据检查、依赖不为空。
缺点:循环依赖、注入的一多就非常的臃肿。setter注入
优点:通过调用,可以让代码控制类依赖的顺序,解决循环依赖。
缺点:注入的一多就非常的臃肿、依赖可变性,不够安全。
构造器注入和field注入的循环依赖的报错提示也有点不同,前者编译时就报错,后者使用时报错。
所以说为什么使用构造器注入呢,主要是因为:
- 保证依赖不可变(final关键字)。
- 保证依赖不为空(省去了我们对其检查)。
- 保证返回客户端(调用)的代码的时候是完全初始化的状态。
- 如果产生了循环依赖的问题,可以在编译是就知道,这样可以及时解决(尽量把错误在编译时就发现才是最好的开发习惯!机器的可信度远高于人类!)。
来自spring官方的解释:
We usually advise people to use constructor injection for all mandatory collaborators and setter injection for all other properties. Again, constructor injection ensures all mandatory properties have been satisfied, and it is simply not possible to instantiate an object in an invalid state (not having passed its collaborators). In other words, when using constructor injection you do not have to use a dedicated mechanism to ensure required properties are set (other than normal Java mechanisms).
具体详情可以参考一下这篇文章:https://spring.io/blog/2007/07/11/setter-injection-versus-constructor-injection-and-the-use-of-required/
循环依赖
1、什么是循环依赖?
循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图:

注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。
循环依赖场景有:
- 构造器的循环依赖。
- field属性的循环依赖。
其中,构造器的循环依赖问题无法通过框架自动去解决,但是它会在编译时就报错,我们可以人为的解决它。在解决属性循环依赖时,Spring通过将实例化后的对象提前暴露给Spring容器中的singletonFactories,解决了循环依赖的问题。
2、Spring怎么解决循环依赖
Spring的循环依赖的理论依据基于Java的引用传递,当获得对象的引用时,对象的属性是可以延后设置的(但是构造器必须是在获取引用之前)。
Spring的单例对象的初始化主要分为三步:

- createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象。
- populateBean:填充属性,这一步主要是多bean的依赖属性进行填充。
- initializeBean:初始化,调用spring xml中的init 方法。
从上面单例bean的初始化可以知道:循环依赖主要发生在第一、二步,也就是构造器循环依赖和field循环依赖。那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。
三级缓存
三级缓存分别指:
1 | 三级缓存主要指: |
- singletonFactories:单例对象工厂的cache。
- earlySingletonObjects:提前暴光的单例对象的Cache。
- singletonObjects:单例对象的cache。
- 在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。
- 如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。
- 如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()获取,如果获取到了则从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。
从上面三级缓存的分析中,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory。这里就是解决循环依赖的关键,发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。
这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况:
A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存 earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象完成了初始化。
知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。

参考内容
https://www.zhihu.com/question/27053548
https://www.cnblogs.com/zzq6032010/p/11406405.html
https://blog.csdn.net/qq_33808244/article/details/102453052
- 本文作者: th3ee9ine
- 本文链接: https://www.blog.ajie39.top/2021/05/05/Java依赖注入详解/
- 版权声明: 本博客所有文章除特别声明外,均采用 LICENSE 下的许可协议。转载请注明出处!