spring实战 装配bean

178 阅读24分钟

装配bean

spring配置的可选方案

bean装配的三种机制

  • 在XML中进行显式配置

  • 在java中进行显式配置

  • 隐式的bean发现机制和自动装配

    尽可能使用自动配置的机制,显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使用XML。

自动化装配bean

两个角度实现自动化装配

  • 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean

  • 自动装配(autowiring):Spring自动满足bean之间的依赖

创建可被发现的bean

尽管如此,CD为我们阐述DI如何运行提供了一个很好的样例。如果你不将CD插入(注入)到CD播放器中,那么CD播放器其实是没有太大用 处的。所以,可以这样说,CD播放器依赖于CD才能完成它的使命。 为了在Spring中阐述这个例子,让我们首先在Java中建立CD的概念。程序清单2.1展现了CompactDisc,它是定义CD的一个接口:

package soundsystem;

public interface CompactDisc {
  void play();
}

CompactDisc的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了CD播放器对一盘CD所能进行的操作。它将CD 播放器的任意实现与CD本身的耦合降低到了最小的程度。 我们还需要一个CompactDisc的实现,实际上,我们可以有CompactDisc接口的多个实现。在本例中,我们首先会创建其中的一个实现, 也就是程序清单2.2所示的SgtPeppers类。

package soundsystem;
import org.springframework.stereotype.Component;

@Component
public class SgtPeppers implements CompactDisc {

  private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
  private String artist = "The Beatles";
  
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
  
}

和CompactDisc接口一样,SgtPeppers的具体内容并不重要。你需要注意的就是SgtPeppers类上使用了@Component注解。这个简单 的注解表明该类会作为组件类,并告知Spring要为这个类创建bean。没有必要显式配置SgtPeppersbean,因为这个类使用 了@Component注解,所以Spring会为你把事情处理妥当。 不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。程序清 单2.3的配置类展现了完成这项任务的最简洁配置。

package soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class CDPlayerConfig { 
}

类CDPlayerConfig通过Java代码定义了Spring的装配规则。在2.3节中,我们还会更为详细地介绍基于Java的Spring配置。不过,现在我们 只需观察一下CDPlayerConfig类并没有显式地声明任何bean,只不过它使用了@ComponentScan注解,这个注解能够在Spring中启用组 件扫描。 如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包。因为CDPlayerConfig类位于soundsystem包中,因此Spring 将会扫描这个包以及这个包下的所有子包,查找带有@Component注解的类。这样的话,就能发现CompactDisc,并且会在Spring中自动为 其创建一个bean。 如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context命名空间的元素。程序清 单2.4展示了启用组件扫描的最简洁XML配置。

<?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:context="http://www.springframework.org/schema/context"
  xmlns:c="http://www.springframework.org/schema/c"
  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
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  <context:component-scan base-package="soundsystem" />

</beans>

尽管我们可以通过XML的方案来启用组件扫描,但是在后面的讨论中,我更多的还是会使用基于Java的配置。如果你更喜欢XML的 话,元素会有与@ComponentScan注解相对应的属性和子元素。 可能有点让人难以置信,我们只创建了两个类,就能对功能进行一番尝试了。为了测试组件扫描的功能,我们创建一个简单的JUnit测试,它会 创建Spring上下文,并判断CompactDisc是不是真的创建出来了。程序清单2.5中的CDPlayerTest就是用来完成这项任务的。

package soundsystem;

import static org.junit.Assert.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {

  @Autowired
  private CompactDisc cd;
  
  @Test
  public void cdShouldNotBeNull() {
    assertNotNull(cd);
  }
}

CDPlayerTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring的应用上下文。

注解@ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置。因为CDPlayerConfig类中包含了@ComponentScan, 因此最终的应用上下文中应该包含CompactDiscbean。

为了证明这一点,在测试代码中有一个CompactDisc类型的属性,并且这个属性带有@Autowired注解,以便于将CompactDiscbean注入 到测试代码之中(稍后,我会讨论@Autowired)。最后,会有一个简单的测试方法断言cd属性不为null。如果它不为null的话,就意味着 Spring能够发现CompactDisc类,自动在Spring上下文中将其创建为bean并将其注入到测试代码之中。

