2.4.3 借助构造器注入初始化 bean

在 Spring XML 配置中,只有一种声明 bean 的方式:使用 <bean> 元素并指定 class 属性。Spring 会从这里获取必要的信息来创建 bean。

但是,在 XML 中声明 DI 时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:

  • <constructor-arg> 元素

  • 使用 Spring 3.0 所引入的 c- 命名空间

两者的区别在很大程度就是是否冗长烦琐。可以看到,<constructor-arg> 元素比使用 c- 命名空间会更加冗长,从而导致 XML 更加难以读懂。另外,有些事情可以做到,但是使用 c- 命名空间却无法实现。

在介绍 Spring XML 的构造器注入时,我们将会分别介绍这两种可选方案。首先,看一下它们各自如何注入 bean 引用。

构造器注入 bean 引用

按照现在的定义,CDPlayer bean 有一个接受 CompactDisc 类型的构造器。这样,我们就有了一个很好的场景来学习如何注入 bean 的引用。

现在已经声明了 SgtPeppers bean,并且 SgtPeppers 类实现了 CompactDisc 接口,所以实际上我们已经有了一个可以注入到 CDPlayer bean 中的 bean。我们所需要做的就是在 XML 中声明 CDPlayer 并通过 ID 引用 SgtPeppers:

<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <constructor-arg ref="compactDisc">
</bean>

当 Spring 遇到这个 <bean> 元素时,它会创建一个 CDPlayer 实例。<constructor-arg> 元素会告知 Spring 要将一个 ID 为 compactDisc 的 bean 引用传递到 CDPlayer 的构造器中。

作为替代的方案,你也可以使用 Spring 的 c- 命名空间。c- 命名空间是在 Spring 3.0 中引入的,它是在 XML 中更为简洁地描述构造器参数的方式。要使用它的话,必须要在 XML 的顶部声明其模式,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:c="http://www.springframework.org/schema/c"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans.xsd" >
  
  ...
  
</beans>

c- 命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示:

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />

在这里,我们使用了 c- 命名空间来声明构造器参数,它作为元素的一个属性,不过这个属性的名字有点诡异。图 2.1 描述了这个属性名是如何组合而成的。

属性名以 c: 开头,也就是命名空间的前缀。接下来就是要装配的构造器参数名,在此之后是 -ref,这是一个命名的约定,它会告诉 Spring,正在装配的是一个 bean 的引用,这个 bean 的名字是 compactDisc,而不是字面量 “compactDisc”。

很显然,使用 c- 命名空间属性要比使用元素简练得多。这是我很喜欢它的原因之一。除了更易读之外,当我在编写样例代码时,c- 命名空间属性能够更加有助于使代码的长度保持在书的边框之内。

在编写前面的样例时,关于 c- 命名空间,有一件让我感到困扰的事情就是它直接引用了构造器参数的名称。引用参数的名称看起来有些怪异,因为这需要在编译代码的时候,将调试标志(debug symbol)保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。

替代的方案是我们使用参数在整个参数列表中的位置信息:

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />

这个 c- 命名空间属性看起来似乎比上一种方法更加怪异。我将参数的名称替换成了 0,也就是参数的索引。因为在 XML 中不允许数字作 为属性的第一个字符,因此必须要添加一个下画线作为前缀。

使用索引来识别构造器参数感觉比使用名字更好一些。即便在构建的时候移除掉了调试标志,参数却会依然保持相同的顺序。如果有多个构造器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还有另外一个方案 —— 根本不用去标示参数:

<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_-ref="compactDisc" />

到目前为止,这是最为奇特的一个 c- 命名空间属性,这里没有参数索引或参数名。只有一个下画线,然后就是用 -ref 来表明正在装配的是一个引用。

我们已经将引用装配到了其他的 bean 之中,接下来看一下如何将字面量值(literal value)装配到构造器之中。

将字面量注入到构造器中

迄今为止,我们所做的 DI 通常指的都是类型的装配 —— 也就是将对象的引用装配到依赖于它们的其他对象之中 —— 而有时候,我们需要做的只是用一个字面量值来配置对象。为了阐述这一点,假设你要创建 CompactDisc 的一个新实现,如下所示:

BlankDisc.java
package soundsystem;

import java.util.List;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;

  public BlankDisc(String title, String artist) {
    this.title = title;
    this.artist = artist;
  }

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }

}

在 SgtPeppers 中,唱片名称和艺术家的名字都是硬编码的,但是这个 CompactDisc 实现与之不同,它更加灵活。像现实中的空磁盘一样,它可以设置成任意你想要的艺术家和唱片名。现在,我们可以将 已有的 SgtPeppers 替换为这个类:

<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
</bean>

我们再次使用 <constructor-arg> 元素进行构造器参数的注入。但是这一次我们没有使用 ref 属性来引用其他的 bean,而是使用了 value 属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中。

如果要使用 c- 命名空间的话,这个例子又该是什么样子呢?第一种方案是引用构造器参数的名字:

<bean id="compactDisc" class="soundsystem.BlankDisc"
      c:_title="Sgt. Pepper's Lonely Hearts Club Band" 
      c:_artist="The Beatles" />

可以看到,装配字面量与装配引用的区别在于属性名中去掉了 -ref 后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所示:

<bean id="compactDisc" class="soundsystem.BlankDisc"
      c:_0="Sgt. Pepper's Lonely Hearts Club Band" 
      c:_1="The Beatles" />

