Spring 5.0.0框架介绍_中文版_3.4

文章作者:Tyan
博客:noahsnail.com

3.4 依赖

        标准企业应用不会由一个对象(或Spring用语中的bean)组成。即使是最简单的应用也是由一些对象共同工作,呈现给终端用户用户看到的是一个连贯的应用。接下来的一节阐述了如何从定义许多独立的bean定义到完全实现的应用,它是一个通过对象协作来实现目标的过程。

3.4.1 依赖注入

        依赖注入(DI)是一个处理过程,凭借对象之间依赖关系,也就是和它们一起工作的其它对象,只能通过构造函数参数,传递参数给工厂方法,在构造完成或工厂方法返回的对象实例之后再设置对象实例的属性。当创建bean时容器再将这些依赖对象注入进去。这个过程从根本上颠倒了bean本身通过直接构建类或通过一种机制例如服务定位模式来控制依赖对象的实例化或定位,因此命名为控制反转(IoC)

        使用依赖注入原则会使代码更简洁,当对象由依赖关系提供时解耦更有效。对象不会查找它的依赖,不知道依赖的位置和依赖关系的类别。同样的,你的类也变的更容易测试,尤其是依赖关系在接口或抽象基类之间的时候,这种情况下单元测试中会要求存桩或模拟实现。(注:Stub和Mock都是软件测试中使用的东西,如有疑问请自行google或百度)。

        依赖有两个主要变种,基于构造函数的依赖注入和基于Setter的依赖注入。

基于构造函数的依赖注入

        基于构造函数的依赖注入通过容器调用有参数的构造函数来实现,每个参数表示一个依赖。调用指定参数的静态工厂方法来构造bean是近似等价的,这里的讨论将给构造函数和静态工厂方法传参看成是类似的。接下来的例子展示了一个类仅能通过构建函数注入进行依赖注入。注意这个类没什么特别的,它是一个POJO,不依赖于容器特定的接口,基类或注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private 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...
}

构造函数参数解析

        构造函数参数解析使用参数类型进行匹配。如果bean定义的构造函数参数中不存在潜在的歧义,bean定义中定义构造函数参数的顺序为bean实例化时提供给恰当构造函数的参数顺序。细想下面的类:

1
2
3
4
5
6
7
8
9
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}

        不存在潜在的歧义,假设Bar类和Baz类之间不存在继承关系。因此下面的配置会工作良好,你不必在<constructor-arg/>元素中显式的指定构造函数参数索引的与/或类型。

1
2
3
4
5
6
7
8
9
10
<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不能决定值的类型,因此没有帮助不能按类型匹配。考虑下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

        在上面的场景中,如果你用type属性显式的指定了构造参数的类型,对于简单类型容器可以使用类型匹配。例如:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

        使用index属性来显式的指定构造函数参数的索引,例如:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>

        除了要解析多个简单值的歧义性之外,当构造函数有两个相同类型的的参数时,指定索引可以解决歧义性问题。注意索引是从0开始的。

        你也可以使用构造函数参数名字解决值的歧义问题。

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

        记住,要使这个起作用你的代码必须使用调试模式进行编译,这样Spring可以从构造函数中查找参数名称。如果你不能用调试模式进行编译(或不想),你可以使用JDK注解@ConstructorProperties显式的命名你的构造函数参数。样板类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}

基于Setter的依赖注入

        基于Setter的依赖注入在容器调用无参构造函数或无参静态工厂方法之后,通过调用bean的setter方法来实现依赖注入。

        下面的例子显示了一个类只能通过纯粹的setter注入进行依赖注入。这个类是常见的Java类。它是一个不依赖于容器中特定接口、基类或注解的POJO。

1
2
3
4
5
6
7
8
9
10
11
12
13
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支持基于构造函数和基于setter对它管理的bean进行依赖注入。它也支持一些依赖通过构造函数方法注入之后,使用基于setter的依赖注入。使用BeanDefinition形式配置依赖项,结合PropertyEditor实例可以将属性从一种形式转成另一种形式。然而大多数Spring用户直接使用这些类(例如以编程形式),而使用XML定义bean,注解组件(例如类中使用 @Component,,@Controller注解等等),或在基于Java的@Configuration类使用@Bean方法。

使用基于构造函数的依赖注入还是基于setter的依赖注入?