这个代码应该能够通过测试,并以测试成功的颜色显示(在你的测试运行器中,或许会希望出现绿色)。你第一个简单的组件扫描练习就成功 了!尽管我们只用它创建了一个bean,但同样是这么少的配置能够用来发现和创建任意数量的bean。在soundsystem包及其子包中,所有带 有@Component注解的类都会创建为bean。只添加一行@ComponentScan注解就能自动创建无数个bean,这种权衡还是很划算的。

为组件扫描的bean命名

前面的例子中没有给SgtPeppersbean设置ID,但spring会根据类名为其指定一个id。

如果想为这个bean设置不同的id,需要将期望的id传递给@Component注解。

package soundsystem;
import org.springframework.stereotype.Component;

@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {

  private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
  private String artist = "The Beatles";
  
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
}

还有另外一种为bean命名的方式,这种方式不使用@Component注解,而是使用Java依赖注入规范(Java Dependency Injection)中所提供 的@Named注解来为bean设置ID:

package soundsystem;
import org.springframework.stereotype.Component;

@Named("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {

  private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
  private String artist = "The Beatles";
  
  public void play() {
    System.out.println("Playing " + title + " by " + artist);
  }
}

Spring支持将@Named作为@Component注解的替代方案。两者之间有一些细微的差异,但是在大多数场景中,它们是可以互相替换的。

设置组件扫描的基础包

我们没有为@ComponentScan设置任何属性。这意味着,按照默认规则,它会以配置类所在的包作为基础包(base package) 来扫描组件。但是,如果你想扫描不同的包,你所需要做的就是在@ComponentScan的value属性中指明包的名称:

@Configuration
@ComponentScan("soundsystem")
public class CDPlayerConfig { 
}

更加清晰地表明你所设置的是基础包,那么你可以通过basePackages属性进行配置

@Configuration
@ComponentScan(basePackages = "soundsystem")
public class CDPlayerConfig { 
}

basePackages属性使用的是复数形式,可以设置多个基础包,只需要将basePackages属性设置为要扫描包的一个数组即可

@Configuration
@ComponentScan(basePackages = {"soundsystem", "video"})
public class CDPlayerConfig { 
}

在上面的例子中,所设置的基础包是以String类型表示的。我认为这是可以的,但这种方法是类型不安全(not type-safe)的。如果重构代码的话,那么所指定的基础包可能就会出现错误了。

除了将包设置为简单的String类型之外,@ComponentScan还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:

@Configuration
@ComponentScan(basePackageClasses ={CDPlayer.class,DVDPlayer.class} )
public class CDPlayerConfig { 
}

可以看到,basePackages属性被替换成了basePackageClasses。同时,我们不是再使用String类型的名称来指定包, 为basePackageClasses属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。

通过bean添加注解实现自动装配

自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其 他bean。

CDPlayer类的构造器上添加了@Autowired注解,这表明当Spring创建CDPlayerbean的时候,会通过 这个构造器来进行实例化并且会传入一个可设置给CompactDisc类型的bean。

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;

  @Autowired
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }

  public void play() {
    cd.play();
  }

}

@Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上。比如说,如果CDPlayer有一个setCompactDisc()方法,那么可 以采用如下的注解形式进行自动装配:

@Autowired
public void setCompactDisc(CompactDisc cd){
    this.cd = cd;
}

在Spring初始化bean之后,它会尽可能得去满足bean的依赖,在本例中,依赖是通过带有@Autowired注解的方法进行声明的,也就 是setCompactDisc()。

实际上,Setter方法并没有什么特殊之处。@Autowired注解可以用在类的任何方法上。假设CDPlayer类有一个insertDisc()方法,那 么@Autowired能够像在setCompactDisc()上那样,发挥完全相同的作用:

@Autowired
public void insertDisc(CompactDisc cd){
    this.cd = cd;
}

不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么 这个bean将会被装配进来。 如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,你可以将@Autowired的required属 性设置为false:

@Autowired(required=false)
public void insertDisc(CompactDisc cd){
    this.cd = cd;
}

将required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring将会让这个bean处于未装配的状态。 但是,把required属性设置为false时,你需要谨慎对待。如果在你的代码中没有进行null检查的话,这个处于未装配状态的属性有可能会 出现NullPointerException。

如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配.

