4.3.依赖

优质
小牛编辑
130浏览
2023-12-01

4.3. 依赖

典型的企业应用不会只由单一的对象(或bean)组成。毫无疑问,即使最简单的系统也需要多个对象一起来满足最终用户的需求。接下来的的内容除了阐述如何单独定义一系列bean外,还将描述如何让这些bean对象一起协同工作来实现一个完整的真实应用。

4.3.1. 注入依赖

依赖注入(DI)背后的基本原理是对象之间的依赖关系(即一起工作的其它对象)只会通过以下几种方式来实现:构造器的参数、工厂方法的参数,或给由构造函数或者工厂方法创建的对象设置属性。因此,容器的工作就是创建bean时注入那些依赖关系。相对于由bean自己来控制其实例化、直接在构造器中指定依赖关系或则类似服务定位器(Service Locator)模式这3种自主控制依赖关系注入的方法来说,控制从根本上发生了倒转,这也正是控制反转(Inversion of Control, IoC) 名字的由来。

应用DI原则后,代码将更加清晰。而且当bean自己不再担心对象之间的依赖关系(以及在何时何地指定这种依赖关系和依赖的实际类是什么)之后,实现更高层次的松耦合将易如反掌。

诚如此前的章节所述,DI主要有两种注入方式,即Setter注入和 构造器注入。

4.3.1.1. Setter注入

通过调用无参构造器或无参static工厂方法实例化bean之后,调用该bean的setter方法,即可实现基于setter的DI。

下面的例子将展示使用setter注入依赖。注意,这个类并没有什么特别之处,它就是普通的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 setMoveFinder(MovieFinder movieFinder) {
  this.movieFinder = movieFinder;
    }
    
    // business logic that actually 'uses' the injected MovieFinder is omitted...
}

4.3.1.2. 构造器注入

基于构造器的DI通过调用带参数的构造器来实现,每个参数代表着一个协作者。此外,还可通过给静态工厂方法传参数来构造bean。接下来的介绍将认为给构造器传参与给静态工厂方法传参是类似的。

下面的展示了只能使用构造器参数来注入依赖关系的例子。再次提醒,这个类并没有什么特别之处

public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the 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...
}

如何在构造器注入和Setter注入之间进行选择?

由于大量的构造器参数可能使程序变得笨拙,特别是当某些属性是可选的时候。因此通常情况下,Spring开发团队提倡使用setter注入。而且setter DI在以后的某个时候还可将实例重新配置(或重新注入)(JMX MBean就是一个很好的例子)。

尽管如此,构造器注入因为某些原因还是受到了一些人的青睐。一次性将所有依赖注入的做法意味着,在未完全初始化的状态下,此对象不会返回给客户代码(或被调用),此外对象也不可能再次被重新配置(或重新注入)。

对于注入类型的选择并没硬性的规定。只要能适合你的应用,无论使用何种类型的DI都可以。对于那些没有源代码的第三方类,或者没有提供setter方法的遗留代码,我们则别无选择--构造器注入将是你唯一的选择。

BeanFactory对于它所管理的bean提供两种注入依赖方式(实际上它也支持同时使用构造器注入和Setter方式注入依赖)。需要注入的依赖将保存在BeanDefinition中,它能根据指定的PropertyEditor实现将属性从一种格式转换成另外一种格式。然而,大部份的Spring用户并不需要直接以编程的方式处理这些类,而是采用XML的方式来进行定义,在内部这些定义将被转换成相应类的实例,并最终得到一个Spring IoC容器实例。

处理bean依赖关系通常按以下步骤进行:

  1. 根据定义bean的配置(文件)创建并初始化BeanFactory实例(大部份的Spring用户使用支持XML格式配置文件的BeanFactoryApplicationContext实现)。

  2. 每个bean的依赖将以属性、构造器参数、或静态工厂方法参数的形式出现。当这些bean被实际创建时,这些依赖也将会提供给该bean。

  3. 每个属性或构造器参数既可以是一个实际的值,也可以是对该容器中另一个bean的引用。

  4. 每个指定的属性或构造器参数值必须能够被转换成属性或构造参数所需的类型。默认情况下,Spring会能够以String类型提供值转换成各种内置类型,比如intlongStringboolean等。