你可以混合使用基于构造函数的依赖注入和基于setter的依赖注入,强制依赖使用构造函数注入,可选依赖使用setter方法或配置方法注入是一个很好的经验法则。注意在setter方法上使用@Required注解会检查依赖是否注入。

当实现的应用组件是不可变对象时,Spring团队通常主张构造函数注入,这样可以确保所需的依赖非空。此外,基于构造函数注入的组件总是以完全初始化状态返回客户(调用)代码。作为附注,含有许多构造函数参数的代码给人的感觉很差,这意味着类可能有很多职责,应该进行重构以便更好的处理关注问题的分离。

setter注入应该主要用来可选依赖上,在类内可以给可选依赖指定合理的默认值。此外,在每处使用依赖的代码都要进行非空检查。setter注入的一个好处就是setter方法使类的对象在后面可以进行再配置或再注入。JMX MBeans的管理是setter注入一个非常好的案例。

使用依赖注入的类型对于特定的类是最有意义的。有时候,当处理没有源码的第三方类时,使用哪种方式取决于你。例如,如果第三方库没有提供任何setter方法,构造函数注入可能是依赖注入唯一可行的方式。

依赖解析过程

        容器按下面的过程处理bean依赖解析:

  • 创建ApplicationContext并使用描述所有bean的配置元数据初始化ApplicationContext,配置元数据可以通过XML,Java代码或注解指定。

  • 对于每一个bean,它的依赖通过属性、构造函数参数、或静态工厂方法参数的形式表示,静态工厂方法可以替代标准的构造函数。当bean在实际创建时,这些依赖会提供给bean。

  • 每个属性或构造函数参数或者是根据实际定义设置的值,或者是容器中另一个bean的引用。

  • 每个属性或构造函数参数是一个从指定形式转成实际类型的属性或构造函数参数的值。

        当容器创建后Spring容器会验证每个bean的配置。然而,bean属性本身只有bean创建时才会进行设置。bean是单例的并且当容器创建时会进行提前实例化(默认情况)。作用范围是在3.5 小节”Bean scopes”中定义的。此外,只有需要时候才会创建bean。bean的创建可能会引起beans图的创建,当bean的依赖和它的依赖的依赖(等等)创建和指定的时候。注意这些依赖中解析不匹配可能会在后面出现,例如,受影响的bean第一次创建时。

循环依赖

如果你主要使用构造函数注入,有可能会出现一个不能解决的循环依赖状况。

例如,类A需要通过构造函数注入得到一个类B的实例,而类B需要通过构造函数获得一个类A的实例。如果你为类A和类B配置了互相注入的bean,Spring IoC容器在运行时检测到循环引用,会抛出BeanCurrentlyInCreationException

一个可能的解决方案是编译某个类的源代码使其通过setter注入而不是构造函数注入。或者,避免构造函数注入仅用setter注入。换句话说,尽管是不被推荐的,但你可以通过setter注入配置循环依赖。

不像通常的情况(没有循环依赖),bean A和bean B之间的循环依赖可以强制其中的一个bean优先注入另一个bean中,可以使其完全初始化(古老的鸡/蛋场景)。

        通常情况下你可以信任Spring去做正确的事情。在容器加载时它检测配置问题,例如引用不存在的beans和循环依赖。当bean实际创建时,Spring设置属性和解析依赖尽可能的晚。这意味着Spring容器正确加载但后面可能会产生异常,当你请求一个对象时,创建对象或它的某个依赖时出现问题,这时容器就会抛出异常。例如,由于缺失或存在无效属性,bean会抛出异常。在真正需要这些beans之前创建它们,会花费一些前期时间和内存,当ApplicationContext创建时你会发现配置问题,而不是在创建之后。为了单例bean懒惰初始化而不是预先实例化,你仍需要重写这个默认行为。

        如果没有循环依赖存在,当一个或更多协作beans注入到一个独立的bean中,在注入独立bean之前,每个协作bean都是完全配置的。这意味着如果bean A有个依赖为bean B,Spring IoC容器在调用bean A的setter方法之前会完整的配置bean B。换句话说,bean被实例化(不是预先实例化的单例),设置依赖和相关的生命周期方法(例如配置初始化方法或初始化bean回调方法)被调用。