@Autowired是Spring特有的注解。如果你不愿意在代码中到处使用Spring的特定注解来完成自动装配任务的话,那么你可以考虑将其替换 为@Inject:

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Named
public class CDPlayer implements MediaPlayer {
  private CompactDisc cd;

  @Inject
  public CDPlayer(CompactDisc cd) {
    this.cd = cd;
  }
}

@Inject注解来源于Java依赖注入规范,该规范同时还为我们定义了@Named注解。在自动装配中,Spring同时支 持@Inject和@Autowired。尽管@Inject和@Autowired之间有着一些细微的差别,但是在大多数场景下,它们都是可以互相替换的。

验证自动装配

在CDPlayer的构造器中添加了@Autowired注解,Spring将把一个可分配给CompactDisc类型的bean自动注入进来。为了 验证这一点,让我们修改一下CDPlayerTest,使其能够借助CDPlayer bean播放CD:

package soundsystem;

import static org.junit.Assert.*;

import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.StandardOutputStreamLog;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {

  @Rule
  public final StandardOutputStreamLog log = new StandardOutputStreamLog();

  @Autowired
  private MediaPlayer player;
  
  @Autowired
  private CompactDisc cd;
  
  @Test
  public void cdShouldNotBeNull() {
    assertNotNull(cd);
  }

  @Test
  public void play() {
    player.play();
    assertEquals(
        "Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles\n", 
        log.getLog());
  }

}

除了注入CompactDisc,我们还将CDPlayerbean注入到测试代码的player成员变量之中(它是更为通用的MediaPlayer类 型)。在play()测试方法中,我们可以调用CDPlayer的play()方法,并断言它的行为与你的预期一致。

通过java代码装配bean

创建配置类

创建JavaConfig类的关键在于为其添加@Configuration注解,此注解表示这个类是一个配置类

声明简单的bean

在JavaConfig中声明bean,编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解

@Bean
public CompactDisc sgtPeppers(){
    return new SgtPeppers();
}

@Bean注解会告诉spring这个方法将会返回一个对象,该对象要注册为spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。

默认情况下,bean的id与带有@Bean注解的方法名是一样的,也可重命名该方法,也可以通过name属性指定一个不同的名字

@Bean(name="lonelyHeartsCluBand")
public CompactDisc sgtPeppers(){
    return new SgtPeppers();
}

方法体返回了一个新的SgtPeppers实例,这里是使用java来进行描述的,因此可以发挥java提供的所有功能,只要最终生成一个CompactDisc实例即可

@Bean
public CompactDisc randomBeatlesCD(){
    int choice = (int)Math.floor(Math.random()*4);
    if(choice == 0){
        return new SgtPeppers();
    }else if(choice == 1){
        return new WhiteAlbum();
    }else if(choice == 2){
        return new HardDaysNight();
    }else{
        return new Revolver();
    }
}

借助JavaConfig实现注入

生命CDPlayer bean,它依赖于CompactDisc

@Bean
public CDPlayer cdPlayer(){
    return new CDPlayer(sgtPeppers());
}

CompactDisc是通过调用sgtPeppers()得到的,因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对他的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。

比如说,假设引入了一个其他的CDPlayer bean,他和之前的那个bean完全一样:

@Bean
public CDPlayer cdPlayer(){
    return new CDPlayer(sgtPeppers());
}
@Bean
public CDPlayer anotherCDPlayer(){
    return new CDPlayer(sgtPeppers());
}

如果对sgtPeppers()的调用就像其他的Java方法调用一样的话,那么每个CDPlayer实例都会有一个自己特有的SgtPeppers实例。

默认情况下,Spring中的bean都是单例的,没有必要为第二个CDPlayer bean创建完全相同的SgtPeppers实例。所以,Spring会拦截对sgtPeppers()的调用并确保返回的是Spring所创建的bean,也就是Spring本身在调用sgtPeppers()时所创建的CompactDisc bean。因此,两个CDPlayer bean会得到相同的SgtPeppers实例。

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
    return new CDPlayer(compactDisc);
}

这里使用CDPlayer的构造器实现了DI功能,但是我们完全可以采用其他风格的DI配置。

@Bean
public CDPlayer cdPlayer(CompactDisc compactDisc){
    CDPlayer cdPlayer = new CDPlayer(compactDisc);
    cdPlayer.setCompactDisc(compactDisc);
    return cdPlayer;
}