需要强调的一点就是,Spring会在容器被创建时验证容器中每个bean的配置,包括验证那些bean所引用的属性是否指向一个有效的bean(即被引用的bean也在容器中被定义)。然而,在bean被实际创建之前,bean的属性并不会被设置。对于那些singleton类型和被设置为提前实例化的bean(比如ApplicationContext中的singleton bean)而言,bean实例将与容器同时被创建。而另外一些bean则会在需要的时候被创建,伴随着bean被实际创建,作为该bean的依赖bean以及依赖bean的依赖bean(依此类推)也将被创建和分配。

循环依赖

当你主要使用构造器注入的方式配置bean时,很有可能会产生循环依赖的情况。

比如说,一个类A,需要通过构造器注入类B,而类B又需要通过构造器注入类A。如果为类A和B配置的bean被互相注入的话,那么Spring IoC容器将在运行时检测出循环引用,并抛出 BeanCurrentlyInCreationException异常。

对于此问题,一个可能的解决方法就是修改源代码,将构造器注入改为setter注入。另一个解决方法就是完全放弃使用构造器注入。

通常情况下,你可以信赖Spring,它会在容器加载时发现配置错误(比如对无效bean的引用以及循环依赖)。Spring会在bean创建的时才去设置属性和依赖关系(只在需要时创建所依赖的其他对象)。Spring容器被正确加载之后,当获取一个bean实例时,如果在创建bean或者设置依赖时出现问题,那么将抛出一个异常。因缺少或设置了一个无效属性而导致抛出一个异常的情况的确是存在的。因为一些配置问题而导致潜在的可见性被延迟,所以在默认情况下,ApplicationContext实现中的bean采用提前实例化的singleton模式。在实际需要之前创建这些bean将带来时间与内存的开销。而这样做的好处就是ApplicationContext被加载的时候可以尽早的发现一些配置的问题。不过用户也可以根据需要采用延迟实例化来替代默认的singleton模式。

最后,我们还要提到的一点就是,当协作bean被注入到依赖bean时,协作bean必须在依赖bean之前完全配置好。例如bean A对bean B存在依赖关系,那么Spring IoC容器在调用bean A的setter方法之前,bean B必须被完全配置,这里所谓完全配置的意思就是bean将被实例化(如果不是采用提前实例化的singleton模式),相关的依赖也将被设置好,而且所有相关的lifecycle方法(如IntializingBean的init方法以及callback方法)也将被调用。

4.3.1.3. 一些例子

首先是一个用XML格式定义的Setter DI例子。相关的XML配置如下:

<bean id="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 bean="yetAnotherBean"/></property>
  <property name="integerProperty" value="1"/>
</bean>
  <bean id="anotherExampleBean"/>
<bean id="yetAnotherBean"/>
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;
    }    
}

正如你所看到的,bean类中的setter方法与xml文件中配置的属性是一一对应的。

接着是构造器注入的例子。以下是xml配置代码以及相对应的java类代码。

<bean id="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"/>
<bean id="yetAnotherBean"/>
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;
    }
}

如你所见,在xml bean定义中指定的构造器参数将被用来作为传递给类ExampleBean构造器的参数。

现在来研究一个替代构造器的方法,采用静态工厂方法返回对象实例:

<bean id="exampleBean"
factory-method="createInstance">
  <constructor-arg><ref bean="anotherExampleBean"/></constructor-arg>
  <constructor-arg><ref bean="yetAnotherBean"/></constructor-arg>
  <constructor-arg><value>1</value></constructor-arg>
</bean>
  <bean id="anotherExampleBean"/>
<bean id="yetAnotherBean"/>
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元素提供,这与使用构造器注入时完全一样。而且,重要的是,工厂方法所返回的实例的类型并不一定要与包含static工厂方法的类类型一致。尽管在此例子中它的确是这样。非静态的实例工厂方法与此相同(除了使用factory-bean属性替代class属性外),因而不在此细述。

4.3.2. 构造器参数的解析

构造器参数将根据类型来进行匹配。如果bean定义中的构造器参数类型明确,那么bean定义中的参数顺序就是对应构造器参数的顺序。考虑以下的类...

package x.y;
  public class Foo {
public Foo(Bar bar, Baz baz) {
  // ...
    }
}

