[TOC]
### 依赖
典型的企业应用不会单一得由一个对象组成(或者说Spring术语中的bean)。即便是最简单的系统也需要多个对象共同协作来展示给终端用户一个条理分明的应用。接下来的这一节内容会阐述如何定义多个独立于应用程序的bean一起协同工作完成目标。
#### 1.4.1. 依赖注入
依赖注入 (DI) 是指对象之间的依赖关系,也就是说,一起协作的其他对象只通过构造器的参数、工厂方法的参数或者由构造函数或者工厂方法创建的对象设置属性。因此容器的工作就是创建bean并注入那些依赖关系。这个过程实质通过直接使用类的构造函数或者服务定位模式来反转控制bean的实例或者其依赖关系的位置,因此它有另外一个名字叫控制反转 (IoC)。
运用了DI原理代码会更加清晰并且由依赖关系提供对象也将使各层次的松耦合变得更加容易。对象不需要知道其依赖关系,也不需要知道它的位置或者类之间的依赖。因此,你的类会更容易测试,尤其是当依赖关系是在接口或者抽象基本类,
DI主要有两种注入方式,即构造器注入和Setter注入。
##### 构造器注入
基于构造器注入 DI通过调用带参数的构造器来实现,每个参数代表着一个依赖关系。此外,还可通过给 静态 工厂方法传参数来构造bean。接下来的介绍将认为给构造器传参数和给静态工厂方法传参数是类似的。下面展示了只能使用构造器来注入依赖关系的例子。请注意这个类并没有什么 特别 之处,它只是个普通的POJO,不依赖于特殊的接口,抽象类或者注解。
~~~Java
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定义中构造器参数的定义顺序就是这些参数的顺序并依次进行匹配。看下面的代码:
~~~Java
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
~~~
上述代码中参数类型定义不存在潜在的歧义,我们假设Bar和Baz之间不存在继承关系。因此,下面代码中在元素<constructor-arg/>的配置即使没有明确指定构造参数顺序或者类型也会起作用。
~~~xml
<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并不能知道该值的类型,不借助其他帮助Spring将不能通过类型进行匹配。看下面的类:
~~~Java
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属性来显式指定那些简单类型那个的构造参数类型,比如:
~~~xml
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
~~~
使用index属性来显式指定构造参数的索引,比如:
~~~xml
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
~~~
使用索引可以解决多个简单值的混淆,还能解决构造方法有两个相同类型的参数的混淆问题,注意index是从0开始的。
你也可以使用构造器参数命名来指定值的类型:
~~~xml
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>
~~~
请记住为了使这个起作用,你的代码编译时要打开编译模式,这样Spring可以检查构造方法的参数。如果你不打开调试模式(或者不想打开),也可以使用 @ConstructorProperties JDK注解明确指出构造函数的参数。下面是简单的例子:
~~~Java
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
~~~
##### Setter注入
在调用了无参的构造方法或者无参的静态工厂方法实例化bean之后,容器通过回调bean的setter方法来完成setter注入。接下来的例子将展示只使用setter注入依赖。这个类是个普通的Java类
~~~Java
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所管理的beans支持构造函数注入和setter注入,在一些依赖已经使用构造器注入之后它还支持setter注入。你可以以BeanDefinition的形式配置依赖,它能根据指定的PropertyEditor实现将属性从一种格式转化为另外一种格式。但是,大多数Spring的使用者不会直接使用这些类(也就是通过编程的形式),而是采用XML配置这些bean,注解的组件(即用@Component,@Controller等注解类),或者基于@Configuration类的@Bean方法。本质上这些资源会转换成BeanDefinition的实例并且用于加载整个Spring IoC容器实例。
>:-: **构造器注入还是Setter注入**
>
>因为你可以混合使用构造器注入和setter注入, 强制性依赖关系 时使用构造器注入, 可选的依赖关系 时使用setter方法或者配置方法是比较好的经验法则。请注意@Required注解在setter方法上可以注入所需要的依赖。
>
>Spring开发团队一般主张当实现的应用组件是不可变对象时使用构造器注入并且要保证所需的依赖不是null。此外构造器注入的组件总是返回给客户端(或调用)完整的初始化状态。另一方面,大量的构造器参数造成糟糕的代码异味,这表明类可能承担了太多的职责应该需要重构以便更好的适当分离要解决的问题。
>
>setter注入主要只用作可选的依赖,这些依赖分配合理的缺省值。否则,当代码使用依赖时必须进行非空检查。setter注入的一个好处是setter方法使得这个类的对象在以后的某个时候还可合理的重新配置或者重新注入。JMX MBeans的管理就是一个很好的setter注入例子。
使用DI的风格可以使特定的类更有意义。有时,使用第三方类时你并没有资源,那么这个选择很适合你。举个例子,如果第三方类并不公开任何setter方法,那么构造器注入可能是唯一可用的依赖注入方式。
###### 依赖解决步骤
容器解决依赖问题通常有以下几个步骤:
描述所有bean的ApplicationContext创建并根据配置的元数据初始化。配置的元数据可以通过置顶的XML,Java代码或者注解。
* 每个bean的依赖将以属性,构造器参数或者静态工厂方法的形式出现。当这些bean被创建时,这些依赖将会提供给该bean。
* 每个属性或者构造器参数既可以是一个实际的值也可以是容器中的另一个引用。
* 每个指定的属性或者构造器参数值必须能够被转换成特定的格式或构造参数所需的类型。默认情况下Spring会以String类型转换为各种内置类型,比如int,long, String, boolean 等。
当Spring容器创建时容器会校验每个bean的配置。但是在bean被实际创建 前,bean的值并不会被设置。那些单例类型的bean和被设置为预安装(默认)的bean会在容器创建时与容器同时创建。Scopes是在Section 5.5, “Bean作用域”中定义的。同时,另外的bean只会在被需要时创建。伴随着bean被实际创建,作为该bean的依赖和它的依赖的依赖(以此类推)会被创建和分配。注意 这些依赖之间的解决会显示迟一些.
>:-: **循环依赖**
>
>如果你主要使用构造器注入,很有可能会产生无法解决的循环依赖问题。 举个例子:A类需要通过构造器注入B类的实例,并且B类又需要通过构造器注入A类的实例。如果为类A和类B配置的bean被相互注入的话,Spring IoC容器在运行时会检测到这个循环依赖并且抛出一个BeanCurrentlyInCreationException异常。
>
>一个可能的解决方法是修改类的源代码,将构造器注入改为setter注入。或者只使用setter注入避免使用构造器注入。换句话说,虽然这并不被推荐使用,你可以使用setter注入配置循环依赖。 和通常的情况不同(没有循环依赖),bean A 和bean B之间的循环依赖将会导致其中一个bean在被完全初始化的之前被注入到另一个bean里(先有鸡先有蛋的问题)
通常你可以信赖Spring。在容器加载时Spring会检查配置,比如不存在的bean和循环依赖。当bean创建时,Spring尽可能迟得设置属性和依赖关系。这意味着即使Spring正常加载,在你需要一个存在问题或者它的依赖存在问题的对象时,Spring会报出异常。举个例子,bean因设置缺少或者无效的属性会抛出一个异常。因为一些配置问题存在将会导致潜在的可见性被延迟,所以默认ApplicationContext的实现bean采用提前实例化的单例模式。在实际需要之前创建这些bean会带来时间和内存的开销,当ApplicationContext创建完成时你会发现配置问题,而不是之后。你也可以重写默认的行为使得单例bean延迟实例化而不是提前实例化。
如果不存在循环依赖,当一个或者多个协助bean会被注入依赖bean时,每个协助bean必须在注入依赖bean之前 完全 配置好。这意味着如果bean A对bean B存在依赖关系, 那么Spring Ioc容器在调用bean A的setter方法之前会完全配置bean B。换句话说,bean会被实例化(如果不是采用提前实例化的单例模式),相关的依赖会被设置好,相关的lifecycle方法(比如configured init 方法或者InitializingBean callback 方法)会被调用
###### 一些依赖注入的例子
接下来的Setter注入例子使用基于XML的配置元数据的方式。相应的Spring XML配置文件:
~~~xml
<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"/>
~~~
~~~Java
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里的属性,接下来的例子会使用构造器注入:
~~~xml
<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"/>
~~~
~~~Java
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调用静态工厂方法来返回对象的实例:
~~~xml
<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"/>
~~~
~~~Java
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属性外),所以这里不作详细讨论。
#### 1.4.2 依赖配置详解
正如在前面章节所提到的,你可以定义bean的属性和构造器参数作为其他所管理的bean的依赖(协作), 或者是内联的bean。基于XML的Spring配置元数据支持使用\<property/> 和 \<constructor-arg/>元素定义。
##### 直接变量 (基本类型, String类型等)
\<property/>元素的value值通过可读的字符串形式来指定属性和构造器参数。Spring的conversion service 把String转换成属性或者构造器实际需要的类型。
~~~xml
<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>
~~~
接下来的例子使用p 命名空间简化XML配置
~~~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
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更加简洁;但是,编码的错误只有在运行时才会被发现而不是编码设计的时候,除非你在定义bean的时候, 使用 IntelliJIDEA 或者 Spring Tool Suite (STS) 支持动态属性补全的IDE。IDE的帮助是非常值得推荐的。
你也可以配置java.util.Properties实例,就像这样:
~~~xml
<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 实例。由于这种做法非常简单,所以这是Spring团队在很多地方采用内嵌的\<value/>元素代替value属性。
##### idref元素
idref元素用来将容器内其他bean的id(值是字符串-不是引用)传给元素\<constructor-arg/> 或者 \<property/>
~~~xml
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean" />
</property>
</bean>
~~~
上面的bean定义片段完全等同于(在运行时)下面片段:
~~~xml
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean" />
</bean>
~~~
第一种形式比第二种形式更好,因为使用idref标签允许容器在部署时验证引用的bean是否存在。 在第二种形式中,传给 client bean中属性targetName的值并没有被验证。 只有当 client bean完全实例化的时候错误才会被发现(可能伴随着致命的结果)。如果 client bean是原型 bean。那么这个错误和异常可能只有再容器部署很长一段时间后才能被发现。
>[info] idref元素上的local属性在4.0之后不再支持,因为它不再提供普通bean的价值。当你升级到4.0 schema时需要修改存在的idref local为idref bean
与ProxyFactoryBean bean定义中使用\<idref/>元素指定AOP 拦截器配置 (版本不低于Spring 2.0)相同之处在于:当你指定拦截器名称的时候使用<idref/>元素可以防止你拼错拦截器的id。
##### 引用其他bean(协作者)
在 \<constructor-arg/> 或者 \<property/>可以使用ref 元素。该元素用来将bean中指定属性的值设置为对容器的另外一个bean(协作者)的引用。 该引用bean将被作为依赖注入,而且再注入之前会被初始化(如果协作者是单例)。所有的引用最终都是另一个对象的引用。 bean的范围和验证依赖于你指定的bean,local,或者 parent属性的id/name。
通过<ref/>标签的bean属性定义目标bean是最常见的形式,通过该标签可以引用同一容器或者父容器任何bean,无论是否在 相同的xml文件中。xml的bean元素的值既可以目标bean的id属性也可以是其中一个目标bean的name 属性值。
~~~xml
<ref bean="someBean"/>
~~~
通过parent属性指定目标bean来创建bean的引用,该bean是当前容器下的父级容器。parent属性值既可以是目标bean的id也可以是 name属性值。而且目标bean必须在当前容器的父级容器中。使用parent属性的主要用途是为了用某个与父级容器中的bean同名的代理来包装父级容器中的一个bean。
~~~xml
<!-- in the parent context -->
<bean id="accountService" class="com.foo.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
~~~
~~~xml
<!-- 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>
~~~
>[info]ref元素上的local属性4.0 beans xsd之后不在支持,因为它能提供的值不比普通bean多。当你升级到4.0schema时需要修改存在的ref local 为ref bean。
##### 内部bean
所谓的内部bean就是指在 \<property/> 或者 \<constructor-arg/> 元素内部使用\<bean/>定义bean。
~~~xml
<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是不可能的。
##### 集合
在\<list/>, \<set/>, \<map/>, 和\<props/>元素中,你可以设置值和参数分别对应Java的集合类型List, Set, Map, 和 Properties
~~~xml
<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值还可以是以下任意元素:
~~~
bean | ref | idref | list | set | map | props | value | null
~~~
###### 集合合并
Spring容器也支持集合的 合并。开发者可以定义parent-style \<list/>, \<map/>, \<set/> 或者\<props/> 元素, 并定义child-style 的\<list/>, \<map/>, \<set/> 或者 \<props/>元素继承和覆盖自父集合。也就是说。父集合元素合并后的值就是子集合的最终结果,而且子集中的元素值将覆盖父集中对应的值。
关于合并的章节涉及到了parent-child bean机制。不熟悉父子bean的读者可参见relevant section.
接下来的例子展示了集合的合并:
~~~xml
<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的定义中\<props/>元素上的merge=true属性的用法。当child bean被解析并且被容器初始化,产生的实例包含了adminEmails ,Properties集合,其adminEmails将与父集合的adminEmails属性进行合并。
~~~
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
~~~
子bean的Properties集合将从父\<props/>集成所有属性元素。同时子bean的support值将覆盖父集合的相应值。
\<list/>, \<map/>, 和 \<set/>集合类型的合并处理都基本类似。\<list/>元素某个方面有点特殊,这和List集合类型的语义学有关 换句话说,比如维护一个有序集合的值,父bean的列表内容将排在子bean列表内容的前面。对于Map, Set和 Properties集合类型没有顺序的概念, 因此作为相关的Map, Set, 和 Properties实现基础的集合类型在容器内部排序的语义。
**集合合并的限制**
你不能合并两种不能类型的集合(比如Map 和 List),如果你这么做了将会抛出相应的异常。merge属性必须在继承 的子bean中定义。定义在父bean的集合属性上指定的merge属性是多余的并且得不到期望的合并结果。
**强类型集合**
Java 5 引入了泛型,这样你可以使用强类型集合。换句话说绳命一个只能包含String类型元素的Collection是可能的(比如)。 如果使用Spring来给bean注入强类型的Collection,你可以利用Spring的类型转换,在向强类型Collection添加元素前,这些元素将被转换。
~~~java
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
~~~
~~~xml
<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的底层类型转换 机制会把各种value元素值转换为Float,因此字符串9.99, 2.75 和3.99将会转换为实际的Float 类型。
###### Null和空字符串
Spring会把空属性当做空字符串处理。以下的基于XML配置的片段将email属性设置为空字符串。
~~~xml
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
~~~
先前的例子等同于以下Java代码:
~~~Java
exampleBean.setEmail("")
~~~
\<null/>元素处理null值,例如:
~~~xml
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
~~~
上面的配置等同于下面的Java代码:
~~~Java
exampleBean.setEmail(null)
~~~
###### XML使用p命名空间简化
使用p命名空间可以用bean 元素的属性代替\<property/>元素来描述属性值或者协作bean。
Spring支持名称空间的可扩展配置with namespaces,这些名称空间基于一种XML Schema定义。这章节涉及到的beans 配置都是定义在一个XML Schema文档理。但是p命名空间不是定义在XSD文件而是存在于Spring内核中。
下面的例子展示了两种XML片段,其结果是一样的:第一个使用了标准XML格式,第二种使用了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
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>
~~~
在例子中,使用p命名空间的bean定义有了一个叫email的属性。这告诉Spring要包含这个属性的声明。正如前面所说的, p命名空间不需要schema定义,因此你可以设置属性的名字作为bean的property的名字。
接下来的例子包括了两种以上bean的定义,都引用了另外一个bean。
~~~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
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"/>创建一个john bean 对jane bean的引用,第二个bean的定义使用了p:spouse-ref="jane",它们做了同样一件事情。在这个例子中spouse是属性名,而-ref部分声明了这不是一个直接的值而是另一个bean的引用。
>[info]p命名空间没有标准XML格式那么灵活。举个例子,声明属性的引用是以Ref结尾的,采用p命名空间将会产生冲突,但是采用标准XML 格式则不会。我们建议你小心的选择并和团队成员交流你的想法,避免在XML文档中同时使用所有的三种方法。
###### XML使用c命名空间简化
和“XML使用p命名空间简化”类似,Spring3.1 引入了c命名空间,使用内联的构造参数代替嵌套的constructor-arg元素
让我们回顾一下the section called “构造器注入”使用c命名空间的例子:
~~~xml
<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>
~~~
和p命名空间约定的一样(bean的引用以-ref结尾),c命名空间使用它们的名称作为构造器参数。同时它需要声明即使它没有 XSD schema中定义(但是它存在于Spring内核中)
极少数的情况下构造器参数的名称不可用(通常字节码没有经过调试信息编译),可以使用备份进行参数索引。
~~~xml
<!-- c-namespace index declaration -->
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>
~~~
>[info]因为XML的语法,索引标记需要_主导作为XML属性名称,这不能已数字开头(即使某些IDE这么允许)
在实践中,构造器解析机智机制在匹配参数上相当高效。除非你需要这么做,我们建议您在配置中使用符号名称。
###### 组合属性名称
当你设置bean属性时可以使用组合或者嵌套属性名称,只要路径上所有组件除了最终属性不为空。看以下bean的定义:
~~~xml
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
</bean>
~~~
foo bean有一个fred属性,fred属性又有个bob属性,bob属性又有个属性,最后把 sammy属性设置值为123。为了是这个起作用,foo的fred属性和 fred的bob属性在bean被构造后都不能为空,否则会抛出NullPointerException异常。
#### 1.4.3. 使用 depends-on
如果一个bean是另外一个bean的依赖,这通常意味着这个bean可以设置成为另外一个bean的属性。在XML配置文件中你可以使用<ref/> element实现依赖。但是某些时候bean之间的依赖并不是那么直接。举个例子:类的静态块初始化,比如数据库驱动的注册。depends-on属性 可以同于当前bean初始化之前显式地强制一个或多个bean被初始化。下面的例子中使用了depends-on属性来指定一个bean的依赖。
~~~xml
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
~~~
为了实现多个bean的依赖,你可以在depends-on中将指定的多个bean名字用分隔符进行分隔,分隔符可以是逗号,空格以及分号等。
~~~xml
<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" />
~~~
>[info]depends-on属性在bean的定义中可以指定初始化时的依赖和指定相应的销毁时的依赖,该依赖只针对于singleton bean 这样depends-on可以控制销毁顺序。
#### 1.4.4. 延迟初始化bean
ApplicationContext实现的默认行为就是再启动时将所有singleton bean提前进行实例化。 通常这样的提前实例化方式是好事,因为配置中或者运行环境的错误就会被立刻发现,否则可能要花几个小时甚至几天。如果你不想 这样,你可以将单例bean定义为延迟加载防止它提前实例化。延迟初始化bean会告诉Ioc容器在第一次需要的时候才实例化而不是在容器启动时就实例化。
在XML配置文件中,延迟初始化通过<bean/>元素的lazy-init属性进行控制,比如:
~~~xml
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
~~~
当ApplicationContext实现上面的配置时,设置为lazy的bean将不会在ApplicationContext启动时提前实例化,而`not.lazy`bean 却会被提前实例化。
但是当一个延迟加载的bean是单例bean的依赖,但这个单例bean又不是 延迟加载,ApplicationContext在启动时创建了延迟加载 的bean,因为它必须满足单例bean的依赖。因此延迟加载的bean会被注入单例bean,然而在其他地方它不会延迟加载。
你也可以使用<beans/>元素上的default-lazy-init属性在容器层次上控制延迟加载。比如:
~~~xml
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
~~~
#### 1.4.5. 自动装配协作者
Spring容器可以自动装配相互协作bean的关联关系。因此,如果可能的话,可以自动让Spring检测ApplicationContext的内容自动 处理协作者(其他bean)。自动装配有以下好处:
* 自动装配可以显著得减少指定属性或者构造器参数的需求。(其他的机制比如bean模板discussed elsewhere in this chapter在这方面也是由价值的)
* 当对象发生变化时自动装配可以更新配置。比如如果你需要给一个类添加依赖,那么这个依赖可以被自动满足而不需要你去修改配置。 因此自动依赖在开发时尤其有用,当系统趋于稳定时改为显式装配。
当使用XML配置元数据,可以使用\<bean/>元素的autowire属性 为定义的bean指定自动装配模式。你可以指定自动装配per bean,选择那种方式来自动装配。
*Table 自动装配模式*
| Mode | Explanation 模式解释 |
| --- | --- |
| no | (默认)不自动装配。Bean的引用必须用ref元素定义。对于较大的部署不建议改变默认设置,因为明确指定协作者能更好控制和维护系统。 在某种程度上,它记录了系统的结构。 |
| byName | 通过属性名称自动装配。Spring会寻找相同名称的bean并将其与属性自动装配。譬如,如果bean的定义设置了根据名称自动装配, 并且包含了一个master 属性(换句话说,它有setMaster(..)方法),Spring会寻找名为master的bean的定义,并用它来装配属性 |
| byType | 如果容器中存在一个与指定属性类型相同的bean,那么将与该属性自动装配。如果存在多个该类型的bean,将会抛出异常,并指出 不能使用byType自动装配这个bean。如果没有找到相同类型的,什么也不会发生。属性不会被设置。|
| constructor | 和byType类似,不同之处在于它应用于构造器参数。如果在容器中没有找到与构造器参数类型一致的bean,就会抛出异常。 |
byType 或者 constructor 自动装配模式也可以应用于数组和指定类型的集合。在这种情况下容器中的所有匹配的自动装配对象将 被应用于满足各种依赖。对于key值类型为String的强类型Map也可以自动装配。一个自动装配的Map value值将由所匹配类型的bean所填充。
你可以结合自动装配和依赖检查,后者将会在自动装配完成之后进行。
##### 自动装配的局限性和缺点
在工程里一致使用自动装配,这将会工作得很好。如果自动装配并不常使用,只使用在一个或两个bean的定义上, 它可能会对开发者产生困扰。
考虑一下自动装配的局限性和缺点:
* property 和 constructor-arg 显式的依赖设置总是会覆盖自动装配。你不能装配所谓的简单属性比如原始的Strings, 和 Classes (这样的简单属性数组也是)。这种缺陷是故意设计的。
* 自动装配没有显示编写精确。虽然在上面的表格提到的,Spring很小心得避免猜测模糊的情况,这可能会导致意想不到的结果。 Spring管理的对象之间的关系不再记录明确。
* 自动装配的依赖信息可能不能用于根据Spring容器生成文档的的工具。
* 在容器内部可能存在多个bean的定义与自动装配的setter方法或者构造器参数匹配。对于数组,集合或者Map来说,这不是问题。 但是对于单值依赖来说,就会存在模棱两可的问题。如果bean定义不唯一,装配时就会抛出异常。
针对于上述场景,你会有多个选项:
* 放弃自动装配以便于明确依赖关系。
* 在bean定义中通过设置autowire-candidate属性为false避免该bean自动装配,这将会在下一节中详细描述。
* 在bean定义中设置\<bean/>元素上的`primary`属性为true,将该bean设置为首选自动装配bean。
* 使用注解配置实现更加细粒度的控制,详情见**Annotation-based container configuration**。
##### 将bean排除在自动装配之外
在提前实例化bean的基础上,你可以将bean排除在自动装配之外。在Spring XML格式中,将\<bean/> 元素中的autowire-candidate属性 设置为false。容器会使特定的bean定义不可于自动装配(包括注解配置比如@Autowired)
你也可以对使用bean名字进行模式匹配来对自动装配进行限制。顶层的<beans/> 元素在它的default-autowire-candidates属性 接受一个或多个模式。譬如,为了限制
对于那些从来就不会被其他bean采用自动装配的方式注入的bean而言,这是有用的。不过这并不意味这被排除的bean自己就 不能使用自动装配来注入其他bean。更确切得说,该bean本身不会被考虑作为其他bean自动装配的候选者。
#### 1.4.6. 方法注入
在大部分的应用场景中,容器中的大部分bean是singletons类型的。当一个单例bean需要和另外一个单例bean, 协作时,或者一个费单例bean要引用另外一个非单例bean时,通常情况下将一个bean定义为另外一个bean的属性值就行了。不过对于具有不同生命周期的bean 来说这样做就会有问题了,比如在调用一个单例类型bean A的某个方法,需要引用另一个非单例(prototype)类型bean B,对于bean A来说,容器只会创建一次,这样就没法 在需要的时候每次让容器为bean A提供一个新的bean B实例。
上面问题的一个解决方法是放弃控制反转,你可以实现ApplicationContextAware接口来让bean A感知到容器, 并且在需要的时候通过使用使用getBean("B")向容器请求一个(新的)bean B实例。下面的例子使用了这个方法:
~~~Java
// 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容器的高级特性,可以以一种 干净的方法来处理这种情况。
你可以在 [this blog entry](https://spring.io/blog/2004/08/06/method-injection/)阅读更多关于方法注入的动机。
##### Lookup 方法注入
Lookup方法具有使容器覆盖受容器管理的bean方法的能力,从而返回指定名字的bean实例。在上述场景中,Lookup方法注入适用于原型bean。 Lookup方法注入的内部机制是Spring利用了CGLIB库在运行时生成二进制代码的功能,通过动态创建Lookup方法bean的子类从而达到复写Lookup方法的目的。
为了使动态子类起作用,Spring容器要子类化的类不能是final,并且需要复写的方法也不能是final。同样的,要测试一个包含 抽象方法的类也稍微有些不同,你需要子集编写它的子类提供该抽象方法的实现。最后,作为方法注入目标的bean不能是序列化的。 在Spring 3.2之后再也没必要添加CGLIB到classpath,因为CGLIB的类打包在了org.springframework下并且在Spring核心JAR中有所描述。 这样做既方便,又避免了与其他使用了不同版本CGLIB的项目的冲突。
再看一下在之前代码片段中的CommandManager类,你可以发现Spring容器会自动复写createCommand()方法的实现。CommandManager类 将不会有任何的Spring依赖,下面返工的例子可以看出:
~~~Java
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),此方法的定义需要按以下形式进行:
~~~Java
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
~~~
如果方法是抽象,动态生成的子类会实现该方法。否则,动态生成的子类会覆盖类里的具体方法。譬如:
~~~xml
<!-- 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部署为 原型(prototype)类型,如果这是实际需要的话。如果部署为singleton。那么每次将返回相同的 `command`bean。
或者,在基于注释的组件模型中,可以通过@lookup 注释声明一个查找方法:
~~~Java
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
~~~
或者,更蠢的方式是,您可以依赖目标 bean 根据查找方法声明的返回类型得到解析:
~~~Java
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
~~~
请注意,您通常会声明这种带有具体存根实现的注释查找方法,以便它们与 Spring 的组件扫描规则兼容,在这里,抽象类默认会被忽略。 此限制不适用于显式注册或显式导入的 bean 类的情况。
>[info]另一种访问不同范围的目标 bean 的方法是 ObjectFactory / Provider 注入点。 查看**Scoped beans as dependencies.**
>
>感兴趣的读者也可以找到 ServiceLocatorFactoryBean (在 org.springframework.beans.factory.config 包中)是有用的。
##### 自定义方法的替代方案
比起Lookup方法注入来,还有一种较少用到的方法注入形式,该注入能使用bean的另一个方法实现去替换自定义方法的方法。 除非你真的需要该功能,否则可以略过本节。
使用基于XML配置文件时,你可以使用replaced-method元素来达到用另一个方法来取代已有方法的目的。考虑下面的类,我们将覆盖 computeValue方法。
~~~Java
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
~~~
实现org.springframework.beans.factory.support.MethodReplacer接口的类提供了新的方法定义。
~~~Java
/**
* 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定义中指定了要配置的原始类和将要复写的方法:
~~~xml
<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:
~~~
java.lang.String
String
Str
~~~
因为参数的数目通常足够用来区别每个可能的选择,这个结晶能减少很多键盘输入的工作,它允许你只输入最短的匹配参数类型的字符串。