Spring 学习笔记之自动化装配 Bean

在 Spring 中可以使用 Java 代码、XML 和自动化装配三种方式来装配 Bean。从便利性角度来说,最强大的还是 Spring 的自动化配置,如果 Spring 能够进行自动化装配的话,那何苦还要显式的将这些 Bean 装配在一起呢? Spring 从两个角度来实现自动化装配:

  • 组件扫描:Spring 会自动发现应用上下文中所创建的 Bean;
  • 自动装配:Spring 自动满足 bean 之间的依赖。

为了阐述组件扫描和装配,我们需要创建几个 Bean,它们代表了一个音响系统中的组件。

一、创建可被发现的 bean

定义 CD 的一个接口:

1
2
3
4
package cn.javacodes.spring.beans.soundsystem;
public interface CompactDisc {
void play();
}

CompactDisc 接口定义了 CD 播放器对一盘 CD 所能进行的操作。它将 CD 播放器的任意实现与 CD 本身的耦合降低到了最小的程度。 下面创建一个 CompactDisc 的实现:

1
2
3
4
5
6
7
8
9
10
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.stereotype.Component;
@Component
public class Transfer implements CompactDisc {
private String title = "transfer";
private String artist = "周传雄/小刚";
public void play() {
System.out.println("正在播放"+artist+"的专辑:" + title);
}
}

这里需要注意的是该类使用了 @Component 注解,表明该类会作为组件类,并告知 Spring 要为这个组件创建 bean。但是在这之前,由于默认组件扫描是不启用的。我们还需要显式配置一下 Spring,从而命令它去寻找带有 @Component 注解的类,并为其创建 bean。 下面的这个类展现了完成这件事情的最简介配置方式:

1
2
3
4
5
6
7
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class CDPlayerConfig {
}

如果没有其他配置的话,@ComponentScan 默认会扫描与配置类相同的包。因为 CDPlayerConfig 类位于 cn.javacodes.spring.beans.soundsystem 包中,因此 Spring 将会扫描这个包以及这个包下的所有子包,查找带有 @Component 注解标示的类,并在 Spring 中自动为其创建一个 bean。 当然,如果你更加倾向于使用 XML 来启用组件扫描的话,那么可以使用 Spring context 命名空间的 context:component-scan 元素。

1
2
3
4
5
6
7
8
9
<?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:configurator="http://www.springframework.org/schema/c"
xmlns:avalon="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
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="cn.javacodes.spring.beans.soundsystem"/>
</beans>

尽管我们可以使用 XML 的方案来启用组件扫描,但在后面的讨论中,更多的还是会使用基于 Java 的配置。 下面我们创建一个简单的 JUnit 测试,它会创建 Spring 上下文,并判断 CompactDisc 是不是真的创建出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.javacodes.spring.beans.soundsystem;
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;
import static org.junit.Assert.assertNotNull;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = CDPlayerConfig.class)
public class CDPlayerTest {
@Autowired
private CompactDisc cd;
@Test
public void cdShouldNotBeNull(){
assertNotNull(cd);
}
}

该类使用了 Spring 的 SpringJUnit4ClassRunner,以便在测试开始的时候自动创建 Spring 的上下文。注解 @ContextConfiguration 会告诉它需要在 CDPlayerConfig 中加载配置。因为 CDPlayerConfig 类中包含了 @ComponentScan 注解,因此最终的应用上下文中应该包含 CompactDisc bean。 为了证明这一点,在测试代码中有一个 CompactDisc 属性,并且这个属性带有 @Autowired 注解,以便于将 CompactDisc bean 注入到测试代码中,有关与 @Autowired 注解的更多内容将在后面讲述。最后,有一个简单的测试方法断言 cd 属性不为 null。如果它不为 null 的话,就意味着 Spring 能够发现 CompactDisc 类,自动在 Spring 上下文中将其创建为 bean 并将其注入到了测试代码中。 这个代码应该能够通过测试,并以测试成功的颜色显示。

二、为组件扫描的 bean 命名

Spring 上下文中所有的 bean 都有一个 id。在前面的例子中,即使我们并没有明确的给定 Transfer bean 一个 id,但 Spring 会根据类名为其给定一个 id。具体来讲,Spring 会默认给定一个将类名首字母变为小写的 id,例如上例中将给定的 id 为 transfer。 如果想为这个 bean 给定不同的 id,你需要做的就是将你所想要给定的 id 作为参数传递给 @Component 注解。例如:

1
2
3
4
5
6
7
8
9
10
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.stereotype.Component;
@Component("transfer")
public class Transfer implements CompactDisc {
private String title = "transfer";
private String artist = "周传雄/小刚";
public void play() {
System.out.println("正在播放"+artist+"的专辑:" + title);
}
}

还有另外一种为 bean 命名的方式,使用 Java 依赖注入规范中提供的 @Named 注解来为 bean 设置 id:

1
2
3
4
5
6
7
8
9
10
package cn.javacodes.spring.beans.soundsystem;
import javax.inject.Named;
@Named("transfer")
public class Transfer implements CompactDisc {
private String title = "transfer";
private String artist = "周传雄/小刚";
public void play() {
System.out.println("正在播放"+artist+"的专辑:" + title);
}
}

Spring 支持将 @Named 作为 @Component 注解的替代方案。两者之间有一些细微的差别,不过大多数场景种它们使可以相互替换的。但是推荐使用 @Component 而不是 @Named,因为 @Component 注解看起来更加能够知道它是干什么的。