这里的参数类型非常明确(当然前提是假定类BarBaz在继承层次上并无任何关系)。因此下面的配置将会很好地工作,且无须显式地指定构造器参数索引及其类型。

<beans>
    <bean name="foo">
  <constructor-arg>
<bean/>
  </constructor-arg>
  <constructor-arg>
<bean/>
  </constructor-arg>
    </bean>
</beans>

当引用的bean类型已知,则匹配没有问题(如上述的例子)。但是当使用象<value>true<value>这样的简单类型时,Spring将无法决定该值的类型,因而仅仅根据类型是无法进行匹配的。考虑以下将在下面两节使用的类:

package examples;
  public class ExampleBean {
// No. of years to the 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;
    }
}

4.3.2.1. 构造器参数类型匹配

针对上面的这种情况,我们可以在构造器参数定义中使用type属性来显式的指定参数所对应的简单类型。例如:

<bean id="exampleBean">
  <constructor-arg type="int"><value>7500000</value></constructor-arg>
  <constructor-arg type="java.lang.String"><value>42</value></constructor-arg>
</bean>

4.3.2.2. 构造器参数的索引

通过使用index属性可以显式的指定构造器参数出现顺序。例如:

<bean id="exampleBean">
  <constructor-arg index="0" value="7500000"/>
  <constructor-arg index="1" value="42"/>
</bean>

使用index属性除了可以解决多个简单类型构造参数造成的模棱两可的问题之外,还可以用来解决两个构造参数类型相同造成的麻烦。注意:index属性值从0开始

提示

指定构造器参数索引是使用构造器IoC首选的方式。

4.3.3. bean属性及构造器参数详解

正如前面所提到的,bean的属性及构造器参数既可以引用容器中的其他bean,也可以是内联(inline,在spring的XML配置中使用<property/><constructor-arg/>元素定义)bean。

4.3.3.1. 直接量(基本类型、Strings类型等。)

<value/>元素通过字符串来指定属性或构造器参数的值。正如前面所提到的,JavaBean PropertyEditor将用于把字符串从java.lang.String类型转化为实际的属性或参数类型。

<bean id="myDataSource" destroy-method="close"
   >
  <!-- results in a setDriverClassName(String) call -->
  <property name="driverClassName">
    <value>com.mysql.jdbc.Driver</value>
  </property>
  <property name="url">
    <value>jdbc:mysql://localhost:3306/mydb</value>
  </property>
  <property name="username">
    <value>root</value>
  </property>
</bean>
4.3.3.1.1. idref元素

idref元素用来将容器内其它bean的id传给<constructor-arg/><property/>元素,同时提供错误验证功能。

<bean id="theTargetBean"/>
  <bean id="theClientBean">
    <property name="targetName">
  <idref bean="theTargetBean" />
    </property>
</bean>

上述bean定义片段完全地等同于(在运行时)以下的片段:

<bean id="theTargetBean"/>
  <bean id="client">
    <property name="targetName">
  <value>theTargetBean</value>
    </property>
</bean>

第一种形式比第二种更可取的主要原因是,使用idref标记允许容器在部署时 验证所被引用的bean是否存在。而第二种方式中,传给client bean的targetName属性值并没有被验证。任何的输入错误仅在client bean实际实例化时才会被发现(可能伴随着致命的错误)。如果client bean 是prototype类型的bean,则此输入错误(及由此导致的异常)可能在容器部署很久以后才会被发现。

此外,如果被引用的bean在同一XML文件内,且bean名字就是bean id,那么可以使用local属性,此属性允许XML解析器在解析XML文件时来对引用的bean进行验证。

<property name="targetName">
   <!-- a bean with an id of 'target' must exist, else an XML exception will be thrown -->
   <idref local="theTargetBean"/>
</property>

上面的例子与在ProxyFactoryBean bean定义中使用<idref/>元素指定AOP interceptor的相同之处在于:如果使用<idref/>元素指定拦截器名字,可以避免因一时疏忽导致的拦截器ID拼写错误。

4.3.3.2. 引用其它的bean(协作者)

<constructor-arg/><property/>元素内部还可以使用ref元素。该元素用来将bean中指定属性的值设置为对容器中的另外一个bean的引用。如前所述,该引用bean将被作为依赖注入,而且在注入之前会被初始化(如果是singleton bean则已被容器初始化)。尽管都是对另外一个对象的引用,但是通过id/name指向另外一个对象却有三种不同的形式,不同的形式将决定如何处理作用域及验证。

