接着上一篇博客:https://blog.csdn.net/qq_43605444/article/details/121938448?spm=1001.2014.3001.5502
5、依赖关系
典型的企业应用程序不包含单个对象(或 Spring 用语中的 bean)。 即使是最简单的应用程序也有一些对象,它们协同工作以呈现最终用户所看到的连贯应用程序。 下一节将解释如何从定义多个独立的 bean 定义到完全实现的应用程序,其中对象协作以实现目标。
5.1 依赖注入
依赖注入 (DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或 从工厂方法返回。 然后容器在创建 bean 时注入这些依赖项。 这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。
使用 DI 原则,代码更清晰,当对象具有依赖关系时,解耦更有效。 该对象不查找其依赖项,也不知道依赖项的位置或类。 因此,您的类变得更容易测试,特别是当依赖项位于接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。
DI 存在两种主要的变体:基于构造函数的依赖注入和基于 Setter 的依赖注入。
5.1.1 基于构造函数的依赖注入
基于构造函数的 DI 是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。 调用带有特定参数的静态工厂方法来构造 bean 几乎是等效的,本讨论将类似地处理构造函数和静态工厂方法的参数。 以下示例显示了一个只能使用构造函数注入进行依赖注入的类:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
请注意,这个类没有什么特别之处。 它是一个不依赖于容器特定接口、基类或注释的 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 无法确定值的类型,因此无法在没有帮助的情况下按类型进行匹配。 考虑以下类:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
这个时候 Spring 无法确定值的类型,注入构造器时会报错,需要指明值的类型,如下。
- 构造函数参数类型匹配
在上述场景中,如果您通过 type
属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如下例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
这个时候,根据构造函数的参数类型进行注入,顺序不一定要按构造方法的参数顺序。
- 构造函数参数索引
您可以使用该 index
属性显式指定构造函数参数的索引,如以下示例所示:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
除了解决多个简单值的歧义之外,指定索引还可以解决构造函数具有两个相同类型参数的歧义。
该索引是从 0 开始的。
- 构造函数参数名称
您还可以使用构造函数参数名称来消除值歧义,如下例所示:
<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 {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
5.1.2 基于 Setter 的依赖注入
基于 Setter 的 DI 是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。
以下示例显示了一个只能使用纯 setter 注入进行依赖注入的类。 这个类是传统的Java。 它是一个不依赖于容器特定接口、基类或注释的 POJO。
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;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
ApplicationContext 为其管理的 bean 支持基于构造函数和基于 setter 的 DI。 在已经通过构造函数方法注入了一些依赖项之后,它还支持基于 setter 的 DI。 您以 BeanDefinition 的形式配置依赖项,将其与 PropertyEditor 实例结合使用以将属性从一种格式转换为另一种格式。 但是,大多数 Spring 用户不直接(即以编程方式)使用这些类,而是使用 XML bean 定义、带注释的组件(即用 @Component、@Controller 等注释的类)或 @Bean 方法。 基于 Java 的 @Configuration 类。 这些源然后在内部转换为 BeanDefinition 的实例,并用于加载整个 Spring IoC 容器实例。
那么究竟是选择基于构造器的注入还是选择基于set方式的注入呢?这里官方给出了我们建议:
- 如果你的 set 注入参数是必须的,那么你可以在 set 方法上加一个 @Required 注解来保证这个参数一定被注入;
- 最好使用带有参数编程式验证的构造函数注入,这样可以保证你需要的参数一定被注入;
- 大量的构造函数参数是一种不好的代码味道,这意味着类可能有太多的责任,应该对其进行重构,以更好地解决问题的适当分离。
- Setter 注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。
- 使用对特定类最有意义的 DI 样式。有时,在处理您没有源代码的第三方类时,需要为您做出选择。例如,如果第三方类没有公开任何 setter 方法,那么构造函数注入可能是唯一可用 DI 形式。
5.1.3 依赖解析过程
容器按照如下方式执行bean依赖解析:
- 使用
ApplicationContext
描述所有 bean 的配置元数据创建和初始化。配置元数据可以由 XML、Java 代码或注解指定。 - 对于每个 bean,它的依赖关系以属性、构造函数参数或静态工厂方法的参数(如果您使用它而不是普通构造函数)的形式表示。在实际创建 bean 时,将这些依赖关系提供给 bean。
- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
- 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如
int
、long
、String
、boolean
等。
在创建容器时,Spring 容器会验证每个 bean 的配置。 然而,在真正创建 bean 之前,不会设置 bean 属性本身。 创建容器时会创建单例范围并设置为预实例化的 Bean(默认)。 作用域在 Bean 作用域中定义。 否则,仅在请求时才创建 bean。 创建 bean 可能会导致创建 bean 图,因为 bean 的依赖项及其依赖项的依赖项(等等)被创建和分配。
请注意,这些依赖项之间的解析不匹配可能会出现较晚 ,也就是说,在第一次创建受影响的 bean 时。
循环依赖
- 如果主要使用构造函数注入,可能会创建无法解析的循环依赖场景。
例如:类 A 需要一个类 B 通过构造函数注入的实例,而类 B 需要一个类 A 通过构造函数注入的实例。如果您将 bean 配置为类 A 和 B 相互注入,Spring IoC 容器将在运行时检测这个循环引用,并抛出一个BeanCurrentlyInCreationException 异常。- 一种可能的解决方案是编辑某些类的源代码,由 setter 注入而不是构造函数来注入。另外,避免构造函数注入,只使用 setter 注入。换句话说,尽管不建议这样做,但您可以使用 setter 注入配置循环依赖项。
- 与典型的情况(没有循环依赖关系)不同,bean A 和 bean B 之间的循环依赖关系迫使一个 bean 在完全初始化之前被注入到另一个 bean 中(典型的先有鸡还是先有蛋的场景)。
- 下面来模拟一下循环依赖
首先要有两个 JavaBean :
@AllArgsConstructor
public class Checken {
private Egg egg;
}
@AllArgsConstructor
public class Egg {
private Checken checken;
}
然后再 xml 中配置:
<!-- 循环依赖 -->
<bean id="checken" class="com.spring.example.pojo.Checken">
<constructor-arg name="egg" ref="egg"/>
</bean>
<bean id="egg" class="com.spring.example.pojo.Egg">
<constructor-arg name="checken" ref="checken"/>
</bean>
这就是典型的循环案例。
现在我们将注入方式改为 set 注入解决问题:
<bean id="egg" class="com.spring.example.pojo.Egg">
<property name="checken" ref="checken"/>
</bean>
<bean id="checken" class="com.spring.example.pojo.Checken">
<property name="egg" ref="egg"/>
</bean>
您通常可以相信 Spring 会做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖。 Spring 在真正创建 bean 时尽可能晚地设置属性并解析依赖项。这意味着,如果创建该对象或其依赖项之一时出现问题,则已正确加载的 Spring 容器稍后可以在您请求对象时生成异常财产。某些配置问题的这种潜在延迟可见性是 ApplicationContext 实现默认预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预实例化。
如果不存在循环依赖项,那么当一个或多个协作 bean 被注入一个依赖 bean 时,每个协作 bean 在被注入依赖bean 之前都要完全配置好。这意味着,如果 beanA 依赖 bean B , Spring IoC 容器会优先完全配置 bean B ,然后再调用 bean A 的 setter 方法。换句话说,bean 实例化(如果它不是一个单例预先实例化),他的依赖项就被设置,相关的生命周期方法(如 InitializingBean init 方法或配置回调方法)调用。
5.1.4 依赖注入的例子
以下示例将基于 XML 的配置元数据用于基于 setter 的 DI。Spring XML 配置文件的一小部分指定了一些 bean 定义,如下所示:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<!-- setter injection using the neater ref attribute -->
<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 文件中指定的属性匹配。以下示例使用基于构造函数的 DI:
<bean id="exampleBean" class="examples.ExampleBean">
<!-- constructor injection using the nested ref element -->
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<!-- constructor injection using the neater ref attribute -->
<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;
}
}
在 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"/>
以下示例显示了相应的 ExampleBean
类:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
静态工厂方法的参数由 <constructor-arg/>
元素提供,与实际使用构造函数完全相同。 工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本示例中是)。 实例(非静态)工厂方法可以以基本相同的方式使用(除了使用 factory-bean 属性而不是 class 属性),因此我们不在这里讨论这些细节。
5.2 依赖关系和配置的详细信息
如上一节所述,您可以将 bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用或作为内联定义的值。 为此,Spring 的基于 XML 的配置元数据支持其 <property/>
和 <constructor-arg/>
元素中的子元素类型。
5.2.1 直接值(原语、字符串等)
<property/>
元素的 value 属性将属性或构造函数参数指定为人类可读的字符串表示形式。 Spring 的转换服务用于将这些值从 String 转换为属性或参数的实际类型。 以下示例显示了正在设置的各种值:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="misterkaoli"/>
</bean>
以下示例使用 p 命名空间
进行更简洁的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="misterkaoli"/>
</beans>
前面的 XML 更简洁。 但是,拼写错误是在运行时而不是设计时发现的,除非您在创建 bean 定义时使用支持自动属性完成的 IDE(例如 IntelliJ IDEA 或 Spring Tools for Eclipse)。 强烈推荐此类 IDE 帮助。
您还可以配置一个 java.util.Properties
实例,如下所示:
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>
Spring 容器使用 JavaBeans PropertyEditor 机制将 <value/>
元素内的文本转换为 java.util.Properties 实例。 这是一个很好的捷径,并且是 Spring 团队支持使用嵌套 元素而不是 value 属性样式的少数几个地方之一。
如果 value 值中存在歧义的符号,可以考虑进行转义,或者是将带特殊符号的值写到 CDATA 中,如下:
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="address">
<value>
<![CDATA[<<南京>>]]>
</value>
</property>
</bean>
5.2.2 idref 元素
idref 元素只是一种将容器中另一个 bean 的 id(字符串值 - 而不是引用)传递给 <constructor-arg/>
或 <property/>
元素的防错方式。 以下示例显示了如何使用它:
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>
前面的 bean 定义片段与以下片段完全等效(在运行时):
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>
第一种形式比第二种形式更可取,因为使用 idref 标记让容器在部署时验证引用的命名 bean 实际存在。 在第二个变体中,不对传递给客户端 bean 的 targetName 属性的值执行验证。 只有在实际实例化客户端 bean 时才会发现拼写错误(最有可能是致命的结果)。 如果客户端 bean 是原型 bean,则可能只有在部署容器很久之后才能发现此错误和由此产生的异常。
4.0 beans XSD 不再支持 idref 元素上的 local 属性,因为它不再提供常规 bean 引用的值。 升级到 4.0 架构时,将现有的 idref 本地引用更改为 idref bean。
<idref/>
元素带来价值的一个常见地方(至少在 Spring 2.0 之前的版本中)是在 ProxyFactoryBean bean 定义中的 AOP 拦截器的配置中。 在指定拦截器名称时使用 <idref/>
元素可防止您拼错拦截器 ID。
文章内容参考官网 :https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-definition