依赖注入的例子

        下面的例子使用基于XML的配置元数据进行setter注入。Spring XML配置文件中的一小部分指定了一些bean的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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文件中指定的属性。下面的例子使用了基于构造函数的依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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调用静态工厂方法返回对象的一个实例:

1
2
3
4
5
6
7
8
<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"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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属性之外),因此这儿不讨论这些细节。

3.4.2 依赖和配置的细节

        正如上一节提到的那样,你可以通过引用其它被管理bean(协作者)来定义bean的属性和构造函数参数,或者在行内定义值。为了实现这个功能,Spring的基于XML的配置元数据在它的<property/><constructor-arg/>中支持子元素类型。

直接使用值 (基本类型,字符串等等)

        <property/>元素的value属性指定了一个属性或构造函数参数作为可读的字符串表示。使用Spring的转换服务将这些值从String转成属性或参数的真实类型。

1
2
3
4
5
6
7
<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="masterkaoli"/>
</bean>

        下面的例子为了更简洁的XML配置使用了p命名空间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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
http://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是更简洁的;然而,错别字是在运行时发现而不是在设计时,除非你使用IDE例如IntelliJ IDEASpring Tool Suite (STS),当你创建bean定义时它们支持自动的属性补全。IDE辅助是强烈推荐的。

        你也可以配置java.util.Properties实例:

1
2
3
4
5
6
7
8
9
10
11
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- 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实例。这是一个很好的捷径,使用嵌入的<value/>元素而不是使用value属性的方式,是Spring团队支持的几个地方之一。

idref元素

        在容器中传递另一个bean的id(字符串值,不是引用)到<constructor-arg/><property/>元素时,idref元素是一种简单的的误差检验方式。

1
2
3
4
5
6
7
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean" />
</property>
</bean>

        上面的bean定义片段与下面的代码片段是等价的(运行时):

1
2
3
4
5
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean" />
</bean>

        第一种形式优于第二种形式,因为idref标签允许容器在部署时验证引用的bean,即命名的bean实际存在。在第二种形式中,当值传给clienttargetName时没有进行验证。拼写错误只有在clientbean实际创建时才会发现(最可能有严重后果)。如果client bean是原型bean,拼写错误和产生的异常可能只有在容器部署很长时间之后才会发现。

idref元素的local属性在4.0 beans xsd中不再支持,因为它不再为合格的bean引用提供值。简单将你现有的idref local引用改成idref bean当更新到4.0 schema时。

        <idref/>元素带来值的通常位置(至少在Spring 2.0之前)是在ProxyFactoryBean bean定义中的AOP拦截器配置中。当你指定拦截器名字时使用<idref/>元素来防止误拼拦截器id。

其它bean的应用(协作bean)

        ref元素是<constructor-arg/><property/>定义元素的最终的元素。在这个元素中设置bean的指定属性的值,值为容器管理的另一个bean(协作bean)的引用。引用的bean是设置属性bean的依赖,在属性设置之前引用bean需要进行初始化。(如果协作bean是一个单例模式的bean,它可能已经被容器初始化了。)所有引用bean根本上都是另一个对象的引用。作用域和验证是根据你是否通过beanlocal,或parent属性指定了另一个对象的id/name来决定的。

        通过<ref/>标签的bean属性指定目标bean是最常用的形式,允许创建同容器或父容器中任何bean的引用,不管它是否是在同一个XML文件中。bean属性的值可能与目标bean的id属性值相同,或与目标bean的name属性值相同。

1
<ref bean="someBean"/>

        通过parent属性指定目标bean会引用当前容器的父容器中的bean。parent属性的值可能与目标bean的id值或name值相同,目标bean必须在当前容器的父容器中。当你有一个容器分层的时候你可以使用parent,你想将现有bean包裹在有代理的父容器中且现有bean与父容器中的bean同名,你可以使用parent属性。

1
2
3
4
<!-- in the parent context -->
<bean id="accountService" class="com.foo.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
1
2
3
4
5
6
7
8
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>

idref元素的local属性在4.0 beans xsd中不再支持,因为它不再为合格的bean引用提供值。简单将你现有的idref local引用改成idref bean当更新到4.0 schema时。

内部bean

        <property/><constructor-arg/>元素内的<bean/>元素中定义bean称为内部bean。