第一种形式也是最常见的形式是通过使用ref标记指定bean属性的目标bean,通过该标签可以引用同一容器或父容器内的任何bean(无论是否在同一XML文件中)。XML 'bean'元素的值既可以是指定bean的id值也可以是其name值。

<ref bean="someBean"/>

第二种形式是使用ref的local属性指定目标bean,它可以利用XML解析器来验证所引用的bean是否存在同一文件中。local属性值必须是目标bean的id属性值。如果在同一配置文件中没有找到引用的bean,XML解析器将抛出一个例外。如果目标bean是在同一文件内,使用local方式就是最好的选择(为了尽早地发现错误)。

<ref local="someBean"/>

第三种方式是通过使用ref的parent属性来引用当前容器的父容器中的bean。parent属性值既可以是目标bean的id值,也可以是name属性值。而且目标bean必须在当前容器的父容器中。使用parent属性的主要用途是为了能引用到与当前容器中同名的父容器中的bean对象。

<ref parent="someBean"/>

4.3.3.3. 内部bean

所谓的内部bean(inner bean)是指在一个bean的<property/><constructor-arg/>元素中使用<bean/>元素定义的bean。内部bean定义不需要有id或name属性,即使指定id 或 name属性值也将会被容器忽略。

以下是个关于内部bean例子。

<bean id="outer">
  <!-- instead of using a reference to a target bean, simply define the target inline -->
  <property name="target">
    <bean> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
    </bean>
  </property>
</bean>

注意:内部bean中的singleton标记及idname属性将被忽略。内部bean总是匿名的且它们总是prototype模式的。同时将内部bean注入到包含该内部bean之外的bean是可能的。

4.3.3.4. 集合

通过<list/><set/><map/><props/>元素可以定义和设置与Java Collection类型对应ListSetMapProperties的值。

<bean id="moreComplexObject">
  <!-- results in a setAdminEmails(java.util.Properties) call -->
  <property name="adminEmails">
    <props>
  <prop key="administrator">administrator@somecompany.org</prop>
  <prop key="support">support@somecompany.org</prop>
  <prop key="development">development@somecompany.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>
    <value>yup an entry</value>
</key>
<value>just some string</value>
  </entry>
  <entry>
<key>
    <value>yup a ref</value>
</key>
<ref bean="myDataSource" />
  </entry>
    </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
4.3.3.4.1. 集合合并

从2.0开始,Spring IoC容器将支持集合的合并。这样我们可以定义parent-style和child-style的listmapsetprops元素,子集合的值从其父集合继承和覆盖而来;也就是说,父子集合元素合并后的值就是子集合中的最终结果,而且子集合中的元素值将覆盖父集全中对应的值。

请注意,关于合并的这部分利用了parent-child bean机制。此内容将在后面介绍,不熟悉父子bean的读者可参见第 4.6 节 “bean的继承”。

用一个例子可能是对此特性的最好描述:

<beans>
<bean id="parent" abstract="true">
    <property name="adminEmails">
  <props>
<prop key="administrator">administrator@somecompany.com</prop>
<prop key="support">support@somecompany.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@somecompany.com</prop>
<prop key="support">support@somecompany.co.uk</prop>
  </props>
    </property>
</bean>
<beans>

在上面的例子中,childbean的adminEmails属性的<props/>元素上使用了merge=true属性。当childbean被容器实际解析及实例化时,其 adminEmails将与父集合的adminEmails属性进行合并。

administrator=administrator@somecompany.com
sales=sales@somecompany.com

support=support@somecompany.co.uk

注意到这里子bean的Properties集合将从父 <props/>继承所有属性元素。同时子bean的support值将覆盖父集合的相应值。

对于listmapset集合类型的合并处理都基本类似,在某个方面list元素比较特殊,这涉及到List集合本身的语义学义学,就拿维护一个有序集合中的值来说,父bean的列表内容将排在子bean列表内容的前面。 对于mapsetprops集合类型没有顺序的概念,因此作为相关的mapsetprops实现基础的集合类型在容器内部没有排序的语义

