# 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：

```markup
<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 的顶部声明其模式，如下所示：

```markup
<?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-` 命名空间和模式声明之后，我们就可以使用它来声明构造器参数了，如下所示：

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

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

![图 2.1　通过 Spring 的 c- 命名空间将 bean 引用注入到构造器参数中](https://3784153818-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LmcjU5gG__lBrRbUxBO%2F-LmhA6eKm0-X-AEMkFR0%2F-Lmh5cX_0Zp5qUf05kPj%2Fc%20%E6%A0%87%E7%AD%BE%E7%BB%84%E6%88%90.jpg?alt=media\&token=aa6809ce-ff81-49d7-a00e-8110270d3c78)

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

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

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

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

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

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

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

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

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

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

**将字面量注入到构造器中**

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

{% code title="BlankDisc.java" %}

```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);
  }

}
```

{% endcode %}

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

```markup
<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-` 命名空间的话，这个例子又该是什么样子呢？第一种方案是引用构造器参数的名字：

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

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

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

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

```markup
<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：

{% code title="BlankDisc.java" %}

```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);
    }
  }

}
```

{% endcode %}

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

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

```markup
<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 异常，因此这并不是理想的方案。

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

```markup
<?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 类，它的构造器如 下所示：

{% code title="Discography.java" %}

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

{% endcode %}

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

```markup
<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>` 元素：

```markup
<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 来装配属性。