1
2
3
4
5
6
7
8
9
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>

        内部bean定义不要求定义id或name;如果指定了,容器不用用这个值作为标识符。容器创建时也忽略scope标记:内部bean总是匿名的且它们总是由外部bean创建。除了注入到封闭bean中或独立的访问它们,不可能将内部bean注入到协作bean中。

        作为一种很少出现的情况,从特定的域中有可能会收到销毁回调函数,例如,对于请求域内的内部bean包含单例bean:内部bean实例的创建会绑定到它的包含bean,但销毁回调函数允许它进入到请求域的生命周期中。这不是一个常见的场景;内部bean通常简单的共享它们的包含bean的作用域。

集合

        在<list/><set/><map/><props/>元素中,你要分别设置Java Collection类型listsetmapProperties的属性和参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<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>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key ="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>

        map的key或value,或者是set value的值也可以是下面元素中的任何一个:

1
bean | ref | idref | list | set | map | props | value | null

集合合并

        Spring也支持集合的合并。应用开发者可以定义父类型<list/><map/><set/><props/>元素,可以有继承和覆盖父集合的子类型元素<list/><map/><set/><props/>。也就是说,子集合的值是父集合和子集合中元素合并的结果,子集合元素覆盖了父集合元素的值。

        关于合并的这节讨论了父子bean机制。对父子bean定义不熟悉的读者可以去读相关的章节。

        下面的例子示范了集合合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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">
<!-- the merge is specified on the child collection definition -->
<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集合,集合中的值是子adminEmails集合和父adminEmails集合合并的结果。

1
2
3
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

        子Properties集合的值继承了父<props/>中的所有属性元素,子集合中的support值覆盖了父集合中的值。

        <list/><map/><set/>集合类型中的合并与上面类似。在特定的<list/>元素情况下,关于List集合类型的语义,也就是说,有序集合值的概念仍然是保留的;父list中的值领先于所有子list中的值。在MapSetProperties集合类型,不存在顺序。因此,无序语义在容器内部使用的集合类型MapSetProperties的实现基础上是有效的。

集合合并的限制

        你不能合并不同的集合类型(例如MapList),如果你试图合并不同的集合类型会有适当的抛出Exceptionmerge属性必须在更低的、继承的子定义中;在父集合定义中指定merge属性是多余的并且不会进行合并。

强类型集合

        随着Java 5中泛型的引入,你可以使用强类型集合。也就是说,你可以声明一个Collection类型但它只能包含String元素(例子)。如果你使用Spring将一个强类型的Collection注入到bean中,你可以利用Spring的类型转换支持,例如在将元素添加到Collection之前,将你的强类型Collection实例中的元素转成恰当的类型。

1
2
3
4
5
6
7
8
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
1
2
3
4
5
6
7
8
9
10
11
<beans>
<bean id="foo" class="x.y.Foo">
<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>

        当注入foo bean的accounts属性时,强类型Map<String, Float>中元素类型的泛型信息可以通过反射得到。因此Spring的类型转换结构能识别各种值元素的类型为Float,字符串9.99, 2.753.99会被转换成实际的Float类型。

Null和空字符串

        Spring把属性的空参数都处理为空Strings。下面基于XML的配置元数据片段将email属性设为空String值(“”)。

1
2
3
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>

        上面的例子与下面的Java代码是等价的:

1
exampleBean.setEmail("")

        <null/>元素处理null值。例如:

1
2
3
4
5
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>

        上面的配置与下面的Java代码等价。

1
exampleBean.setEmail(null)

XML 使用p命名空间的缩写

        p命名空间可以让你不需要嵌入<property/>元素便能使用bean元素的属性来描述你的属性值以及/或协作beans。

        Spring支持含有命名空间的扩展配置形式,命名控件是基于XML Schema定义的。本章讨论的beans配置形式是在XML Schema文档中定义的。但是p命名空间不能在XSD文件中定义并且只在Spring core中存在。

        下面的例子显示了两个XML片段,解析结果是相同的:第一个是标准的XML形式,第二个使用了p命名空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
<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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="foo@bar.com"/>
</bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="foo@bar.com"/>
</beans>

        这个例子显示了bean定义中p命名空间中有个一个叫email的属性。这会通知Spring包含属性声明。如前面所述,p命名空间没有schema定义,因此你可以将特性值(attribute)设到属性值(property)上。

        下面的例子包括两个bean定义,且它们都引用了另一个bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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