最后需要指出的一点就是,合并功能仅在Spring 2.0(及随后的版本中)可用。不同的集合类型是不能合并(如maplist是不能合并的),否则将会抛出相应的Exceptionmerge属性必须在继承的子bean中定义,而在父bean的集合属性上指定的merge属性将被忽略。

4.3.3.4.2. 强类型集合(仅适用于Java5+)

你若有幸在使用Java5(Tiger),那么你可以使用强类型集合(我自己推荐使用)。比如,声明一个只能包含String类型元素的Collection

假若使用Spring来给bean注入强类型的Collection,那就可以利用Spring的类型转换能,当向强类型Collection中添加元素前,这些元素将被转换。

用一个例子就可以更清楚的说明。考虑以下的类定义,及其相应的(XML)配置...。

public class Foo {
    
    private Map<String, Float> accounts;
    
    public void setAccounts(Map<String, Float> accounts) {
  this.accounts = accounts;
    }
  }
<beans>
    
    <bean id="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>

foobean的accounts属性被注入之前,通过反射,利用强类型Map<String, Float>的泛型信息,Spring的底层类型转换机制将会把各种value元素值转换为Float类型,因此字符串9.99、2.753.99就会被转换为实际的Float类型。

4.3.3.5. Nulls

<null/>用于处理null值。Spring会把属性的空参数当作空字符串处理。以下的xml片断将email属性设为空字符串

<bean>
  <property name="email"><value></value></property>
</bean>

这等同于Java代码: exampleBean.setEmail("")。 而null值则可以使用<null>元素可用来表示。例如:

<bean>
  <property name="email"><null/></property>
</bean>

上述的配置等同于Java代码:exampleBean.setEmail(null)

4.3.3.6. XML-based configuration metadata shortcuts

针对常见的value值或bean的引用,Spring提供了简化格式用于替代<value/><ref/>元素。<property/><constructor-arg/><entry/>元素都支持value属性(attribute),它可以用来替代内嵌的<value/>元素。因而,以下的代码:

<property name="myProperty">
  <value>hello</value>
</property>
<constructor-arg>
  <value>hello</value>
</constructor-arg>
<entry key="myKey">
  <value>hello</value>
</entry>

等同于:

<property name="myProperty" value="hello"/>
<constructor-arg value="hello"/>
<entry key="myKey" value="hello"/>

通常情况下,当手工编写配置文件时,你可能会偏向于使用简写形式(Spring的开发团队就是这么做的)。

<property/><constructor-arg/>支持类似的简写属性ref,它可能用来替代整个内嵌的<ref/>元素。因而,以下的代码:

<property name="myProperty">
  <ref bean="myBean">
</property>
<constructor-arg>
  <ref bean="myBean">
</constructor-arg>

等同于:

<property name="myProperty" ref="myBean"/>
<constructor-arg ref="myBean"/>

注意,尽管存在等同于<ref bean="xxx"> 元素的简写形式,但并没有<ref local="xxx">的简写形式,为了对当前xml中bean的引用,你只能使用完整的形式。

最后,map中entry元素的简写形式为key/key-refvalue /value-ref属性,因而,以下的代码:

<entry>
  <key>
    <ref bean="myKeyBean" />
  </key>
  <ref bean="myValueBean" />
</entry>

等同于:

<entry key-ref="myKeyBean" value-ref="myValueBean"/>

再次强调,只有<ref bean="xxx">元素的简写形式,没有<ref local="xxx">的简写形式。

4.3.3.7. 组合属性名称

当设置bean的组合属性时,除了最后一个属性外,只要其他属性值不为null,组合或嵌套属性名是完全合法的。例如,下面bean的定义:

<bean id="foo">
  <property name="fred.bob.sammy" value="123" />
</bean>

foo bean有个fred属性,此属性有个 bob属性,而bob属性又有个sammy属性,最后把sammy属性设置为123。为了让此定义能工作, foofred属性及fredbob属性在bean被构造后都必须非空,否则将抛出NullPointerException异常。

4.3.4. 方法注入