带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。构造器和Setter只是@Bean方法的两个简单样例。

通过XML装配bean

创建XML配置规范

使用JavaConfig的时候,这意味要创建一个带有@Configuration,而在XML配置中,要创建一个XML文件,并且要以元素为根

<?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">

 <!-- configuration details go here -->

</beans>

使用xml时,需要在配置文件的顶部声明多个xml模式(XSD)文件,这些文件定义了配置Spring的XML元素。

借助Spring Tool Suite 配置文件创建和管理 File>New>Spring Bean Configuration File创建Spring XML配置文件,并且可以选择可用的配置命名空间。

装配bean的最基本的XML元素包含在spring-beans模式中,定义为根命名空间。是该模式中的一个元素,是所有Spring配置文件的根元素。

声明一个简单的

使用spring-beans模式的另一个元素,类似JavaConfig中的@Bean注解

<bean class="soundsystem.SgtPeppers" />

这个bean的类通过class属性来制定的,并且要用全限定的类名,bean的ID将是"soundsystem.SgtPeppers#0","#0"是一个计数形式,用来区分相同类型的其他bean

<bean id="compactDisc" class="soundsystem.SgtPeppers" />

虽然自动化命名很方便,但如果要引用他,自动产生的名字就没有多大的用处了。通常借助id属性来为bean设置一个自己选择的名字

借助构造器注入初始化bean

Spring XML配置中只有一种声明bean的方式,使用元素并指定class属性。但是,在XML中声明DI时,具体到构造器注入,有两种基本配置方案:

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

两者区别就是是否冗长繁琐。元素比使用c-命名空间会更加冗长,导致XML更加难以读懂。另外,有些事情可以做到,而c-命名空间无法实现

构造器注入bean引用

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

Spring遇到元素时,会创建CDPlayer实例。元素会告知Spring要将一个id为compactDisc的bean引用传递到CDPlayer构造器中。

要使用c-命名空间,必须在XML的顶部声明其模式

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

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

引用参数的名称看起来有些怪异, 因为这需要在编译代码的时候,将调试标志(debug symbol)保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能 就无法正常执行了。

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

将参数的名称替换成了“0”,也就是参数的索引。因为在XML中不允许数字作为属性 的第一个字符,因此必须要添加一个下画线作为前缀。

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

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

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

将字面量注入到构造器中

创建CompactDisc新实现

package soundsystem;

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

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

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

<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" />

装配集合

package soundsystem.collections;

import java.util.List;

import soundsystem.CompactDisc;

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

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

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

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

<bean id="compactDisc" class="soundsystem.collections.BlankDisc">
  <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
  <constructor-arg value="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>
      <value>She's Leaving Home</value>
      <value>Being for the Benefit of Mr. Kite!</value>
      <value>Within You Without You</value>
      <value>When I'm Sixty-Four</value>
      <value>Lovely Rita</value>
      <value>Good Morning Good Morning</value>
      <value>Sgt. Pepper's Lonely Hearts Club Band (Reprise)</value>
      <value>A Day in the Life</value>
    </list>
  </constructor-arg>
</bean>

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

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

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

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

在装配集合方面,比c-命名空间的属性更有优势。

设置属性

package soundsystem;
import org.springframework.beans.factory.annotation.Autowired;

import soundsystem.CompactDisc;
import soundsystem.MediaPlayer;

public class CDPlayer implements MediaPlayer {
  private CompactDisc compactDisc;

  @Autowired
  public void setCompactDisc(CompactDisc compactDisc) {
    this.compactDisc = compactDisc;
  }

  public void play() {
    compactDisc.play();
  }

}

该选择构造器注入还是属性注入呢?作为一个通用的规则,我倾向于对强依赖使用构造器注入,而对可选性的依赖使用属性注入。

我们可以说对于BlankDisc来讲,唱片名称、艺术家以及磁道列表是强依赖,因此构造器注入是正确的方案。不过,对于CDPlayer来 讲,它对CompactDisc是强依赖还是可选性依赖可能会有些争议。

CDPlayer没有任何的构造器(除了隐含的默认构造器),它也没有任何的强依赖。因此,你可以采用如下的方式将其声明为Spring bean:

<bean id="cdPlayer"
  class="soundsystem.CDPlayer"/>

