ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
依赖注入(DI)是一个处理对象的依赖的过程,换句话说就是处理与某个对象一起工作的其它对象。所依赖的对象可以通过构造参数、工厂方法参数以及设置对象实例的属性来定义。当一个对象被创建时,IOC 容器会注入它的依赖,这个过程与传统创建对象的方式是相反的故称为控制反转(IOC)。 &emsp; 使用DI后代码十分的简洁,并且在定义依赖时也能有效的解耦。对象不需要过多的去关注它依赖的对象,所以类更容易测试,特别是依赖于接口或者是抽象类时,可以在单元测试中使用stub或者mock实现。 &emsp; DI存在两种主要的形式:基于构造器注入和Setter方法注入。 * * * * * &emsp; ### **基于构造器注入** 构造注入通过容器提供指定的参数调用构造方法来完成,每个参数都称为依赖。这和通过提供给静态的工厂方法的参数来创建对象几乎是等价的。下面的样例展示了一个类通过构造方法来完成依赖注入的过程。 ~~~ public class SimpleMovieLister { // SimpleMovieLister依赖于MovieFinder private MovieFinder movieFinder; // Spring通过构造器注入MovieFinder实例 public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } //使用被注入MovieFinder实例来完成的业务逻辑已被忽略 } ~~~ 注意:这个类是普通的Java对象,不依赖于容器相关的接口、基类或者注解。 ### **构造参数的识别** 构造参数的识别是通过类型匹配来完成的,如果没有模糊不清的构造参数,那么构造参数的定义顺序就是容器实例化Bean时提供参数给构造器的顺序。参考如下代码: ~~~ package x.y; public class Foo { public Foo(Bar bar, Baz baz) { // ... } } ~~~ 现在,参数都是明确的。假定`Bar`和`Baz`没有继承关系,因此下面的配置没有任何问题,你也不需要在`<constructor-arg/>`元素中指定参数的索引和类型。 ~~~ <beans> <bean id="foo" class="x.y.Foo"> <constructor-arg ref="bar"/> <constructor-arg ref="baz"/> </bean> <bean id="bar" class="x.y.Bar"/> <bean id="baz" class="x.y.Baz"/> </beans> ~~~ 当引用的Bean的类型是确定的,这能很好的进行参数匹配。如果使用的是简单类型,比如:`<value>true</value>`,Spring就不能判断其类型了,所以不能独自完成类型匹配。参考如下代码: ~~~ package examples; public class ExampleBean { //计算最终答案的年份 private int years; //生活、宇宙、万物的答案 private String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ~~~ ##### 1.构造参数类型匹配 在上面的例子中,如果你通过`type`属性指定参数的类型,那么容器可以很好的进行简单参数类型的类型匹配。参考如下代码: ~~~ <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg type="int" value="7500000"/> <constructor-arg type="java.lang.String" value="42"/> </bean> ~~~ ##### 2.构造参数索引 使用`index`属性指定参数在构造器中的索引,参考如下代码: ~~~ <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg index="0" value="7500000"/> <constructor-arg index="1" value="42"/> </bean> ~~~ > 如果不指定index的值,则默认以<constructor-arg/>出现的顺序为参数索引。 为了解决多个简单类型间的歧义性,可以通过指定值在构造参数中的索引。注意:索引从0开始。 ##### *3.构造参数名* 你也可以指定值所对应构造器中的参数名来消除歧义: ~~~ <bean id="exampleBean" class="examples.ExampleBean"> <constructor-arg name="years" value="7500000"/> <constructor-arg name="ultimateAnswer" value="42"/> </bean> ~~~ 注意:要使上述样例正常工作,必须启用调试标记来编译代码,这样Spring才能从构造器中查找到参数名。否则你必须使用`@ConstructorProperties` JDK注解来显式指定参数名,参考如下代码: ~~~ package examples; public class ExampleBean { // 忽略属性 @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ~~~ ***** &emsp; ### **基于Setter方法注入** 容器通过无参构造方法或者无参静态工厂方法实例化Bean之后,再通过调用Setter方法来完成依赖注入。 下面的样例展示了一个类通过Setter方法来完成依赖注入。注意:这个类是普通的Java对象,不依赖于容器相关的接口、基类或者注解。 ~~~ public class SimpleMovieLister { // SimpleMovieLister依赖于MovieFinder private MovieFinder movieFinder; // Spring通过Setter方法注入MovieFinder实例 public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } //使用被注入MovieFinder实例来完成的业务逻辑已被忽略 } ~~~ `ApplicationContext`支持构造注入和Setter方法注入,也支持通过构造参数注入某些依赖后再通过Setter注入其它依赖。配置的依赖存在于`BeanDefinition`中,可以结合`PropertyEditor`完成属性的转换。然而,大多开发者并不会直接使用这些类(即以编程的方式)而是使用XML、被注解的组件(即被`@Component`、 `@Controller`注解的类)以及带有`@Configuration`的类中`@Bean`注解的方法来定义Bean,这些配置在内部被转换为`BeanDefinition`实例并被用来加载整个Spring IOC容器。 <p style="margin-bottom:0px">&emsp;</p> >### :-: **使用构造注入还是Setter方法注入?** > > 尽管可以使用混合注入,但是通过**构造方法完成必要依赖的注入**,通过**Setter方法完成可选依赖的注入**是一个好的编程方式。注意:在Setter方法上标注`@Required`([1.9.1.@Required](1.9.1.Required.md))注解使得属性为必需依赖。不过,使用构造注入(带有编程式参数验证)更可取。 > **Spring团队提倡使用构造注入**,原因如下: > 1.该方式可以使得依赖为不可变对象并且不会为空; > 2.通过构造注入的依赖的对象被客户端调用时都是完全初始化好的; > 3.它还可以作为启示:过多的构造参数是一种不好的编程方式,意味着此类拥有过多的职责,最好重构代码,将多余的职责分离开来。 > &emsp; > Setter注入主要用于可选依赖,但是在类中也应该分配合理的默认值,否则在客户端使用此依赖时需要做非空检查。使用Setter注入有一个好处:可以重新配置或者重新注入依赖。通过JMX mbean进行管理是Setter注入的一个引人注目的用例。 > DI只在大多场景下是有效的,当处理没有源码的第三方类时,可以选择其它注入方式。比如:第三方类没有暴露任何Setter方法,那只能通过构造注入了。 ***** &emsp; ### **依赖处理过程** 容器处理依赖的的细节如下: * ` ApplicationContext`通过配置元数据来创建和初始化,配置元数据可以通过XML、Java代码、注解来指定。 * 每个Bean的依赖都表示为:属性、构造参数、静态工厂方法参数。当Bean创建后容器会注入其依赖。 * 每个属性、构造参数都是需要设置的值或者需要引用容器中的其它Bean。 * 每个属性、构造参数的值都被从指定的类型转换成实际类型。默认情况下,Spring可以将字符串类型的值转换为`int`,`long`,`String`,`boolean`等。 Spring容器被创建时会验证每个Bean的配置,然而Bean的属性在Bean没有创建之前不会设置。Bean默认是单例的并且会在容器创建时初始化(关于Bean的作用域将在[1.5.Bean的范围](1.5.Bean的范围.md)详细解释),非单例的Bean将会在被客户请求获取时创建。Bean的创建可能会一系列Bean(Bean的依赖的依赖......的依赖)被创建。注意,这些依赖项之间的不匹配解析可能在较晚的时候才出现,例如在第一次创建受影响的Bean时。 <p style="margin-bottom:0px">&emsp;</p> >### :-: **循环依赖** >如果通过构造器完成依赖注入,这可能会出现循环依赖的情况。 > >比如:类A通过构造注入类B的实例,而类B也通过构造注入类A的实例。容器在运行时会检查出循环引用并抛出`BeanCurrentlyInCreationException`。 >&emsp; >一个可行的解决方案:编辑源码将一些类的构造注入替换成Setter注入。此外,还可以避免使用构造注入而只使用Setter注入。换而言之,可以通过Setter注入配置循环依赖,虽然这是不推荐的。 >&emsp; >和典型的场景(没有循环依赖)不同,Bean A和Bean B之间的循环依赖关系迫使在完全初始化之前将其中一个Bean注入到另一个Bean中,这是一个矛盾的问题(这就像一个经典的问题:先有鸡还是先有蛋)。 > 开发者可以信任Spring做的事都是正确的。在容器加载时会检查不存在依赖和循环依赖等问题。当一个Bean被创建时,Spring会尽可能晚的设置属性和解析依赖。这意味着Spring容器被正确的加载后,当你请求一个对象时,如果创建该对象或者它的一项依赖出现问题,生成异常也会更晚。例如:当Bean的属性缺失或者无效时会抛出异常。所以一些配置问题可能延迟可见,这也就是为什么`ApplicationContext`的实现默认提前实例化单例Bean的原因。在容器创建之时花费一些时间和内存创建Bean可以提前发现问题。你仍然可以覆盖提前初始化的行为,将单例Bean的提前加载转变为延迟加载。 &emsp; 如果没有循环依赖存在,一个Bean在注入到依赖Bean之前就已经完成初始化好了。这意味着,如果Bean A依赖Bean B,在调用Bean A 的Setter方法之前,Bean B已是完全可用的。换而言之,当一个Bean初始化完成时,它的依赖已经设置好了,相关的生命周期方法(比如:初始化回调方法,详情参见:[1.6.1.生命周期回调函数](1.6.1.生命周期回调函数.md))也调用完毕。 ***** &emsp; ### **依赖注入的例子** 下面的样例展示了基于XML配置的Setter注入。 ~~~ <bean id="exampleBean" class="examples.ExampleBean"> <!-- 使用ref元素完成Setter注入--> <property name="beanOne"> <ref bean="anotherExampleBean"/> </property> <!-- 使用更简洁的ref属性完成Setter注入 --> <property name="beanTwo" ref="yetAnotherBean"/> <property name="integerProperty" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~ public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public void setBeanOne(AnotherBean beanOne) { this.beanOne = beanOne; } public void setBeanTwo(YetAnotherBean beanTwo) { this.beanTwo = beanTwo; } public void setIntegerProperty(int i) { this.i = i; } } ~~~ 在上面的样例中,Setter方法和和`<property/>`对应。下面的样例采用构造注入。 ~~~ <bean id="exampleBean" class="examples.ExampleBean"> <!-- 使用ref元素完成Setter注入--> <constructor-arg> <ref bean="anotherExampleBean"/> </constructor-arg> <!-- 使用更简洁的ref属性完成Setter注入 --> <constructor-arg ref="yetAnotherBean"/> <constructor-arg type="int" value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~ public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public ExampleBean( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { this.beanOne = anotherBean; this.beanTwo = yetAnotherBean; this.i = i; } } ~~~ Bean定义指定的构造器参数会作为ExampleBean构造器中的参数。 现在请参考另一个例子,Spring通过静态工厂来代替构造器创建实例。 ~~~ <bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance"> <constructor-arg ref="anotherExampleBean"/> <constructor-arg ref="yetAnotherBean"/> <constructor-arg value="1"/> </bean> <bean id="anotherExampleBean" class="examples.AnotherBean"/> <bean id="yetAnotherBean" class="examples.YetAnotherBean"/> ~~~ ~~~ public class ExampleBean { // 私有构造器 private ExampleBean(...) { ... } /** * 静态工厂方法; * 这个方法的参数可以被认为是待创建Bean的依赖项, * 不管这些参数实际是如何使用的的。 */ public static ExampleBean createInstance (AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { ExampleBean eb = new ExampleBean (...); // 其它操作...... return eb; } } ~~~ `<constructor-arg/>`提供给静态工厂方法的参数和构造器实际使用的参数是一致的。从工厂方法返回的对象类型可以和包含静态工厂方法的类不一致,尽管在本例子中是一致的。实例(非静态)工厂方法的用法和静态工厂方法本质上是一样(使用`factory-bean` 代替`class`属性),故不再此详述。