在大部分情况下,容器中的bean都是singleton类型的。如果一个singleton bean要引用另外一个singleton bean,或者一个非singleton bean要引用另外一个非singleton bean时,通常情况下将一个bean定义为另一个bean的property值就可以了。不过对于具有不同生命周期的bean来说这样做就会有问题了,比如在调用一个singleton类型bean A的某个方法时,需要引用另一个非singleton(prototype)类型的bean B,对于bean A来说,容器只会创建一次,这样就没法在需要的时候每次让容器为bean A提供一个新的的bean B实例。

上述问题的一个解决办法就是放弃控制反转。通过实现BeanFactoryAware接口(见这里)让bean A能够感知bean 容器,并且在需要的时候通过使用getBean("B")方式(见这里)向容器请求一个新的bean B实例。不过该做法绝非上策,因为这样bean的代码将与Spring藕合在了一起。

于是,方法注入(method injection)Spring IoC容器这一高级功能闪亮登场了,该功能将以一种清晰的方式解决上述类似问题。

4.3.4.1. Lookup方法注入

Lookup方法注入利用了Spring IoC容器复写bean的抽象(或具体)方法的能力,从而返回指定名字的bean实例。尽管Lookup方法注入也适用于singleton bean,但是它一般用来得到一个非singleton bean实例(就象上面的那种情形)。Lookup方法注入的内部机制是Spring利用了CGLIB库在运行时生成二进制代码功能,通过动态创建Lookup方法bean的子类而达到复写Lookup方法的目的。

在客户类中将包含被注入的方法,此方法定义必须按以下形式进行定义:

protected abstract SingleShotHelper createSingleShotHelper();

如果方法不是抽象的,Spring会简单地覆盖已有的实现。在基于XML的配置元数据文件中,通过在bean定义中使用lookup-method元素来告诉Spring所要注入/覆盖的方法将要返回的实际bean。例如:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="singleShotHelper" singleton="false"/>
  <!-- myBean uses singleShotHelper -->
<bean id="myBean">
  <lookup-method name="createSingleShotHelper" bean="singleShotHelper"/>
  <property>
    ...
  </property>
</bean>

在上面的例子中,标识为myBean的bean在需要一个新的singleShotHelperbean实例时,会调用createSingleShotHelper方法。一般情况下,我们会将singleShotHelper指定为非singleton。当然也可以指定为singleton,如果是这样的话,那么每次将返回相同的singleShotHelper实例!

最后需要提到的一点就是,Lookup方法注入既可以采用构造器注入的方式(通过可选的构造器参数来指定要构造的bean),也可以采用setter方法注入的方式(通过set属性指定要构造的bean)。

4.3.4.2. 自定义方法的替代方案

比起Lookup 方法注入来,还有一种很少用到的方法注入形式,该注入能使用bean的另一个方法实现去替换自定义的方法。除非你真的需要该功能,否则可以略过本节。

当使用基于XML配置元数据文件时,可以在bean定义中使用replaced-method元素来达到用另一个方法来取代已有方法的目的。考虑下面的类,我们将覆盖computeValue方法:

public class MyValueCalculator {
    public String computeValue(String input) {
    // some real code...
  }
    // some other methods...
  }

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