三、设置组件扫描的基础包

现在我们已经知道,默认情况下 @ComponentScan 注解会扫描当前配置类所在的包及其子包,但我们可能更希望将配置类与其它类放在不同的包中,那么为了指定不同的基础包,可以将指定的包名作为参数传递给 @ComponentScan 注解即可:

1
2
3
4
@Configuration
@ComponentScan("cn.javacodes.spring.beans.soundsystem")
public class CDPlayerConfig {
}

当然也可以更加清晰的指明其是基础包,使用 basePackages 属性:

1
2
3
4
@Configuration
@ComponentScan(basePackages = "cn.javacodes.spring.beans.soundsystem")
public class CDPlayerConfig {
}

这里我们发现 basePackages 属性是复数形式,我们猜测它是否可以指定多个基础包呢?答案是正确的,如果想要指定多个包,那么只需要将要扫描的包放到一个数组中即可:

1
2
3
4
@Configuration
@ComponentScan(basePackages = {"cn.javacodes.spring.beans.soundsystem", "cn.javacodes.spring.beans.video"})
public class CDPlayerConfig {
}

上面的方式中,包名以简单的字符串进行表示,当然这是可以的。但是如果我们日后对代码进行重构,很有可能就会出现问题,所以这种通过简单的字符串来配置基础包的方式是不安全的。为了解决这个问题,我们可以将其指定为包中所包含的类或接口:

1
2
3
4
5
6
7
8
9
package cn.javacodes.spring.configuration;
import cn.javacodes.spring.beans.soundsystem.CDPlayer;
import cn.javacodes.spring.beans.video.DVDPlayer;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackageClasses = {CDPlayer.class, DVDPlayer.class})
public class CDPlayerConfig {
}

注意:这里不再使用 basePackages 属性,取而代之的是 basePackageClasses 属性。我们不再使用 String 类型的包名来指定包,而是为 basePackageClasses 属性设置的数组中包含了类。这些类所在的包会作为组件扫描的基础包。 当然,使用组件类直接给 basePackageClasses 属性并不是很好的方式,我们可以考虑在包中创建一个用来进行扫描的空标记接口。通过标记接口的方式,你依然能够保持对重构友好的接口引用,但是可以避免引用任何实际的应用程序代码。

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

简单来说,自动装配就是让 Spring 自动满足 bean 依赖的一种方法,在满足依赖的过程中,会在 Spring 应用上下文中寻找匹配某个 bean 需求的其它 bean。为了声明要进行自动装配,我们可以考虑使用 Spring 的 @Autowired 注解。 比如下面的 CDPlayer 类,它的构造器使用了 @Autowired 注解,表明当 Spring 创建 CDPlayer bean 的时候,会通过这个构造器来进行实例化并会传入一个可以设置给 CompactDisc 类型的 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.javacodes.spring.beans.soundsystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class CDPlayer {
private CompactDisc cd;
@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}

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

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

1
2
3
4
@Autowired
public void setCompactDisc(CompactDisc cd){
this.cd = cd;
}

在 Spring 完成初始化 bean 之后,它会尽可能的去满足 bean 的依赖。实际上,Setter 方法并没有什么特殊之处,@Autowired 可以出现在任何方法上。 假如有且只有一个 bean 匹配依赖需求的话,那么这个 bean 将会被封装起来。 如果没有匹配的 bean,那么在应用上下文创建的时候,Spring 将会抛出一个异常。为了避免异常,可以将 @Autowired 的 required 属性设置为 false,Spring 会尝试执行自动匹配,但是如果没有匹配的 bean 的话,Spring 会让这个 bean 处于未装配的状态:

1
2
3
4
@Autowired(required = false)
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}

但是,把 required 属性设置为 false 的时候你需要注意,如果你的代码中没有 null 检查的话,这个处于未装配状态的属性有可能会出现空指针异常(NullPointerException)。 如果有多个 bean 都能满足依赖关系的话,Spring 会抛出一个异常,表明没有明确指定要选择哪个 bean 进行装配,有关于 Spring 自动化装配的歧义性的问题,我会在后续的文章中进行说明。 @Autowired 是 Spring 特有的注解,如果你不希望在代码中到处使用 Spring 特有的注解的话,那么可以考虑使用 @Inject 注解对其进行替换,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.javacodes.spring.beans.soundsystem;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class CDPlayer {
private CompactDisc cd;
@Inject
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
public void play(){
cd.play();
}
}

@Inject 注解来源于 Java 依赖注入规范,同 @Named 注解一样,@Inject 注解与 @Autowired 注解存在一些细微的差别,但大多数情况下它们可以进行相互替换。

五、验证自动装配

我们修改一下测试类 CDPlayerTest,使其能够借助 CDPlayer bean 播放 CD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.javacodes.spring.beans.soundsystem;
import cn.javacodes.spring.beans.MediaPlayer;
import cn.javacodes.spring.configuration.CDPlayerConfig;
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@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("正在播放周传雄/小刚的专辑:transfern", log.getLog());
}
}

该类中,除了注入 CompactDisc,还将 CDPlayer bean 注入到了测试代码中(更为通用的 MediaPlayer 类型)。在 play () 方法中,我们可以调用 CDPlayer 的 play () 方法并断言它的行为与你的预期是否一致。 自动化装配 Bean 还有更多的细节,我会在后续的文章中进行阐述。