Spring在创建bean的时候不会有任何的问题,但是CDPlayerTest会因为出现NullPointerException而导致测试失败,因为我们并没有 注入CDPlayer的compactDisc属性。

<bean id="cdPlayer" class="soundsystem.CDPlayer">
  <property name="compactDisc" ref="compactDisc" />
</bean>

元素为属性的Setter方法所提供的功能与元素为构造器所提供的功能是一样的。在本例中,它引用了ID 为compactDisc的bean(通过ref属性),并将其注入到compactDisc属性中(通过setCompactDisc()方法)

Spring提供了更加简洁的p-命名空间,作为元素的替代方案。为了启用p-命名空间,必须要在XML文件中与其他的命名空间一起对其进行声明:

<?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:p="http://www.springframework.org/schema/p"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>
<bean id="cdPlayer"
      class="soundsystem.properties.CDPlayer"
      p:compactDisc-ref="compactDisc" />

属性的名字使用了“p:”前缀,表明我们所设置的是一个属性。接下来就是要注入的属性名。最后,属性的名称以“-ref”结尾,这会提示 Spring要进行装配的是引用,而不是字面量。

将字面量注入到属性中

package soundsystem.properties;

import java.util.List;

import soundsystem.CompactDisc;

public class BlankDisc implements CompactDisc {

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

  public void setTitle(String title) {
    this.title = title;
  }

  public void setArtist(String artist) {
    this.artist = artist;
  }

  public void setTracks(List<String> tracks) {
    this.tracks = tracks;
  }

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

}
<bean id="compactDisc" class="soundsystem.BlankDisc" />

如果在装配bean的时候不设置这些属性,那么在运行期CD播放器将不能正常播放内容。play()方法可能会遇到的输出内容是“Playing null by null”,随之会抛出NullPointerException异常,这是因为我们没有指定任何的磁道。

<bean id="compactDisc"
      class="soundsystem.properties.BlankDisc">
  <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
  <property name="artist" value="The Beatles" />
  <property name="tracks">
    <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>
      <value>She's Leaving Home</value>
      <value>Being for the Benefit of Mr. Kite!</value>
      <value>Within You Without You</value>
      <value>When I'm Sixty-Four</value>
      <value>Lovely Rita</value>
      <value>Good Morning Good Morning</value>
      <value>Sgt. Pepper's Lonely Hearts Club Band (Reprise)</value>
      <value>A Day in the Life</value>
    </list>
  </property>
</bean>

除了使用元素的value属性来设置title和artist,我们还使用了内嵌的元素来设置tracks属性,这与之前通过装配tracks是完全一样的。

<bean id="compactDisc"
      class="soundsystem.properties.BlankDisc"
      p:title="Sgt. Pepper's Lonely Hearts Club Band"
      p:artist="The Beatles">
  <property name="tracks">
    <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>
      <value>She's Leaving Home</value>
      <value>Being for the Benefit of Mr. Kite!</value>
      <value>Within You Without You</value>
      <value>When I'm Sixty-Four</value>
      <value>Lovely Rita</value>
      <value>Good Morning Good Morning</value>
      <value>Sgt. Pepper's Lonely Hearts Club Band (Reprise)</value>
      <value>A Day in the Life</value>
    </list>
  </property>
</bean>

与c-命名空间一样,装配bean引用与装配字面量的唯一区别在于是否带有“-ref”后缀。如果没有“-ref”后缀的话,所装配的就是字面量。

但需要注意的是,我们不能使用p-命名空间来装配集合,没有便利的方式使用p-命名空间来指定一个值(或bean引用)的列表。但是,我们可 以使用Spring util-命名空间中的一些功能来简化BlankDiscbean。

需要在XML中声明util-命名空间及其模式:

<?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:p="http://www.springframework.org/schema/p"
  xmlns:util="http://www.springframework.org/schema/util"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/util 
    http://www.springframework.org/schema/util/spring-util.xsd">

</beans>

util-命名空间所提供的功能之一就是元素,它会创建一个列表的bean。借助,我们可以将磁道列表转移 到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示:

<bean id="compactDisc"
      class="soundsystem.properties.BlankDisc"
      p:title="Sgt. Pepper's Lonely Hearts Club Band"
      p:artist="The Beatles"
      p:tracks-ref="trackList" />

