Spring中的Bean装配
创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码很难被复用也很难进行单元测试。如果情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此之间高度耦合,难以复用和测试。
在 Spring 中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。因为 DI 是 Spring 的最基本要素,所以在开发基于 Spring 的应用时,你随时都在使用这些技术。
Spring 容器负责创建应用程序中的 bean 并通过 DI 来协调这些对象之间的关系。但是,作为开发人员,你需要告诉 Spring 要创建哪些 bean 并且如何将其装配在一起。当描述 bean 如何进行装配 时,Spring 具有非常大的灵活性,它提供了三种主要的装配机制:
- 在 XML 中进行显式配置。
- 在 Java 中进行显式配置。
- 隐式的 bean 发现机制和自动装配。
乍看上去,提供三种可选的配置方案会使 Spring 变得复杂。每种配置技术所提供的功能会有一些重叠,所以在特定的场景中,确定哪种技术最为合适就会变得有些困难。但是,不必紧张 —— 在很多场景下,选择哪种方案很大程度上就是个人喜好的问题,你尽可以选择自己最喜欢的方式。
Spring 有多种可选方案来配置 bean,这是非常棒的,但有时候你必须要在其中做出选择。
这方面,并没有唯一的正确答案。你所做出的选择必须要适合你和你的项目。而且,谁说我们只能选择其中的一种方案呢?Spring 的配置风格是可以互相搭配的,所以你可以选择使用 XML 装配一些 bean,使用 Spring 基于 Java 的配置(JavaConfig)来装配另一些 bean,而将剩余的 bean 让 Spring 去自动发现。
即便如此,我的建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置 bean 的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置 bean 的时候),我推荐使用类型安全并且比 XML 更加强大的 JavaConfig。最后,只有当你想要使用便利的 XML 命名空间,并且在 JavaConfig 中没有同样的实现时,才应该使用 XML。
自动化装配 bean
Spring 从两个角度来实现自动化装配:
- 组件扫描(component scanning):Spring 会自动发现应用上下文中所创建的 bean。
- 自动装配(autowiring):Spring 自动满足 bean 之间的依赖。
组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。
为了阐述组件扫描和装配,我们需要创建几个 bean,它们代表了一个音响系统中的组件。首先,要创建 CompactDisc 类,Spring 会发现它并将其创建为一个 bean。然后,会创建一个 CDPlayer 类,让 Spring 发现它,并将 CompactDiscbean 注入进来。
创建可被发现的 bean
在这个 MP3 和流式媒体音乐的时代,CD(compact disc)显得有点典雅甚至陈旧。它不像卡带机、八轨磁带、塑胶唱片那么普遍,随着以物理载体进行音乐交付的方式越来越少,CD 也变得越来越稀少了。
尽管如此,CD 为我们阐述 DI 如何运行提供了一个很好的样例。如果你不将 CD 插入(注入)到 CD 播放器中,那么 CD 播放器其实是没有太大用处的。所以,可以这样说,CD 播放器依赖于 CD 才能完成它的使命。
为了在 Spring 中阐述这个例子,让我们首先在 Java 中建立 CD 的概念。以下展现了 CompactDisc,它是定义 CD 的一个接口:
/**
* 一个CD唱片接口
*/
public interface CompactDisc {
/**
* 播放
*/
void play();
}
CompactDisc 的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了 CD 播放器对一盘 CD 所能进行的操作(就是播放)。它将 CD 播放器的任意实现与 CD 本身的耦合降低到了最小的程度。
我们还需要一个 CompactDisc 的实现,实际上,我们可以有 CompactDisc 接口的多个实现。在本例中,我们首先会创建其中的一个实现,带有 @Component 注解的 CompactDisc 实现类 SgtPeppers:
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";
@Override
public void play() {
System.out.println("Playing " + title + " by " + artist);
}
}
和 CompactDisc 接口一样,SgtPeppers 的具体内容并不重要。你需要注意的就是 SgtPeppers 类上使用了 @Component 注解。这个简单的注解表明该类会作为组件类,并告知 Spring 要为这个类创建 bean。没有必要显式配置 SgtPeppersbean,因为这个类使用了 @Component 注解,所以 Spring 会为你把事情处理妥当。
不过,组件扫描默认是不启用的。我们还需要显式配置一下 Spring, 从而命令它去寻找带有 @Component 注解的类,并为其创建 bean。以下的配置类展现了完成这项任务的最简洁配置,@ComponentScan 注解启用了组件扫描:
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* CD配置类
*/
@Configuration
@ComponentScan
public class CDPlayerConfig {
}
类 CDPlayerConfig 通过 Java 代码定义了 Spring 的装配规则。我们只需观察一下 CDPlayerConfig 类并没有显式地声明任何 bean,只不过它使用了 @ComponentScan 注解,这个注解能够在 Spring 中启用组件扫描。
如果没有其他配置的话,@ComponentScan 默认会扫描与配置类相同的包。Spring 将会扫描这个包以及这个包下的所有子包,查找带有 @Component 注解的类。这样的话,就能发现 CompactDisc,并且会在 Spring 中自动为其创建一个 bean。
可能有点让人难以置信,我们只创建了两个类,就能对功能进行一番尝试了。为了测试组件扫描的功能,我们创建一个简单的 JUnit 测试,它会创建 Spring 上下文,并判断 CompactDisc 是不是真的创建出来 了。CDPlayerTest 就是用来完成这项任务的,测试组件扫描能够发现 CompactDisc:
import static org.junit.Assert.*;
import org.junit.Test;
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。
为组件扫描的 bean 命名
Spring 应用上下文中所有的 bean 都会给定一个 ID。在前面的例子中,尽管我们没有明确地为 SgtPeppers bean 设置 ID,但 Spring 会根据类名为其指定一个 ID。具体来讲,这个 bean 所给定的 ID 为 sgtPeppers,也就是将类名的第一个字母变为小写。
如果想为这个 bean 设置不同的 ID,你所要做的就是将期望的 ID 作为值传递给 @Component 注解。比如说,如果想将这个 bean 标识为 lonelyHeartsClub,那么你需要将 SgtPeppers 类的 @Component 注解配置为如下所示:
@Componet("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
......
}
还有另外一种为 bean 命名的方式,这种方式不使用 @Component 注解,而是使用 Java 依赖注入规范(Java Dependency Injection)中所提供的 @Named 注解来为 bean 设置 ID,但是它并没有像 @Component 那样清楚地表明它是做什么的。
通过为 bean 添加注解实现自动装配
简单来说,自动装配就是让 Spring 自动满足 bean 依赖的一种方法,在满足依赖的过程中,会在 Spring 应用上下文中寻找匹配某个 bean 需求的其他 bean。为了声明要进行自动装配,我们可以借助 Spring 的 @Autowired 注解。
首先创建一个MediaPlayer接口
/**
* 这是个多媒体播放器接口
*/
public interface MediaPlayer {
/**
* 播放
*/
void play();
}
通过它实现一个CD播放器CDPlayer
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;
}
@Override
public void play() {
cd.play();
}
}
它的构造器上添加了 @Autowired 注解,这表明当 Spring 创建 CDPlayerbean 的时候,会通过这个构造器来进行实例化并且会传入一个可设置给 CompactDisc 类型的 bean,将一个 CompactDisc 注入到 CDPlayer 之中。
@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 CDPlayer(CompactDisc cd) {
this.cd = cd;
}
将 required 属性设置为 false 时,Spring 会尝试执行自动装配,但是如果没有匹配的 bean 的话,Spring 将会让这个 bean 处于未装配的状 态。但是,把 required 属性设置为 false 时,你需要谨慎对待。如果在你的代码中没有进行 null 检查的话,这个处于未装配状态的属性有可能会出现 NullPointerException。
如果有多个 bean 都能满足依赖关系的话,Spring 将会抛出一个异常,表明没有明确指定要选择哪个 bean 进行自动装配。在后面的文章中,我们会进一步讨论自动装配中的歧义性。
@Autowired 是 Spring 特有的注解。如果你不愿意在代码中到处使用 Spring 的特定注解来完成自动装配任务的话,那么你可以考虑将其替换为 @Inject:
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CDPlayer {
...
@Inject
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
...
}
由于篇幅,不做详细讨论了。
验证自动装配
现在,我们已经在 CDPlaye r的构造器中添加了 @Autowired 注解, Spring 将把一个可分配给 CompactDisc 类型的 bean 自动注入进来。首先在pom.xml中添加此次测试需要用到的依赖:
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
在测试代码中使用 System.out.println() 是稍微有点棘手的事情。因此,使用了System Rules (stefanbirkner.github.io) 库
接着修改一下 CDPlayerTest,使其能够借助 CDPlayer bean 播放 CD:
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=CDPlayerConfig.class)
public class CDPlayerTest {
@Rule
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog();
@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\r\n",
systemOutRule.getLog());
}
}
现在,除了注入 CompactDisc,我们还将 CDPlayerbean 注入到测试代码的 player 成员变量之中(它是更为通用的 MediaPlayer 类 型)。在 play() 测试方法中,我们可以调用 CDPlayer 的 play() 方法,并断言它的行为与你的预期一致。
现在,你已经了解了组件扫描和自动装配的基础知识。Spring还支持显式地装配 bean,这里不在讨论,看书去吧。 ^_^