http://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"作为一个特性同样定义了从bean john到bean jane的引用。在spouse是属性名的情况下,-ref部分表示这不是一个直接的值而是另一个bean的引用。

p命名空间不是标准的XML格式,例如,声明的属性引用会与以Ref结尾的属性相冲突,而标准XML格式则不会。我们建议你仔细的选择你的方法并与你的团队成员交流,避免生成的XML文档同时使用了三种方式。

XML 使用c命名空间的缩写

        与“XML shortcut with the p-namespace”小节类似,在Spring 3.1新引入的c命名空间允许使用行内属性配置构造函数参数而不用嵌入constructor-arg元素。

        让我们重新回顾一下“Constructor-based dependency injection”小节中的例子并使用c:命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<!-- traditional declaration -->
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
<constructor-arg value="foo@bar.com"/>
</bean>
<!-- c-namespace declaration -->
<bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="foo@bar.com"/>
</beans>

        c:命名空间遵循与p:命名空间相同的约定在通过名字设置构造函数参数时。同样的,它也需要进行声明,虽然它不能在XSD schema中使用(但在Spring core中存在)。

        对于很少出现的不能找到构造函数参数名字的情况(通常如果编译字节码且没有调试信息),可以使用参数索引:

1
2
<!-- c-namespace index declaration -->
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>

由于XML语法,索引符号需要前面加上_,因为XML属性名字不能以数字开头(即使一些IDE允许)。

        在实践中,构造函数解析机制能有效匹配参数,因此除非真的需要,否则我们推荐在配置中使用名字符号。

混合属性名字

        当你设置bean属性时,你可以使用混合的或嵌入的属性名字,只要路径中除了最后的属性名之外所有组件都是非null。考虑下面的bean定义。

1
2
3
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
</bean>

        foobean有一个fred属性,fred有一个sammy属性,bob有一个sammy属性,最后的sammy属性设置值为123。为了这样设置,foofred属性,fredbob属性在bean创建后必须是非null或抛出NullPointerException

3.4.3 使用depends-on

        如果一个bean是另一个bean的一个依赖,这通常意味着一个bean作为另一个bean的一个属性去设置。在基于XML的配置元数据中通常使用<ref/>元素实现。然而有时beans之间的依赖关系是间接的;例如,类中的静态初始化程序需要触发,例如数据驱动注册。depends-on特性能显示的强制一个bean或多个beans在使用这个元素的bean初始化之前进行初始化。下面的例子使用depends-on特性表示一个单一bean的一个依赖:

1
2
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

        为了表示多个bean上的依赖关系,提供一个bean名字列表作为depends-on特性的值,用逗号,空格或分号作为有效分隔符:

1
2
3
4
5
6
<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定义中可以指定初始化时的依赖和对应的销毁时依赖(仅在单例情况下)。依赖beans与给定bean之间定义了一个depends-on关系,依赖beans在给定bean本身被销毁之前首先被销毁。因此depends-on也可以控制销毁顺序。

3.4.4 延迟初始化beans

        默认情况下,作为初始化过程的一部分,ApplicationContext实现时渴望创建并配置所有的单例beans。通常情况下,预实例化是必要的,因为配置中或周围环境中的错误可以立即发现,与几小时或几天后发现截然相反。当预实例化是不必要的时候,你可通过标记bean定义为延迟初始化来阻止单例bean的预实例化。延迟初始化的bean会通知IoC容器当第一次请求bean时创建一个bean实例,而不是在启动时创建。

        在XML中,延迟初始化通过<bean/>元素中的lazy-init特性来控制;例如:

1
2
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>

        当ApplicationContext读取到上面的配置,ApplicationContext启动时名字为lazy的bean不会进行预实例化,而名字为not.lazy的bean会进行预实例化。

        然而,当延迟初始化的bean是一个非延迟初始化的单例bean的依赖时,ApplicationContext会在启动时创建延迟初始化的bean,因为它必须提供单例bean的依赖。延迟初始化的bean会注入到单例bean中,而在其它地方它是非延迟初始化的。

        你也可以在容器中通过<beans/>中的default-lazy-init特性控制延迟初始化;例如:

1
2
3
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>

