Spring 依赖注入详解
/ 依赖关系,指的就是对象之间的相互协作关系依赖注入(DI)是一个过程,在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象被实例化后通过属性设置来定义它们的依赖项(即与该对象一起工作的其他对象)。然后,容器在创建 bean 时注入这些依赖项。这个过程基本上是与对象直接通过构造类或等机制来控制其依赖项的实例化或位置是相反的,因此得名控制反转。// 对象不直接创建自己,而是通过 Spring
目录
// 依赖关系,指的就是对象之间的相互协作关系
依赖注入(DI)是一个过程,在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象被实例化后通过属性设置来定义它们的依赖项(即与该对象一起工作的其他对象)。然后,容器在创建 bean 时注入这些依赖项。这个过程基本上是与对象直接通过构造类或等机制来控制其依赖项的实例化或位置是相反的,因此得名控制反转。// 对象不直接创建自己,而是通过 Spring 容器创建,那么 Spring 容器是如何创建对象的?
使用 DI 原则,代码会更简洁,当对象具有依赖关系时,也能更有效的解耦。对象不检索它的依赖项,也不知道依赖项的具体类或者位置。因此,你的类将变得更容易测试,特别是当依赖关系建立在接口或者抽象基类上时,他们的子类或者模拟实现将允许在单元测试中被使用。// 依赖注入最大的好处,就是代码可以解耦合变得更加简洁
DI 主要有两种变体:基于构造函数的依赖注入和基于 Setter 方法的依赖注入
1、基于构造器的依赖注入
基于构造函数的 DI 是通过容器调用构造函数来实现的,如果构造函数带有参数,那么每个参数都表示一个依赖项。通过特定参数调用静态工厂方法去构造 bean 也是一样的,这两种方式并没有什么本质上的不同。下面例子展示了一个只通过构造函数来进行依赖注入的类:
public class SimpleMovieLister {
// SimpleMovieLister依赖于MovieFinder
private final MovieFinder movieFinder;
// 构造函数,Spring容器可以注入MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 实际使用 MovieFinder 对象的商业逻辑被省略...
}
这个类它是只一个不依赖容器中特定接口、基类或注释的普通 POJO 类,并无特别之处。
构造函数的参数解析
构造函数的参数解析是通过匹配参数的类型进行的。如果在 bean 定义中的构造函数参数不存在歧义,那么在 bean 定义中的参数顺序就是在实例化 bean 时向构造函数提供的参数的顺序。例如下边这个类:// 通过匹配的参数类型进行构造函数解析
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
假设 ThingTwo 和 ThingThree 类之间没有继承关系,不存在潜在的歧义。那么,下边的配置将会正常工作。在该配置中,你不需要在 <constructor-arg/> 标签中去显式指定构造函数参数的索引或类型。// 不用显示的指定参数的索引或者类型
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
当引用一个 bean 时,它的类型是已知的,可以通过类型进行匹配。但是,当使用简单类型时,例如 <value> true </value>, Spring 就无法确定值的类型,所以,Spring 在没有帮助的情况下无法按类型进行匹配。例如以下类: // 可以通过参数类型、参数位置、参数名字进行匹配
package examples;
public class ExampleBean {
// 计算最终答案的年数
private final int years;
// 最终的答案:生命、宇宙和一切的答案
private final 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>
除了解决多个简单值的模糊性外,指定索引还解决了构造函数具有两个相同类型参数时的模糊性。
(3)构造函数参数的名字匹配
你也可以使用构造函数的参数名字来消除值的歧义,如下面的例子所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
请记住,如果要想开箱即用,必须在编译代码时启用调试标志,这样 Spring 才能从构造函数中查找到参数名。如果不能或不希望用调试标志编译代码,可以使用 JDK 中的 @ConstructorProperties 注释显式的命名构造函数参数。示例如下所示:// 这样的写法还真是罕见,不理解,不过不影响继续阅读
package examples;
public class ExampleBean {
// 省略全局属性...
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
2、基于 Setter 方法的依赖注入
基于 setter 方法的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法实例化 bean 之后,再调用 bean 上的 setter 方法来实现的。// 其实就是实例化后的属性赋值,需要无参构造函数
下面示例展示了一个通过 setter 方法进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 实际使用注入的 MovieFinder 的业务逻辑被省略了...
}
ApplicationContext 支持基于构造函数和基于 setter 方法的依赖注入。当一个 Bean 通过构造函数注入了一些依赖项后,还可以通过 setter 方法进行属性赋值。你可以通过 BeanDefinition 的形式配置一个 Bean 依赖项,将其与 PropertyEditor 实例一起使用,能够将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类,而是使用 XML 配置、Spring 注解(例如,使用 @Component、@Controller 等注释的类),或者 Java 代码配置(在 @Configuration 标记的类中使用 @Bean 注解的方法)。然后,在内部将这些配置转化为 BeanDefinition,然后将创建的实例加载到 Spring 容器中。// 最常见的方法还是通过配置文件进行配置。
3、使用构造器注入还是 setter 方法注入?
构造器注入和 setter 方法注入两者可以混合使用,对于强制依赖项推荐使用构造器注入,对于可选依赖项则推荐使用 setter 方法注入,这是一个非常好的经验规则。虽然,在 setter 方法上使用 @Required 注释也可以使属性成为必需依赖项;但是,通过构造器来注入必须依赖项显然更加的合适。// 强制依赖使用构造器注入,可选依赖使用 setter 方法注入
Spring 官方推荐使用构造器注入,通过构造器注入会将应用程序中的对象实现为不可变对象,并确保对象的必须属性不为空。而且,通过构造器注入返回的实例总是完全初始化后的对象(非半成品)。当然,构造器拥有太多参数是一种不好的代码设计,意味着该构造类负责的功能太多,需要进行代码重构,以便于使类的职责更加单一。// 构造器注入的优势
Setter 方法主要应用于可选的依赖项注入,同时,这些依赖项需要在类中分配合理的默认值。如果在不分配默认值的情况下,那么需要在代码使用该依赖项的所有地方都执行非空检查。使用 Setter 方法注入的好处是,该方法可以使对象在之后进行重新配置或重新注入。因此,通过 JMX MBeans 进行对 setter 方法注入进行管理是一种非常不错的方式。
根据不同的类去选择不同的注入方式。有时,在处理没有源代码的第三方类时,你需要做出选择。例如,如果第三方类不公开任何 setter 方法,那么构造器注入是唯一可选用的注入方式。
4、依赖注入解析的过程
容器执行 bean 依赖解析的过程如下:
- 通过配置文件配置的 bean 元数据创建并初始化 ApplicationContext,元数据配置格式可以是 XML、Java代码或 Spring 注解。
- 对于每个 bean,它的依赖关系都是通过属性、构造器参数或静态工厂方法参数的形式表示(如果创建实例使用静态工厂方法)。当实际创建 bean 时,将向 bean 提供这些依赖项。
- 每一个属性或者构造器参数都需要去设置一个实际的值,或者引用容器中的另一个 bean。
- 设置的值会根据指定的格式,转化为该属性或者构造器参数的实际类型。默认情况下,Spring 可以将字符串格式的值转换为所有的内置类型,比如 int,long,String,boolean 等等。
当前容器创建的时候,Spring 容器会校验每个 bean 的配置。但是,Spring 容器在实际创建 bean 之前,并不会去设置 bean 的属性。当容器创建的时候,只有单例 Bean 会被提前创建(pre-instantiated),其他域(Scopes)的 Bean 只有被访问到时才会去创建。创建一个 Bean 的时候,该 Bean 的依赖项,以及依赖项的依赖项都会被创建和分配。这种解析机制,会导致当创建一个 Bean 的依赖项不匹配时,可能需要经过一系列的解析之后才能被发现。// 依赖注入过程中,了解哪些 bean 将会被创建
循环依赖问题
如果使用构造器进行注入,就有可能出现循环依赖的情况。
例如:class A 通过构造函数注入,需要 class B 的实例,class B 通过构造函数注入,需要 class A 的实例。如果将 class A 和 class B 的 bean 定义配置为相互注入,那么 Spring IoC 容器会在运行时检测到这个循环引用,并抛出 BeanCurrentlyInCreationException 异常。// 循环依赖场景
一种可行的解决方法是修改其中一些类的源代码,把构造器注入改成 setter 方法注入,或者涉及到循环依赖的所有类都使用 setter 方法注入,尽管这样做不推荐,但是使用 setter 方法注入可以有效的解决循环依赖的问题。// 因为 setter 方法是在对象实例化后才设置属性的
与普通的情况相比,循环依赖关系要求 bean A 或 bean B 在注入之前就已经被完全的实例化(典型的是先有鸡还是先有蛋的场景)。
Spring 会在容器加载时检测配置问题,例如,引用的 bean 不存在,bean 之间存在循环依赖等。当实际创建 bean 的时候,Spring 会尽可能晚的去设置 bean 的属性和解析依赖项。这意味着,如果创建 bean 或 bean 的依赖项时出现了问题,Spring 容器可以在用到该 bean 的时侯再抛出异常。但是,这样做可能导致配置问题不能及时被发现,这也是为什么 ApplicationContext 在创建容器时,需要把 bean 提前实例化(pre-instantiated)的原因。默认情况下,在创建 ApplicationContext 容器时就可以发现 bean 的配置问题,不过这样做,需要耗费一些时间和内存去创建一些不被马上用到的 bean。当然,你也可以替换这个默认行为,让一些 bean 实现懒加载而不是过早的提前实例化。// 提前实例化可以及时发现配置问题,这一段很重要
在没有循环依赖的情况下,将一个或多个 bean 注入到另一个 bean 中时,每个 bean 在注入之前就已经被完全实例化了。也就是说,如果创建 bean A 需要注入 bean B,那么 Spring 容器会在调用 bean A 的 setter 方法之前,就已经完全实例化了 bean B。一个 bean 被完全实例化,意味着它的依赖关系已经确定,相关的生命周期方法也已经被调用(例如配置的 init 方法或 InitializingBean 回调方法等)。// 完全被实例化的 bean,属性和依赖关系都已经被确定好了
5、依赖注入的相关示例
使用 xml 配置的 setter 方法注入,在 xml 配置文件中指定了一些 bean 定义,如下所示:
<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"/>
下面的示例显示了相应的 ExampleBean 类:
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 方法与 xml 文件配置的属性一一匹配。下面的例子使用了基于构造器的依赖注入:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- 使用嵌套ref元素的构造函数注入 -->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- 使用更整洁ref属性的构造函数注入 -->
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
下面的示例显示了相应的 ExampleBean 类:
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;
}
}
现在考虑另一个情况,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"/>
下面的示例显示了相应的 ExampleBean 类:
public class ExampleBean {
// 私有构造函数
private ExampleBean(...) {
...
}
// 静态工厂方法;
// 方法的参数可以被认为是返回bean的依赖项,而不用它们是怎么被使用的.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
工厂方法的参数由 <constructor-arg/> 标签提供,就像实际使用了构造函数一样。由工厂方法返回的 bean 的类型不必与原类的类型相同(尽管在本例中它是相同的)。非静态的实例工厂方法跟静态的工厂方法使用的方式基本相同,所以不再详细描述。
更多推荐
所有评论(0)