XML 不允许某个元素的多个属性具有相同的名字。因此,如果有两个或更多的构造器参数的话,我们不能简单地使用下画线进行标示。但是如果只有一个构造器参数的话,我们就可以这样做了。为了完整地展现该功能,假设 BlankDisc 只有一个构造器参数,这个参数接受唱片的名称。在这种情况下,我们可以在 Spring 中这样声明它:

<bean id="compactDisc" class="soundsystem.BlankDisc"
      c:_="Sgt. Pepper's Lonely Hearts Club Band" />

在装配 bean 引用和字面量值方面,<constructor-arg>c- 命名空间的功能是相同的。但是有一种情况是 <constructor-arg> 能够实现,c- 命名空间却无法做到的。接下来,让我们看一下如何将集合装配到构造器参数中。

装配集合

到现在为止,我们假设 CompactDisc 在定义时只包含了唱片名称和 艺术家的名字。如果现实世界中的 CD 也是这样的话,那么在技术上就不会任何的进展。CD 之所以值得购买是因为它上面所承载的音乐。大多数的 CD 都会包含十多个磁道,每个磁道上包含一首歌。

如果使用 CompactDisc 为真正的 CD 建模,那么它也应该有磁道列表的概念。请考虑下面这个新的 BlankDisc:

BlankDisc.java
package soundsystem;

import java.util.List;

public class BlankDisc implements CompactDisc {

  private String title;
  private String artist;
  private List<String> tracks;

  public BlankDisc(String title, String artist, List<String> tracks) {
    this.title = title;
    this.artist = artist;
    this.tracks = tracks;
  }

  public void play() {
    System.out.println("Playing " + title + " by " + artist);
    for (String track : tracks) {
      System.out.println("-Track: " + track);
    }
  }

}

这个变更会对 Spring 如何配置 bean 产生影响,在声明 bean 的时候,我们必须要提供一个磁道列表。

最简单的办法是将列表设置为 null。因为它是一个构造器参数,所以必须要声明它,不过你可以采用如下的方式传递 null 给它:

<bean id="compactDisc" class="soundsystem.BlankDisc">
    <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
    <constructor-arg value="The Beatles" />
    <constructor-arg><null/></constructor-arg>
</bean>

<null/> 元素所做的事情与你的期望是一样的:将 null 传递给构造器。这并不是解决问题的好办法,但在注入期它能正常执行。当调用 play() 方法时,你会遇到 NullPointerException 异常,因此这并不是理想的方案。

更好的解决方法是提供一个磁道名称的列表。要达到这一点,我们可以有多个可选方案。首先,可以使用元素将其声明为一个列 表:

<?xml version="1.0" encoding="UTF-8"?>
<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="compactDisc"
        class="soundsystem.BlankDisc"
        c:_0="Sgt. Pepper's Lonely Hearts Club Band"
        c:_1="The Beatles">
    <constructor-arg>
      <list>
        <value>Sgt. Pepper's Lonely Hearts Club Band</value>
        <value>With a Little Help from My Friends</value>
        <value>Lucy in the Sky with Diamonds</value>
        <value>Getting Better</value>
        <value>Fixing a Hole</value>
        <!-- ...other tracks omitted for brevity... -->
      </list>
    </constructor-arg>
  </bean>

</beans>

其中,<list> 元素是 <constructor-arg> 的子元素,这表明一个包含值的列表将会传递到构造器中。其中,<value> 元素用来指定列表中的每个元素。

与之类似,我们也可以使用 <ref> 元素替代 <value>,实现 bean 引用列表的装配。例如,假设你有一个 Discography 类,它的构造器如 下所示:

Discography.java
public Discography(String artist, List<CompactDisc> cds) { ... }

那么,你可以采取如下的方式配置 Discography bean:

<bean id="beatlesDiscography"
        class="soundsystem.Discography" >
  <constructor-arg>
    <list>
      <ref bean="sgtPeppers" />
      <ref bean="whiteAlbum" />
      <ref bean="hardDaysNight" />
      <ref bean="revolver" />
      ...
    </list>
  </constructor-arg>
</bean>

当构造器参数的类型是 java.util.List 时,使用 <list> 元素是合情合理的。尽管如此,我们也可以按照同样的方式使用 <set> 元素:

<bean id="compactDisc" class="soundsystem.BlankDisc" >
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="The Beatles" />
  <constructor-arg>
    <set>
      <value>Sgt. Pepper's Lonely Hearts Club Band</value>
      <value>With a Little Help from My Friends</value>
      <value>Lucy in the Sky with Diamonds</value>
      <value>Getting Better</value>
      <value>Fixing a Hole</value>
      <!-- ...other tracks omitted for brevity... -->
    </set>
  </constructor-arg>
</bean>

<set><list> 元素的区别不大,其中最重要的不同在于当 Spring 创建要装配的集合时,所创建的是 java.util.Set 还是 java.util.List。如果是 Set 的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。不过无论在哪种情况下,<set><list> 都可以用来装配 List、Set 甚至数组。

在装配集合方面,<constructor-arg>c- 命名空间的属性更有优势。目前,使用 c- 命名空间的属性无法实现装配集合的功能。

使用 <constructor-arg>c- 命名空间实现构造器注入时,它们之间还有一些细微的差别。但是到目前为止,我们所涵盖的内容已经足够了,尤其是像我之前所建议的那样,要首选基于 Java 的配置而不是 XML。因此,与其不厌其烦地花费时间讲述如何使用 XML 进行构造器注入,还不如看一下如何使用 XML 来装配属性。

Last updated