3.4.5 自动注入协作bean

        Spring容器能自动装配协作beans之间的关联关系。你可以允许Spring通过检查ApplicationContext中的内容自动的为你的bean解析协作者(其它bean)。自动装配有以下优势:

  • 自动装配能明显减少指定属性或构造函数参数的需要。(其它的机制例如在本章其它地方讨论的bean模板在这一点上也是非常重要的。)

  • 当对象变化时自动装配能更新配置。例如,如果你需要增加一个类的依赖项,依赖项可以是满足自动装配的而不需要你去修改配置。因此自动装配在开发时尤其有用,当代码基础变的更稳定时可以改为显式装配。

        当使用基于XML的配置元数据时,通过使用<bean/>元素的autowire特性你可以指定一个bean定义的自动装配模式。自动注入功能有四种模式。你可以指定每个bean的自动装配模式,因此你可以选择使用哪一种模式。

表 3.2 自动装配模式

模式 解析
no (默认)无自动装配。引用bean必须通过ref元素定义。对于更大的部署,不推荐更改默认设置,因为显式指定协作者更清晰并且更易控制。在某种程度上来说,它记录了系统的结构。
byName 通过属性名称自动装配。Spring寻找与需要自动装配的属性同名的bean。例如,如果一个bean定义设置为通过名称自动装配,它有一个master属性(也就是说,它有一个setMaster(..)方法),Spring寻找名字为master的bean定义,使用它设置属性值。
byType 如果容器中含有属性类型已知的一个bean,那么可以允许按类型自动装配属性。如果此类型的bean不止一个,则会抛出致命的异常,这意味着你可能不能使用byType来注入那个bean。如果没有匹配的bean,则什么也不做;属性没有被设置。
constructor byType类似,但是应用到构造函数参数上的。如果容器中没有一个构造函数参数bean的确定类型,将会抛出一个致命的异常。

        通过byType或构造函数自动装配模式,你可以配置数组和集合类型。在这种情况下容器内所有能匹配期望类型的自动装配候选对象将被提供合适的依赖项。如果期望的key类型是String类型,你可以自动装配强类型的Maps。自动装配的Maps的值将有所有匹配期望类型的bean组成,Maps的键将包含对应的bean名称。

        你可以将依赖检查与自动装配相结合,它将在自动装配完成之后执行。

自动装配的优势与限制

        当自动装配在整个工程中一致的使用时其效果最好。如果通常情况下不使用自动装配,仅在一两个bean定义中使用自动装配开发人员可能感到非常困惑。

        考虑一下自动装配的限制与缺点:

  • propertyconstructor-arg中显式依赖的设置总是会覆盖自动装配。你不能自动装配所谓的简单属性例如基本类型,StringsClasses(和简单类型的数组)。这是设计上的限制。

  • 与显式配置相比,自动装配是更不确定的。尽管Spring小心的避免猜测以防歧义性引起无法预料的后果,但Spring管理的对象之间的关系不再被显式的记录。

  • Spring容器中能产生文档的工具可能得不到配置信息。

  • setter方法或构造函数参数指定的类型进行自动装配时可能匹配到容器中多个bean的定义。对于数组,集合或Maps而言,这是一个不必要的问题。然而对于只期望一个值的依赖而言,这个歧义性不能任意解决。如果不能获得唯一的bean定义,会抛出异常。

        后面的方案中,你有一些选择:

  • 放弃自动装配支持显式配置。

  • 通过设置bean的autowire-candidate特性为false来避免自动装配。

  • 通过设置<bean/>元素的primary特性为true来指定一个单例bean定义作为主要的候选bean。

  • 通过基于注解的配置实现更多细颗粒的控制,如3.9小节 “基于注解的容器配置”。

排除bean在自动装配之外

        在单个bean的基础上,你可以排除bean在自动装配之外。在Spring的XML形式中,设置<bean/>元素的autowire-candidate特性为false;容器会使自动装配基础框架不能得到指定bean定义(包括注解类型的配置,例如@Autowired)。

        你也可以根据bean名称的匹配模式限制自动装配的候选目标。顶层的<beans/>元素可以接收default-autowire-candidates特性中的一个或多个模式。例如,为了限制自动装配候选目标匹配任何名字以Repository结尾的bean,可以提供一个*Repository值。为了提供多种模式,可以定义一个以逗号为分隔符的列表。bean定义中autowire-candidate特性显示的值truefalse最是优先起作用的,对于这些bean而言,模式匹配规则不起作用。

        这些技术对于那些你从不想通过自动装配方式注入到其它bean中的beans而言是很有用的。这不意味着一个排除的bean它本身不能通过自动装配进行配置。更确切的说,bean本身不是一个进行其它bean进行自动装配的候选者。