/** meant to be used to override the existing computeValue
    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定义中指定了将要复写的方法以及执行替换处理的bean定义:

<bean id="myValueCalculator>
  <!-- arbitrary method replacement -->
  <replaced-method name="computeValue" replacer="replacementComputeValue">
    <arg-type>String</arg-type>
  </replaced-method>
</bean>
  <bean id="replacementComputeValue"/>

<replaced-method/>元素内可包含一个或多个<arg-type/>元素,这些元素用来标明被复写的方法签名。只有被复写(override)的方法存在重载(overload)的情况(同名的多个方法变体)才会使用方法签名。为了方便,参数的类型字符串可以采用全限定类名的简写。例如,下面的字符串都表示参数类型为java.lang.String

    java.lang.String
    String
    Str

因为参数个数的不同可以很容易将重载的方法区分开来,所以只要使用的参数类型字符串能匹配一个参数,那么可以采用尽可能少的字符串来减少输入。

4.3.5. 使用depends-on

多数情况下,一个bean对另一个bean的依赖最简单的做法就是将一个bean设置为另外一个bean的属性。在xml配置文件中最常见的就是使用<ref/>元素。有时候它还有另外一种变体,如果一个bean能感知IoC容器,只要给出它所依赖的id,那么就可以通过编程的方式从容器中取得它所依赖的对象。无论采用哪一种方法,被依赖bean将在依赖bean之前被适当的初始化。

在少数情况下,有时候bean之间的依赖关系并不是那么的直接(例如,当类中的静态块的初始化被时,如数据库驱动的注册)。depends-on属性可以用于当前bean初始化之前显式地强制一个或多个bean被初始化。下面的例子中使用了depends-on属性来指定一个bean的依赖。

<bean id="beanOne" depends-on="manager">
  <property name="manager" ref="manager" />
</bean>
  <bean id="manager" />

若需要表达对多个bean的依赖,可以在<depends-on/>中将指定的多个bean名字用分隔符进行分隔,分隔符可以是逗号、空格及分号等。下面的例子中使用了depends-on来表达对多个bean的依赖。

<bean id="beanOne" depends-on="manager,accountDao">
  <property name="manager" ref="manager" />
</bean>
  <bean id="manager" />
<bean id="accountDao" />

4.3.6. 延迟初始化bean

ApplicationContext实现的默认行为就是在启动时将所有singleton bean提前进行实例化。提前实例化意味着作为初始化过程的一部分,ApplicationContext实例会创建并配置所有的singleton bean。通常情况下这是件好事,因为这样在配置中的任何错误就会即刻被发现(否则的话可能要花几个小时甚至几天)。

有时候这种默认处理可能并不是你想要的。如果你不想让一个singleton bean在ApplicationContext实现在初始化时被提前实例化,那么可以将bean设置为延迟实例化。一个延迟初始化bean将告诉IoC 容器是在启动时还是在第一次被用到时实例化。

在xml配置文件中,延迟初始化将通过<bean/>元素中的lazy-init属性来进行控制。

<bean id="lazy" lazy-init="true">
    <!-- various properties here... -->
</bean>
  <bean name="not.lazy">
    <!-- various properties here... -->
</bean>

ApplicationContext实现加载上述配置时,设置为lazy的bean将不会ApplicationContext启动时提前被实例化,而not.lazy却会被提前实例化。

需要说明的是,如果一个bean被设置为延迟初始化,而另一个非延迟初始化的singleton bean依赖于它,那么当ApplicationContext提前实例化singleton bean时,它必须也确保所有上述singleton 依赖bean也被预先初始化,当然也包括设置为延迟实例化的bean。因此,如果Ioc容器在启动的时候创建了那些设置为延迟实例化的bean的实例,你也不要觉得奇怪,因为那些延迟初始化的bean可能在配置的某个地方被注入到了一个非延迟初始化singleton bean里面。

在容器层次中通过在<beans/>元素上使用'default-lazy-init'属性来控制延迟初始化也是可能的。如下面的配置:

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

4.3.7. 自动装配(autowire)协作者

Spring IoC容器可以自动装配(autowire)相互协作bean之间的关联关系。因此,如果可能的话,可以自动让Spring通过检查BeanFactory中的内容,来替我们指定bean的协作者(其他被依赖的bean)。由于autowire可以针对单个bean进行设置,因此可以让有些bean使用autowire,有些bean不采用。autowire的方便之处在减少或者消除属性或构造器参数的设置,这样可以给我们的配置文件减减肥![2] 在xml配置文件中,autowire一共有五种类型,可以在<bean/>元素中使用autowire属性指定:

表 4.2. Autowiring modes

模式说明
no

不使用自动装配。必须通过ref元素指定依赖,这是默认设置。由于显式指定协作者可以使配置更灵活、更清晰,因此对于较大的部署配置,推荐采用该设置。而且在某种程度上,它也是系统架构的一种文档形式。

byName

根据属性名自动装配。此选项将检查容器并根据名字查找与属性完全一致的bean,并将其与属性自动装配。例如,在bean定义中将autowire设置为by name,而该bean包含master属性(同时提供setMaster(..)方法),Spring就会查找名为master的bean定义,并用它来装配给master属性。

byType

如果容器中存在一个与指定属性类型相同的bean,那么将与该属性自动装配。如果存在多个该类型的bean,那么将会抛出异常,并指出不能使用byType方式进行自动装配。若没有找到相匹配的bean,则什么事都不发生,属性也不会被设置。如果你不希望这样,那么可以通过设置dependency-check="objects"让Spring抛出异常。

constructor

byType的方式类似,不同之处在于它应用于构造器参数。如果在容器中没有找到与构造器参数类型一致的bean,那么将会抛出异常。

autodetect

通过bean类的自省机制(introspection)来决定是使用constructor还是byType方式进行自动装配。如果发现默认的构造器,那么将使用byType方式。


如果直接使用propertyconstructor-arg注入依赖的话,那么将总是覆盖自动装配。而且目前也不支持简单类型的自动装配,这里所说的简单类型包括基本类型、StringClass以及简单类型的数组(这一点已经被设计,将考虑作为一个功能提供)。自动装配还可以与依赖检查结合使用,这样依赖检查将在自动装配完成之后被执行。

理解自动装配的优缺点是很重要的。其中优点包括:

  • 自动装配能显着减少配置的数量。不过,采用bean模板 (见这里)也可以达到同样的目的。

  • 自动装配可以使配置与java代码同步更新。例如,如果你需要给一个java类增加一个依赖,那么该依赖将被自动实现而不需要修改配置。因此强烈推荐在开发过程中采用自动装配,而在系统趋于稳定的时候改为显式装配的方式。

自动装配的一些缺点:

  • 尽管自动装配比显式装配更神奇,但是,正如上面所提到的,Spring会尽量避免在装配不明确的时候进行猜测,因为装配不明确可能出现难以预料的结果,而且Spring所管理的对象之间的关联关系也不再能清晰的进行文档化。

  • 对于那些根据Spring配置文件生成文档的工具来说,自动装配将会使这些工具没法生成依赖信息。

  • 如果采用by type方式自动装配,那么容器中类型与自动装配bean的属性或者构造函数参数类型一致的bean只能有一个,如果配置可能存在多个这样的bean,那么就要考虑采用显式装配了。

尽管使用autowire没有对错之分,但是能在一个项目中保持一定程度的一致性是最好的做法。例如,通常情况下如果没有使用自动装配,那么仅自动装配一个或两个bean定义可能会引起开发者的混淆。

4.3.7.1. 设置Bean使自动装配失效

你也可以针对单个bean设置其是否为被自动装配对象。当采用XML格式配置bean时,<bean/>元素的 autowire-candidate属性可被设为false,这样容器在查找自动装配对象时将不考虑该bean。

对于那些从来就不会被其它bean采用自动装配的方式来注入的bean而言,这是有用的。不过这并不意味着被排除的bean自己就不能使用自动装配来注入其他bean,它是可以的,或者更准确地说,应该是它不会被考虑作为其他bean自动装配的候选者。

4.3.8. 依赖检查

Spring除了能对容器中bean的依赖设置进行检查外。还可以检查bean定义中实际属性值的设置,当然也包括采用自动装配方式设置属性值的检查。

当需要确保bean的所有属性值(或者属性类型)被正确设置的时候,那么这个功能会非常有用。当然,在很多情况下,bean类的某些属性会具有默认值,或者有些属性并不会在所有场景下使用,因此这项功能会存在一定的局限性。就像自动装配一样,依赖检查也可以针对每一个bean进行设置。依赖检查默认为not, 它有几种不同的使用模式,在xml配置文件中,可以在bean定义中为dependency-check属性使用以下几种值:

当需要确保bean的所有属性值(或者属性类型)被正确设置的时候,那么这个功能会非常有用。当然,在很多情况下,bean类会有一些具有默认值的属性,或者有些属性并不会在所有场景下使用,因此这项功能会存在一定的局限性。就像自动装配一样,依赖检查也可以针对每一个bean进行设置。依赖检查默认为not, 它有几种不同的使用模式,在xml配置文件中,可以在bean定义中为dependency-check属性使用以下几种值:

表 4.3. 依赖检查方式

模式说明
none

没有依赖检查,如果bean的属性没有值的话可以不用设置。

simple

对于原始类型及集合(除协作者外的一切东西)执行依赖检查

object

仅对协作者执行依赖检查

all

对协作者,原始类型及集合执行依赖检查


假若你在使用Java 5(Tiger),可以采用源代码级的注解(annotations)来进行配置,关于这方面的内容可以在第 26.3.1 节 “@Required”这一节找到。



[2] 参见第 4.3.1 节 “注入依赖”