1.4 依赖 #
典型的企业应用程序不会仅由单个对象(或Spring术语中的bean)组成的。即使是最简单的应用程序,也有几个对象一起工作,以让最终用户感觉是协调一致的应用程序。下一节将解释如何从定义大量独立的bean定义过渡到一个完全实现的应用程序,在这个应用程序中,对象相互协作以实现一个目标。
1.4.1 依赖注入 #
依赖注入(DI)是一个过程,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后在对象实例上设置的属性来定义其依赖项(即与之一起工作的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上与bean本身通过使用类直接构造或服务定位器模式来控制其依赖项的实例化或位置相反(因此称为控制反转)。
使用DI原理,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖项位于接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。
DI有两种主要变体:基于构造函数的依赖注入和基于Setter的依赖注入。
基于构造函数的依赖注入 #
基于构造函数的DI是通过容器调用具有多个参数的构造函数来完成的,每个参数表示一个依赖项。调用带有特定参数的静态工厂方法来构造bean几乎是相同的,在此探讨中对待构造函数和静态工厂方法的参数是类似的。以下示例展示了一个类,该类只通过构造函数注入来实现依赖项注入:
public class SimpleMovieLister {
// SimpleMovieLister有一个依赖项:MovieFinder
private MovieFinder movieFinder;
// 该构造器使得Spring容器可以注入一个MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 此处省略了实际使用注入的MovieFinder的业务逻辑
}
// 该构造器使得Spring容器可以注入一个MovieFinder
class SimpleMovieLister(private val movieFinder: MovieFinder) {
// 此处省略了实际使用注入的MovieFinder的业务逻辑
}
请注意,这个类没有什么特别之处。它就是是一个POJO,不依赖于容器特定的接口、基类或注解。
构造函数参数解析 #
构造函数参数解析是通过使用参数的类型进行匹配的。如果bean定义的构造函数参数中不存在潜在的歧义,那么在bean定义中定义构造函数参数的顺序就是在实例化bean时将这些参数提供给相应构造函数的顺序。考虑下面的类:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
package x.y
class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree)
假设ThingTwo
和ThingTree
类没有继承关系,则不存在潜在的歧义。因此,以下配置可以正常工作,您不需要在<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 int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
package examples
class ExampleBean(
private val years: Int, // Number of years to calculate the Ultimate Answer
private val ultimateAnswer: String// The Answer to Life, the Universe, and Everything
)
构造函数参数类型匹配
在前面的场景中,如果使用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;
}
}
package examples
class ExampleBean
@ConstructorProperties("years", "ultimateAnswer")
constructor(val years: Int, val ultimateAnswer: String)
基于setter的依赖注入 #
基于setter的DI是由容器在调用无参构造函数或无参静态工厂方法实例化bean之后,在bean上调用setter方法来完成的。
下面的示例展示了一个只能使用纯setter注入进行依赖注入的类。这个类是传统的Java类。它是一个POJO,不依赖于容器特定的接口、基类或注解。
public class SimpleMovieLister {
// SimpleMovieLister依赖于MovieFinder
private MovieFinder movieFinder;
// 此setter方法可以使Spring容器注入MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// 省略了实际使用注入的MovieFinder的业务逻辑
}
class SimpleMovieLister {
// 这是一个延迟初始化的属性,可以使Spring容器注入MovieFinder
lateinit var movieFinder: MovieFinder
// 省略了实际使用注入的MovieFinder的业务逻辑
}
ApplicationContext
为其管理的bean支持基于构造函数和基于setter的DI。在通过构造函数方法注入一些依赖项之后,它还支持使用基于setter的DI再注入一些依赖项。您可以以BeanDefinition
的形式配置依赖项,将其与PropertyEditor
实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接使用这些类(即编程),而是使用XML bean
定义、带注解的组件(即用@Component
、@Controller
等注解标注的类)或基于Java的@Configuration
类中的@Bean
方法。然后,这些源在内部转换为BeanDefinition
的实例,并用于加载整个Spring IoC容器实例。
基于构造函数的DI还是基于setter的DI? #
因为您可以混合使用基于构造函数和基于setter的依赖注入,所以使用构造函数注入强制依赖项,使用setter方法或配置方法注入可选依赖项,这是一个很好的经验法则。注意,在setter方法上使用@Required注解可以使属性成为必需的依赖项;但是,最好在构造函数注入时使用编程的方式验证参数。
Spring团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为null。此外,构造函数注入的组件总是以完全初始化的状态返回给客户端代码(调用)。顺便说一下,构造函数存在大量参数是一种糟糕的代码味道,这意味着此类可能有太多的责任,应该进行重构以更好地对关注点进行分离。
setter注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,必须在代码使用依赖项的任何地方执行非空检查。setter注入的一个好处是setter方法使该类的对象易于以后重新配置或重新注入。因此,通过JMX MBeans进行管理是setter注入的一个引人注目的案例。
使用对特定类最有意义的依赖方式。有时,在处理您没有源代码的第三方类时,您可以做出选择。例如,如果第三方类不公开任何setter方法,那么构造函数注入可能是唯一可用的依赖注入方式。
依赖项解析过程 #
容器执行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的实例。如果为类A和类B配置bean以相互注入,Spring IoC容器将在运行时检测到此循环引用,并抛出
BeanCurrentlyInCreationException
。一种可能的解决方案是编辑某些类的源代码,由setter而不是构造函数进行配置。或者,避免构造函数注入,只使用setter注入。换句话说,虽然不建议使用setter注入,但您可以使用setter注入来配置循环依赖项。
与常见情况(没有循环依赖)不同,bean A和bean B之间的循环依赖迫使一个bean在完全初始化之前注入另一个bean(典型的先有鸡还是先有蛋的场景)。
您通常可以相信Spring会做正确的事情。它在容器加载时检测配置问题,例如对不存在的bean的引用和循环依赖项。Spring在bean实际创建时尽可能晚地设置属性并解析依赖项。这意味着,如果创建该对象或其依赖项之一时出现问题(例如,bean由于缺少或无效属性而引发异常),则已正确加载的 Spring 容器稍后可以在您请求该对象时生成异常。这可能会延迟某些配置问题的可见性,这就是ApplicationContext
实现在默认情况下预先实例化单例bean的原因。在实际需要这些bean之前,您需要花费一些前期时间和内存来创建它们,但在创建ApplicationContext
时(而不是以后)会发现配置问题。您仍然可以覆盖此默认行为,以便单例bean延迟初始化,而不是预先实例化。
如果不存在循环依赖关系,那么当一个或多个协作bean被注入到依赖bean中时,每个协作bean在被注入到依赖bean中之前都会被完全配置。这意味着,如果bean A依赖于bean B,那么Spring IoC容器将在调用bean A上的setter方法之前完全配置完bean B。换句话说,bean被实例化了(如果它不是预实例化的单例),则它的依赖项会被设置,相关的生命周期方法(例如配置的init方法或InitializingBean回调方法)也会被调用。
依赖注入示例 #
以下示例将基于XML的配置元数据使用setter方式进行依赖注入。Spring 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;
}
}
class ExampleBean {
lateinit var beanOne: AnotherBean
lateinit var beanTwo: YetAnotherBean
var i: Int = 0
}
在前面的示例中,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;
}
}
class ExampleBean(
private val beanOne: AnotherBean,
private val beanTwo: YetAnotherBean,
private val i: Int)
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 {
// 私有的构造函数
private ExampleBean(...) {
...
}
// 静态工厂法;此方法的参数可以被视为要返回的bean的依赖项,而不管这些参数实际如何使用。
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// 其他操作...
return eb;
}
}
class ExampleBean private constructor() {
companion object {
// 静态工厂法;此方法的参数可以被视为要返回的bean的依赖项,而不管这些参数实际如何使用。
fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean {
val eb = ExampleBean (...)
// 其他操作...
return eb
}
}
}
静态工厂方法的参数由<constructor-arg/>
元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是相同的)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用factory-bean
属性而不是class
属性之外),因此我们这里不讨论这些细节。
1.4.2 详细的依赖项和配置 #
如前一节所述,您可以将bean属性和构造函数参数定义为对其他bean(协作者)的引用或内联定义的值。为此,Spring基于XML的配置元数据支持其<property/>
和<constructor-arg/>
元素中使用子元素类型。
直接的值(基本类型、字符串等等) #
<property/>
元素的value
属性将属性或构造函数参数指定为人可读字符串的表示形式。Spring的转换服务用于将这些值从字符串转换为属性或参数的实际类型。以下示例显示了正在设置的各种值:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- 调用setDriverClassName(String) -->
<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="masterkaoli"/>
</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="masterkaoli"/>
</beans>
前面的XML更简洁。但是,输入错误是在运行时而不是在设计时发现的,除非您在创建bean定义时使用支持自动属性补全的IDE(如IntelliJ IDEA或Spring Tool Suite)。强烈建议使用此类IDE帮助。
您还可以配置java.util.Properties
实例,如下所示:
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<!-- 类型为 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
属性样式的几个地方之一。
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是否确实存在。在第二种形式中,没有对传递给client
bean的targetName
属性的值执行任何验证。只有在实际实例化client
bean时才会发现输入错误(很可能是致命的结果)。如果client
bean是一个原型bean,那么只有在部署容器后很长一段时间才能发现这个输入错误和由此产生的异常。
idref
元素上的local
属性在4.0 beans XSD中不再受支持,因为它不再提供常规bean
引用上的值。升级到4.0架构时,需要将现有的idref local
引用更改为idref bean
。
<idref/>
元素带来价值的一个常见位置(至少在Spring 2.0之前的版本中)是ProxyFactoryBean
bean定义中的AOP拦截器配置。在指定拦截器名称时使用<idref/>
元素可以防止拼写拦截器ID错误。
对其他bean(协作者)的引用 #
ref
元素是<constructor-arg/>
或<property/>
定义元素中的最后一个元素。这里,您将bean的指定属性的值设置为容器管理的另一个bean(协作者)的引用。被引用的bean是要被设置属性bean的依赖项,并且在设置属性之前根据需要对其进行初始化(如果协作者是单例bean,它可能已经被容器初始化。)所有引用最终都是对另一个对象的引用。作用域和验证取决于是否通过bean
、local
或parent
属性指定其他对象的ID或名称。
通过<ref/>
标记的bean
属性来指定目标bean是最常见的方式,它允许创建对同一容器或父容器中任何bean的引用,而不管它们是否在同一XML文件中。bean
属性的值可以与目标bean的id
属性相同,或者与目标bean的name
属性中的一个值相同。以下示例显示了如何使用ref
元素:
<ref bean="someBean"/>
通过parent
属性指定目标bean将创建对当前容器的父容器中的bean的引用。parent
属性的值可以与目标bean的id
属性或目标bean的name
属性中的一个值相同。目标bean必须位于当前bean的父容器中。使用这个bean引用变量,主要场景应该是当容器有层次结构,并且希望使用与父bean同名的代理来将现有bean包装到父容器中时。以下展示示了如何使用parent
属性:
<!-- 在父上下文中 -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- 根据需要在此处插入依赖项 -->
</bean>
<!-- 在子(后代)上下文中 -->
<bean id="accountService" <!-- bean名称与父bean相同 -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- 注意我们如何引用父bean -->
</property>
<!-- 根据需要在此处插入其他配置和依赖项 -->
</bean>
ref
元素上的local
属性在4.0 bean XSD中不再受支持,因为它不再提供常规bean
引用上的值。升级到4.0时,请将现有的ref local
引用更改为ref bean
。
内部bean #
<property/>
或<constructor-arg/>
元素中的<bean/>
元素定义内部bean,如下例所示:
<bean id="outer" class="...">
<!-- 不使用对目标bean的引用,只需内联定义目标bean -->
<property name="target">
<bean class="com.example.Person"> <!-- 这就是内部bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
内部bean定义不需要定义ID或名称。即使指定了,容器也不使用此类值作为标识符。容器在创建时也会忽略scope
标志,因为内部bean总是匿名的,并且总是使用外部bean创建的。不能单独访问内部bean,也不能将它们注入到协作bean而不是封闭bean中。
作为一种特殊情况,内部bean可以从自定义范围接收销毁回调 — 例如,对于单例bean中包含的请求范围的内部bean。内部bean实例的创建与其包含的bean相关联,但销毁回调允许它参与请求范围的生命周期。这不是常见的情况。内部bean通常只是共享其包含bean的范围。
集合 #
<list/>
、<set/>
、<map/>
和<props/>
元素分别设置Java集合类型List
、Set
、Map
和Properties
的属性和参数。以下示例展示了如何使用它们:
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- 调用setAdminEmails(java.util.Properties) -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- 调用setSomeList(java.util.List) -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- 调用setSomeMap(java.util.Map) -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- 调用setSomeSet(java.util.Set) -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
map的key或value的值或集合的值也可以是以下任意元素:
bean | ref | idref | list | set | map | props | value | null
集合合并 #
Spring容器还支持合并集合。应用程序的开发人员可以定义父级<list/>
、<map/>
、<set/>
或<props/>
元素,并让子级<list/>
、<map/>
、<set/>
或<props/>
元素继承和重写父集合中的值。也就是说,子集合的值是父集合和子集合的元素合并的结果,子集合的元素覆盖父集合中指定的值。
关于合并的这一节讨论父子bean机制。不熟悉父bean和子bean定义的读者可能希望在继续往下之前阅读相关部分。
以下示例演示了集合合并:
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- 合并是在子集合定义上指定的 -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
注意在child
bean定义的adminEmails
属性的<props/>
元素上使用了merge=true
属性。当容器解析并实例化child
bean时,生成的实例具有一个adminEmails
Properties
的集合,该集合包含将子bean的adminEmails
集合与父bean的adminEmails
集合合并的结果。下面的列表显示了结果:
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
子Properties
集合的值继承父级<props/>
的所有属性元素,并且子级中support
的值覆盖父级集合中的support
值。
此合并行为类似于<list/>
、<map/>
和<set/>
集合类型。<list/>
元素在特定情况下,与List
集合类型(即值的有序集合的概念)相关联的行为将得到保持。父列表的值位于子列表的所有值之前。对于Map
、Set
和Properties
集合类型,不存在排序。因此,对于容器内部使用的关联Map
、Set
和Properties
实现类型的基础集合类型,没有有效的排序语义。
集合合并的局限性 #
不能合并不同的集合类型(例如Map
和List
)。如果真的尝试这样做,则会引发相应的异常。必须在较低的继承子定义上指定merge
属性。在父集合定义上指定merge
属性是多余的,不会导致所需的合并。
强类型集合 #
随着Java5中泛型类型的引入,您可以使用强类型集合。也就是说,可以声明Collection
类型,使其只能包含(例如)String
元素。如果使用Spring将强类型Collection
注入bean中,则可以利用Spring的类型转换支持,以便在将强类型Collection
实例的元素添加到集合之前将其转换为适当的类型。以下Java类和bean定义说明了如何执行此操作:
public class SomeClass {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
class SomeClass {
lateinit var accounts: Map<String, Float>
}
<beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>
当something
bean的accounts
属性准备好注入时,强类型Map<String, Float>
的元素类型的泛型信息可以通过反射获得。因此,Spring的类型转换基础设施将各种值元素识别为Float
类型,字符串值(9.99
、2.75
和3.99
)被转换为实际的Float
类型。
null和空字符串值 #
Spring将属性的空参数视为空字符串。以下基于XML的配置元数据片段将属性email
设置为空字符串值("")。
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
前面的示例相当于以下Java代码:
exampleBean.setEmail("");
exampleBean.email = ""
<null/>
元素用于处理空值。下面的列表展示了一个示例:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
上述配置相当于以下Java代码:
exampleBean.setEmail(null);
exampleBean.email = null
使用p-命名空间的XML快捷方式 #
p-命名空间允许您使用bean
元素的属性(而不是嵌套的<property/>
元素)来描述您的属性值、协作bean,或者两者都有。
Spring支持带有命名空间的可扩展配置格式,这些命名空间基于XML模式定义。本章讨论的beans
配置格式在XML模式文档中定义。但是,p-命名空间没有在XSD文件中定义,只存在于Spring核心中。
以下示例展示了两个解析为相同结果的XML片段(第一个使用标准XML格式,第二个使用p命名空间):
<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 name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>
该示例显示了bean定义中一个名为email
的p命名空间中的属性。这告诉Spring包含一个属性声明。如前所述,p-命名空间没有模式定义,因此可以将属性名设置为配置名。
下一个示例包括另外两个bean定义,它们都引用了另一个bean:
源码如下:
<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 name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
此示例不仅包括使用p命名空间的属性值,还使用特殊格式声明属性引用。第一个bean定义使用<property name="spouse" ref="jane"/>
来创建从bean john
对bean jane
的引用,而第二个bean定义使用p:spouse-ref="jane"
作为属性来执行完全相同的操作。在本例中,spouse
是属性名,而-ref
部分表示这不是一个直接的值,而是对另一个bean的引用。
p命名空间不如标准XML格式灵活。例如,用于声明属性引用的格式与以Ref
结尾的属性冲突,而标准XML格式则不冲突。我们建议您慎重选择您的方法,并将此告知您的团队成员,以避免生成同时使用这三种方法的XML文档。
使用c-命名空间的XML快捷方式 #
与p-命名空间的XML快捷方式类似,Spring3.1中引入的c-命名空间允许使用行内属性来配置构造函数参数,而不是嵌套的constructor-arg
元素。
以下示例使用c:
命名空间执行与基于构造函数的依赖注入相同的操作:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
<!-- 使用可选参数名称的传统声明方式 -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>
<!-- 使用参数名的c-命名空间声明 -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>
</beans>
c:
命名空间使用与p:
(对于bean引用尾部会有-ref
)相同的约定,通过名称设置构造函数参数。类似地,它需要在XML文件中声明,即使它没有在XSD schema中定义(它存在于Spring内核中)。
对于构造函数参数名称不可用的罕见情况(通常是在字节码编译时没有调试信息),作为退路可以使用参数索引,如下所示:
<!-- c-命名空间 索引定义 -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="something@somewhere.com"/>
由于XML语法的原因,索引表示法要求必须以_
开头,因为XML属性名称不能以数字开头(即使某些IDE允许)。对于<constructor-arg>
元素,也可以使用相应的索引表示法,但并不常用,因为声明的简单顺序通常就足够了。
实际上,构造函数解析机制在匹配参数方面非常有效,因此除非您真的需要,否则我们建议在整个配置中使用名称表示法。
复合属性名 #
设置bean属性时,可以使用复合属性名或嵌套属性名,只要路径上的所有组件(最终属性名除外)都不为null。考虑下面的bean定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something
bean有一个fred
属性,fred
有一个bob
属性,bob
有一个sammy
属性,最后的sammy
属性被设置为值123
。为了使其能达到目的,something
的fred
属性和fred
的bob
属性在构建bean后不能为null
。否则,将抛出NullPointerException
。
1.4.3 使用depends-on
#
如果一个bean是另一个bean的依赖项,这通常意味着一个bean被设置为另一个bean的属性。通常,在基于XML的配置元数据中您可以通过<ref/>
元素来实现这一点。然而,有时候bean之间的依赖关系不那么直接。例如,需要触发类中的静态初始化器,例如数据库驱动程序注册。depends-on
属性可以显式地强制一个或多个bean在使用该元素的bean初始化之前进行初始化。以下示例使用depends-on
属性表示对单个bean的依赖关系:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
要表示对多个bean的依赖关系,需要提供一个bean名称列表作为depends-on
属性的值(逗号、空格和分号都是有效的分隔符):
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on
属性既可以指定初始化时间依赖,也可以指定相应的销毁时间依赖(仅在单例bean的情况下)。通过depends-on
定义与给定bean关系的依赖bean首先被销毁,然后再销毁给定bean本身。因此,depends-on
还可以控制程序退出的顺序。
1.4.4 延迟初始化bean #
默认情况下,ApplicationContext
实现在初始化过程中早早地创建和配置好所有的单例bean。通常,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是数小时甚至数天之后。当这种行为不可取时,可以通过将bean定义标记为延迟初始化来防止单例bean的预实例化。延迟初始化bean告诉IoC容器在第一次请求时而不是在启动时创建bean实例。
在XML中,此行为由<bean/>
元素上的lazy-init
属性控制,如下例所示:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
当前面的配置被ApplicationContext
使用时,当ApplicationContext
启动,lazy
bean并不急于预实例化,而not.lazy
bean则会立即预实例化。
但是,当延迟初始化bean是非延迟初始化的单例bean的依赖项时,ApplicationContext
会在启动时创建延迟初始化的bean,因为它必须满足单例的依赖项。延迟初始化bean被注入到其他地方的非延迟初始化的单例bean中。
您还可以通过在<beans/>
元素上使用default-lazy-init
属性来控制容器级别的延迟初始化,如下示例所示:
<beans default-lazy-init="true">
<!-- 没有bean会被预实例化... -->
</beans>
1.4.5 自动装配协作者 #
Spring容器可以自动装配协作bean之间的关系。通过检查ApplicationContext
的内容,您可以让Spring为您的bean自动解析协作者(其他bean)。自动装配具有以下优点:
- 自动装配可以显著减少需要指定的属性或构造函数参数(在这方面,本章其他地方讨论的其他机制(如bean模板)也很有价值。)
- 自动装配可以随着对象的演变而更新配置。例如,如果您需要向类添加依赖项,该依赖项则可以自动满足,而无需修改配置。因此,在开发过程中,自动装配特别有用,而不会在代码库变得更稳定时取消切换到显式装配的选项。
使用基于XML的配置元数据时(请参见依赖注入),可以使用<bean/>
元素的autowire
属性为bean定义指定自动装配模式。自动装配功能有四种模式,您可以为每个bean指定自动装配,从而可以选择要自动装配的bean。下表介绍了四种自动装配模式:
表2. 自动装配模式
模式 | 解释 |
---|---|
no |
(默认)无自动装配。bean引用必须由ref 元素定义。对于较大的部署,不建议更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了一个系统的结构。 |
byName |
按属性名称自动装配。Spring查找与需要自动装配的属性同名的bean。例如,如果一个bean定义被设置为按名称自动装配,并且它包含一个master 属性(即,它有一个setMaster(..) 方法),Spring将查找一个名为master 的bean定义并使用它来设置属性。 |
byType |
如果容器中正好存在一个属性类型的bean,则可以自动装配该属性。如果存在多个,将抛出一个致命异常,这表示您可能不应该对该bean使用byType 自动装配。如果没有匹配的bean,则不会发生任何事情(未设置属性)。 |
constructor |
类似于byType ,但适用于构造函数参数。如果容器中没有该构造函数参数类型的bean,则会引发致命错误。 |
使用byType
或constructor
自动装配模式,可以装配数组和类型化集合。在这种情况下,将提供容器中与预期类型匹配的所有自动装配候选项以满足依赖关系。如果所需的键类型为String
,则可以自动装配强类型Map
实例。自动装配Map
实例的值由与预期类型匹配的所有bean实例组成,Map
实例的键包含相应的bean名称。
自动装配的局限性和缺点 #
在整个项目中一致使用自动装配时效果最佳。如果通常不使用自动装配,那么仅使用它装配一个或两个bean定义可能会让开发人员感到困惑。
考虑如下自动装配的局限性和缺点:
property
和constructor-arg
设置中的显式依赖项始终会覆盖自动装配。不能自动装配简单属性,例如基本数据类型、Strings
和Classes
(以及此类简单属性的数组)。这一限制是由设计造成的。- 自动装配不如显式装配精确。尽管如此,如前表所述,Spring小心避免在可能产生意外结果的歧义情况下进行猜测。Spring托管对象之间的关系不再明确记录。
- 装配信息也需不适用于可能从Spring容器生成文档的工具。
- 容器中的多个bean定义可能与要自动装配的setter方法或构造函数参数指定的类型匹配。对于数组、集合或
Map
实例,这不一定是问题。然而,对于期望单个值的依赖项,这种模糊性并不是随意解决的。如果没有唯一的bean定义可用,将引发异常。
在后一种情况下,您有几个选项:
- 放弃自动装配,采用显式装配。
- 通过将bean定义的
autowire-candidate
属性设置为false
,以避免自动装配,如下一节所述。 - 通过将其
<bean/>
元素的primary
属性设置为true
,将单个bean定义指定为主要候选项。 - 如基于注解的容器配置中所述,使用基于注解的配置实现更细粒度的控制。
从自动装配中排除bean #
在每个bean的基础上,您可以将bean从自动装配中排除。在Spring的XML格式中,将<bean/>
元素的autowire-candidate
属性设置为false
。该容器使该特定bean定义对自动装配基础结构(包括注解放个配置,如@Autowired
)不可用。
autowire-candidate
属性被设计为仅影响基于类型的自动装配。它不会通过名称影响显式引用,即使指定的bean未标记为自动装配候选,也会解析显式的引用。因此,如果名称匹配,按名称自动装配将注入该bean。
您还可以基于对bean名称的模式匹配来限制自动装配候选项。顶级<beans/>
元素在其default-autowire-candidates
属性中接受一个或多个模式。例如,要将自动装配候选状态限制为名称以Repository
结尾的任何bean,那请提供值*Repository
。要提供多个模式,请在逗号分隔的列表中定义它们。bean定义的autowire-candidate
属性的显式值true
或false
始终更优先。对于这样的bean,模式匹配规则不适用。
这些技术对于您永远不希望通过自动装配注入其他bean的bean非常有用。这并不意味着排除的bean本身不能通过使用自动装配进行配置。相反,bean本身不是自动装配其他bean的候选对象。
1.4.6 方法注入 #
在大多数应用程序场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性来处理依赖关系。当bean的生命周期不同时,就会出现问题。假设单例bean A可能是在A上的每个方法调用时需要使用非单例(原型)bean B,。容器只创建单例bean A一次,因此只有一次机会设置属性。容器不能在每次需要时为bean A提供bean B的新实例。
一个解决办法是放弃部分控制反转。您可以通过实现ApplicationContextAware
接口使bean A对容器有所感知,并在每次bean A需要时对容器发出getBean("B")
调用以请求(通常是新的)bean B实例。以下示例展示了此方法:
// 使用有状态的命令风格类执行某些处理的类
package fiona.apple;
// 导入Spring的相关API
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// 获取相应Command的新实例
Command command = createCommand();
// 在(希望是全新的)Command实例上设置状态
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// 注意Spring API的依赖关系!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
// 使用有状态的命令风格类执行某些处理的类
package fiona.apple
// 导入Spring的相关API
import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware
class CommandManager : ApplicationContextAware {
private lateinit var applicationContext: ApplicationContext
fun process(commandState: Map<*, *>): Any {
// 获取相应Command的新实例
val command = createCommand()
// 在(希望是全新的)Command实例上设置状态
command.state = commandState
return command.execute()
}
// 注意Spring API的依赖关系!
protected fun createCommand() =
applicationContext.getBean("command", Command::class.java)
override fun setApplicationContext(applicationContext: ApplicationContext) {
this.applicationContext = applicationContext
}
}
前面的内容是不可取的,因为业务代码能感知到Spring框架并与之耦合。方法注入是Spring IoC容器的一个稍微高级的特性,它允许您干净地处理这种场景。
您可以在这篇博客文章中阅读更多关于方法注入动机的信息。
查找方法注入 #
查找方法注入是容器重写容器管理bean上的方法并返回容器中另一个命名bean的查找结果的能力。查找通常涉及一个原型bean,如前一节所述。Spring框架通过使用CGLIB库中的字节码生成来动态生成重写该方法的子类,从而实现这种方法注入。
- 为了使这个动态子类能够工作,Spring bean容器子类的类不能是
final
,要重写的方法也不能是final
。- 对具有
abstract
方法的类进行单元测试需要您自己对该类进行子类化,并提供abstract
方法的存根实现。- 组件扫描还需要具体的方法,这需要具体的类来提供。
- 另一个关键限制是,查找方法不适用于工厂方法,特别是配置类中的
@Bean
方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。
对于前面代码段中的CommandManager
类,Spring容器动态重写createCommand()
方法的实现。CommandManager
类没有任何Spring依赖项,修改后的示例如下所示:
package fiona.apple;
// 没有Spring导入
public abstract class CommandManager {
public Object process(Object commandState) {
// 获取相应Command接口的新实例
Command command = createCommand();
// 在(希望是全新的)Command实例上设置状态
command.setState(commandState);
return command.execute();
}
// 可以,但是这种方法的实现在哪里呢?
protected abstract Command createCommand();
}
package fiona.apple
// 没有Spring导入
abstract class CommandManager {
fun process(commandState: Any): Any {
// 获取相应Command接口的新实例
val command = createCommand()
// 在(希望是全新的)Command实例上设置状态
command.state = commandState
return command.execute()
}
// 可以,但是这种方法的实现在哪里呢?
protected abstract fun createCommand(): Command
}
在包含要注入的方法的客户端类(本例中为CommandManager
)中,要注入的方法需要以下形式的签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是abstract
的,则动态生成的子类将实现该方法。否则,动态生成的子类将重写在原始类中定义的具体方法。考虑下面的例子:
<!-- 作为原型部署的有状态bean(非单例) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- 根据需要在此处注入依赖项 -->
</bean>
<!-- commandProcessor使用statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
当需要myCommand
bean的新实例时,标识为commandManager
的bean就会调用自己的createCommand()
方法。如果需要的话,您必须注意将myCommand
bean部署为原型。如果是单例,则每次都返回相同的myCommand
bean实例。
或者,在基于注解的组件模型中,可以通过@Lookup
注解声明查找方法,如下例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup("myCommand")
protected abstract fun createCommand(): Command
}
或者,更习惯地,您可以依靠目标bean根据查找方法的声明返回类型进行解析:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
abstract class CommandManager {
fun process(commandState: Any): Any {
val command = createCommand()
command.state = commandState
return command.execute()
}
@Lookup
protected abstract fun createCommand(): Command
}
请注意,您通常应该使用具体的存根实现声明此类带注解的查找方法,以便它们与Spring的组件扫描规则兼容,默认情况下抽象类会被忽略。此限制不适用于显式注册或显式导入的bean类。
另一种访问不同范围的目标bean的方法是
ObjectFactory
/Provider
注入点。参考有作用域的bean作为依赖项。您还可能发现
ServiceLocatoryFactoryBean
(在org.springframework.beans.factory.config
包中)很有用。
任意方法替换 #
与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一种方法实现替换托管bean中的任意方法。在实际需要此功能之前,您可以安全地跳过本节的其余部分。
对于基于XML的配置元数据,对于已部署的bean,可以使用replaced-method
元素将现有的方法实现替换为另一个方法实现。考虑下面的类,它有一个我们想重写的叫做computeValue
的方法:
public class MyValueCalculator {
public String computeValue(String input) {
// 一些真实的代码...
}
// 一些其他方法...
}
class MyValueCalculator {
fun computeValue(input: String): String {
// 一些真实的代码...
}
// 一些其他方法...
}
一个实现了org.springframework.beans.factory.support.MethodReplacer
接口的类提供了新的方法定义,如下例所示:
/**
* 用于MyValueCalculator中重写现有的computeValue(String)实现
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// 获取输入值,使用它,并返回计算结果
String input = (String) args[0];
...
return ...;
}
}
/**
* 用于MyValueCalculator中重写现有的computeValue(String)实现
*/
class ReplacementComputeValue : MethodReplacer {
override fun reimplement(obj: Any, method: Method, args: Array<out Any>): Any {
// 获取输入值,使用它,并返回计算结果
val input = args[0] as String;
...
return ...;
}
}
部署原始类并指定方法重写的bean定义类似于以下示例:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- 任意方法替换 -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
您可以在<replaced-method/>
元素中使用一个或多个<arg-type/>
元素来指定要重写的方法的方法签名。只有当方法重载且类中存在多种变体时,才需要参数的签名。为方便起见,参数的字符串类型可以是完全限定类名的子字符串。例如,以下所有选项都与java.lang.String
匹配:
java.lang.String
String
Str
由于参数的数量通常足以区分每个可能的选择,因此此快捷方式可以通过只允许键入与参数类型匹配的最短字符串来节省大量键入。