3.4.6 方法注入

        在大多数应用场景中,容器中的大多数bean是单例的。当一个单例bean需要与另一个单例bean协作时,或一个非单例bean需要与另一个非单例bean协作时,你通常通过定义一个bean作为另一个bean的一个属性来处理这个依赖关系。当bean的生命周期不同时问题就出现了。假设一个单例bean A需要使用非单例(标准)bean B时,也许A中的每一个方法调用都要使用bean B。容器仅创建单例bean A一次,因此仅有一次设置属性的机会。容器不能在每次需要bean B时提供一个bean B的新的实例。

        一个解决方案是放弃一些控制反转。你可以使bean A通过实现ApplicationContextAware接口感知到容器,每个bean A需要的时候就通过getBean("B")调用向容器请求(通常是新的)一个bean B的实例。下面是这种方法的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
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) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

        前面所讲的不是让人满意的,因为业务代码能感知并耦合了Spring框架。方法注入,Spring IoC容器的一个有点高级的特性,允许使用一种干净的方式来处理这个案例。

你可以在blog entry中了解更多关于方法注入的动机。

查找方法注入

        查找方法注入是容器的一种覆盖其管理的beans中的方法的能力,可以返回容器中另一个命名bean查找结果。查找通常会涉及到一个标准bean,如前一小节中讲的那样。Spring框架实现了查找方法注入,它是通过使用CGLIB库生成的字节码来动态的产生一个覆盖这个方法的子类。

  • 为了使动态子类化起作用,Spring bean容器要进行子类化的类不能是最终的类,要进行重写的方法也不是最终的方法。

  • 单元测试一个含有抽象方法的类需要你自己对这个类进行子类化,并且提供这个抽象方法的stub实现。

  • 实体方法对于要求获得实体类的组件扫描也是必需的。

  • 一个更关键的限制是查找方法不能与工厂方法一起工作,尤其是在配置类中不能与@Bean方法同时起作用,由于那种情况下容器不能控制实例的创建,因此不能在飞速写入中创建一个运行时产生的子类。

  • 最后,方法注入的目标对象不能被序列化。

        看一下前面代码片中的CommandManager类,你可以看到Spring容器将会动态的覆盖createCommand()方法的实现。CommandManager类不会有任何Spring依赖,重写的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}

        客户类中包含要注入的方法(在这个例子中是CommandManager),要注入的方法需要下面形式的一个签名:

1
<public|protected> [abstract] <return-type> theMethodName(no-arguments);

        如果这个方法是抽象的,动态产生的子类会实现这个方法。另外,动态产生的子类会覆盖原来的类中定义的实体方法。例如:

1
2
3
4
5
6
7
8
9
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="command"/>
</bean>

        无论什么时候识别为commandManager的bean需要一个command bean的新实例,它都会调用它的createCommand()方法。如果真的需要的话,你必须小心的部署command bean为一个原型。如果它被部署为一个单例,每次都会返回同一个command实例。

感兴趣的读者可能也会发现ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)使用这种方法。ServiceLocatorFactoryBean中使用的方法与另一个工具类ObjectFactoryCreatingFactoryBean中的方法类似,但它允许你指定你自己的查找接口,与Spring特定的查找接口相反。这些类的额外信息请查询Java文档。

任意的方法替换

        一种比查找方法注入更少使用的形式是用另一种方法实现替换管理的bean中任意方法的能力。用户可以安全跳过本节剩下的部分,直到这个方法真正需要的时候再看。

        在基于XML的配置元数据中,对于一个部署的bean,你可以通过replaced-method元素用另一个方法实现替换现有的方法实现。考虑下面的类,有一个我们想覆盖的computeValue方法:

1
2
3
4
5
6
7
8
9
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}

        实现了org.springframework.beans.factory.support.MethodReplacer接口的类提供了一种新的方法定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}

        部署最初的类的bean定义和指定的重写方法如下:

1
2
3
4
5
6
7
8
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<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

1
2
3
java.lang.String
String
Str

        因为参数数目经常是足够区分每个可能的选择的,通过允许定义匹配参数类型的最短字符串类型,这个缩写可以保存许多类型。

如果有收获,可以请我喝杯咖啡!