<util:list id="trackList">  
  <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>
  <value>She's Leaving Home</value>
  <value>Being for the Benefit of Mr. Kite!</value>
  <value>Within You Without You</value>
  <value>When I'm Sixty-Four</value>
  <value>Lovely Rita</value>
  <value>Good Morning Good Morning</value>
  <value>Sgt. Pepper's Lonely Hearts Club Band (Reprise)</value>
  <value>A Day in the Life</value>
</util:list>

< util:list >只是util-命名空间中的多个元素之一。

元素描述
util:constant引用某个类型的public static域,并将其暴露为bean
util:list创建一个java.util.List类型的bean,其中包含值或引用
util:map创建一个java.util.Map类型的bean,其中包含值或引用
util:properties创建一个java.util.Properties类型的bean
util:property-path引用一个bean的属性(或内嵌属性),并将其暴露为bean
util:set创建一个java.util.Set类型的bean,其中包含值或引用

导入和混合配置

在JavaConfig中引用XML配置

BlankDisc从CDPlayerConfig拆分出来,定义到它自己的CDConfig类中,如下所示:

package soundsystem;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CDConfig {
  @Bean
  public CompactDisc compactDisc() {
    return new SgtPeppers();
  }
}

compactDisc()方法已经从CDPlayerConfig中移除掉了,我们需要有一种方式将这两个类组合在一起。一种方法就是 在CDPlayerConfig中使用@Import注解导入CDConfig:

package soundsystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(CDConfig.class)
public class CDPlayerConfig {
  
  @Bean
  public CDPlayer cdPlayer(CompactDisc compactDisc) {
    return new CDPlayer(compactDisc);
  }

}

或者采用一个更好的办法,也就是不在CDPlayerConfig中使用@Import,而是创建一个更高级别的SoundSystemConfig,在这个类中 使用@Import将两个配置类组合在一起:

package soundsystem;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import({CDPlayerConfig.class,CDConfig.class})
public class SoundSystemConfig {

}

不管采用哪种方式,我们都将CDPlayer的配置与BlankDisc的配置分开了。现在,我们假设(基于某些原因)希望通过XML来配 置BlankDisc,如下所示:

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

现在BlankDisc配置在了XML之中,用@ImportResource注解,假设BlankDisc定义在名为cd-config.xml的文件中,该文件位于根类路径下,那么可以修 改SoundSystemConfig,让它使用@ImportResource注解,如下所示:

package soundsystem;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;

@Configuration
@Import(CDPlayerConfig.class)
@ImportResource("classpath:cd-config.xml")
public class SoundSystemConfig {

}

配置在JavaConfig中的CDPlayer以及配置在XML中BlankDisc——都会被加载到Spring容器之中。因为CDPlayer中带 有@Bean注解的方法接受一个CompactDisc作为参数,因此BlankDisc将会装配进来,此时与它是通过XML配置的没有任何关系。

在XML配置中引用JavaConfig

将BlankDisc bean拆分到自己的配置文件中,该文件名为cd-config.xml,这与我们之前使用@ImportResource是一样的。 我们可以在XML配置文件中使用元素来引用该文件:

<?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 class="soundsystem.CDConfig" />

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

不再将BlankDisc配置在XML之中,而是将其配置在JavaConfig中,CDPlayer则继续配置在XML中。

元素只能导入其他的XML配置文件,并没有XML元素能够导入JavaConfig类。

有一个你已经熟知的元素能够用来将Java配置导入到XML配置中:元素。为了将JavaConfig类导入到XML配置中,我们可以这 样声明bean:

<?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 class="soundsystem.CDConfig" />

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

采用这样的方式,两种配置——其中一个使用XML描述,另一个使用Java描述——被组合在了一起。类似地,你可能还希望创建一个更高层次 的配置文件,这个文件不声明任何的bean,只是负责将两个或更多的配置组合起来。

以将CDConfig bean从之前的XML文件中移 除掉,而是使用第三个配置文件将这两个组合在一起:

<?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 class="soundsystem.CDConfig" />

  < import resource="cdplayer-config.xml" />
        
</beans>

不管使用JavaConfig还是使用XML进行装配,我通常都会创建一个根配置(root configuration),也就是这里展现的这样,这个配置会将两个 或更多的装配类和/或XML文件组合起来。我也会在根配置中启用组件扫描(通过<context:component-scan >或@ComponentScan)。