(二)《Spring实战》——Spring核心
第二章:装配Bean
在Spring中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。例如,一个订单管理组件需要信用卡认证组件,但它不需要自己创建信用卡认证组件。订单管理组件只需要表明自己两手空空,容器就会主动赋予它一个信用卡认证组件。
创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。
1. Spring配置的可选方案
Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。但是,作为开发人员,你需要告诉Spring要创建哪些bean并且如何将其装配在一起。当描述bean如何进行装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:
- 在XML中进行显式配置。
- 在Java中进行显式配置。
- 隐式的bean发现机制和自动装配。
Spring有多种可选方案来配置bean,这是非常棒的,但有时候你必须要在其中做出选择。
这方面,并没有唯一的正确答案。你所做出的选择必须要适合你和你的项目。而且,谁说我们只能选择其中的一种方案呢?Spring的配置风格是可以互相搭配的,所以你可以选择使用XML装配一些bean,使用Spring基于Java的配置(JavaConfig)来装配另一些bean,而将剩余的bean让Spring去自动发现。
即便如此,我的建议是尽可能地使用自动配置的机制。显式配置越少越好。当你必须要显式配置bean的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置bean的时候),我推荐使用类型安全并且比XML更加强大的JavaConfig。最后,只有当你想要使用便利的XML命名空间,并且在JavaConfig中没有同样的实现时,才应该使XML。
2. 自动化装配bean
尽管显式装配技术(借助Java和XML来进行Spring装配)非常有用,但是在便利性方面,最强大的还是Spring的自动化配置。
Spring从两个角度来实现自动化装配:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
- 自动装配(autowiring):Spring自动满足bean之间的依赖。
为了阐述组件扫描和装配,我们需要创建几个bean,它们代表了一个音响系统中的组件。首先,要创建CompactDisc 类,Spring会发现它并将其创建为一个bean。然后,会创建一个CDPlayer 类,让Spring发现它,并将CompactDisc bean注入进来。
2.1 创建可被发现的bean
CompactDisc接口在Java中定义了CD的概念
package soundsystem; public interface CompactDisc { void play(); }
CompactDisc 的具体内容并不重要,重要的是你将其定义为一个接口。作为接口,它定义了CD播放器对一盘CD所能进行的操作。它将CD播放器的任意实现与CD本身的耦合降低到了最小的程度。
带有@Component注解的CompactDisc实现类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。没有必要显式配置SgtPeppers bean,因为这个类使用了@Component 注解,所以Spring会为你把事情处理妥当。
不过,组件扫描默认是不启用的。我们还需要显式配置一下Spring,从而命令它去寻找带有@Component 注解的类,并为其创建bean。
@ComponentScan注解启用了组件扫描
package soundsystem; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
如果没有其他配置的话,@ComponentScan 默认会扫描与配置类相同的包。因为CDPlayerConfig 类位于soundsystem 包中,因此Spring将会扫描这个包以及这个包下的所有子包,查找带有@Component 注解的类。这样的话,就能发现CompactDisc ,并且会在Spring中自动为其创建一个bean。
如果你更倾向于使用XML来启用组件扫描的话,那么可以使用Spring context 命名空间的<context:component-scan> 元素。
通过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>
测试组件扫描能够发现CompactDisc
package soundsystem; import static org.junit.Assert.*; import org.junit.Rule; 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 ,因此最终的应用上下文中应该包含CompactDisc bean。
测试代码中有一个CompactDisc 类型的属性,并且这个属性带有@Autowired 注解,以便于将CompactDisc bean注入到测试代码之中。
2.2 为组件扫描的bean命名
Spring应用上下文中所有的bean都会给定一个ID。在前面的例子中,尽管我们没有明确地为SgtPeppers bean设置ID,但Spring会根据类名为其指定一个ID。具体来讲,这个bean所给定的ID为sgtPeppers ,也就是将类名的第一个字母变为小写。
如果想为这个bean设置不同的ID,你所要做的就是将期望的ID作为值传递给@Component 注解。
还有一种方式是使用Java依赖注入规范(Java Dependency Injection)中所提供的@Named 注解来为bean设置ID。(不常用)
2.3 设置组件扫描的基础包
为了指定不同的基础包,你所需要做的就是在@ComponentScan 的value属性中指明包的名称:
要表明你所设置的是基础包,那么你可以通过basePackages属性进行配置:
要设置多个基础包的话,只需要将basePackages 属性设置为要扫描包的一个数组即可:
除了将包设置为简单的String类型(缺点类型不安全【not type-safe】)之外,@ComponentScan 还提供了另外一种方法,那就是将其指定为包中所包含的类或接口:
可以看到,basePackages 属性被替换成了basePackageClasses 。同时,我们不是再使用String 类型的名称来指定包,为basePackageClasses 属性所设置的数组中包含了类。这些类所在的包将会作为组件扫描的基础包。
2.4 通过为bean添加注解实现自动装配
简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired 注解。
如下,构造器上添加了@Autowired 注解,这表明当Spring创建CDPlayer bean的时候,会通过这个构造器来进行实例化并且会传入一个可设置给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方法上。
@Autowired public setCompactDisc(CompactDisc cd) { this.cd = cd; }
@Autowired 注解可以用在类的任何方法上。
@Autowired public insertCompactDisc(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 。
@Autowired 是Spring特有的注解。如果你不愿意在代码中到处使用Spring的特定注解来完成自动装配任务的话,那么你可以考虑将其替换为@Inject :
@Inject 注解来源于Java依赖注入规范,该规范同时还为我们定义了@Named 注解。在自动装配中,Spring同时支持@Inject 和@Autowired 。尽管@Inject 和@Autowired 之间有着一些细微的差别,但是在大多数场景下,它们都是可以互相替换的。
2.5 验证自动装配
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 { //测试代码使用StandardOutputStreamLog 基于控制台的输出编写断言 @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog(); //将CDPlayer bean注入到测试代码的player 成员变量之中(它是更为通用的MediaPlayer 类型) @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()); } }
3. 通过Java代码装配bean
在进行显式配置时,JavaConfig是更好的方案,因为它更为强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他Java代码一样。
同时,JavaConfig与其他的Java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其他的组件一样都使用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。尽管不是必须的,但通常会将JavaConfig放到单独的包中,使它与其他的应用程序逻辑分离开来,这样对于它的意图就不会产生困惑了。
3.1 创建配置类
package soundsystem;import org.springframework.context.annotation.Configuration; @Configuration public class CDPlayerConfig { }
创建JavaConfig类的关键在于为其添加@Configuration 注解,@Configuration 注解表明这个类是一个配置类,该类应该包含在Spring应用上下文中如何创建bean的细节。
3.2 声明简单的bean
要在JavaConfig中声明bean ,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean 注解。
@Bean public CompactDisc compactDisc() { return new SgtPeppers(); }
默认情况下,bean的ID与带有@Bean 注解的方法名是一样的。当然也可通过name属性自定义。
@Bean(name="lonelyHeartsClubBand") public CompactDisc compactDisc() { return new SgtPeppers(); }
3.3 借助JavaConfig实现注入
可以使用上面的代码,通过调用方法来引用bean,但最常用的方法是下面这种:
@Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); }
在这里,cdPlayer() 方法请求一个CompactDisc 作为参数。当Spring调用cdPlayer() 创建CDPlayer bean的时候,它会自动装配一个CompactDisc 到配置方法之中。然后,方法体就可以按照合适的方式来使用它。借助这种技术,cdPlayer() 方法也能够将CompactDisc 注入到CDPlayer 的构造器中,而且不用明确引用CompactDisc 的@Bean 方法。
4. 通过xml装配bean
4.1 创建xml配置规范
创建新的配置规范。在使用JavaConfig的时候,这意味着要创建一个带有@Configuration 注解的类,而在XML配置中,这意味着要创建一个XML文件,并且要以<beans> 元素为根。
<?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" 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"> </beans>
4.2 声明一个简单的bean
<bean id="compactDisc" class="soundsystem.SgtPeppers" />
4.3 借助构造器注入初始化bean
构造器注入,有两种基本的配置方案可供选择:
- <constructor-arg> 元素
- 使用Spring 3.0所引入的c-命名空间
4.3.1 构造器注入bean引用
<constructor-arg >使用ref 来引入其他bean
c-命名空间
4.3.2 将字面量注入到构造器中
<constructor-arg>使用value 属性表明给定的值要以字面量的形式注入到构造器之中。
c-命名空间
①引用构造器参数的名字
②通过参数索引装配相同的字面量值
4.3.3 装配集合
4.4 设置属性
引用bean
<property >通过ref引用bean
p-命名空间
将字面量注入到属性中
<property> 使用value 属性设置属性值
p-命名空间
不能使用p-命名空间来装配集合,装配集合可使用<util:list>
现在,我们能够像使用其他的bean那样,将磁道列表bean注入到BlankDisc bean的tracks 属性中
5. 导入和混合配置
在自动装配时,它并不在意要装配的bean来自哪里。自动装配的时候会考虑到Spring容器中所有的bean,不管它是在JavaConfig或XML中声明的还是通过组件扫描获取到的。
5.1 在JavaConfig中引用XML配置
假设BlankDisc 定义在名为cd-config.xml 的文件中,该文件位于根类路径下,那么可以修改SoundSystemConfig ,让它使用@ImportResource 注解,如下所示:
两个bean——配置在JavaConfig中的CDPlayer 以及配置在XML中BlankDisc ——都会被加载到Spring容器之中。因为CDPlayer 中带有@Bean 注解的方法接受一个CompactDisc 作为参数,因此BlankDisc 将会装配进来,此时与它是通过XML配置的没有任何关系。
5.2 在XML配置中引用JavaConfig
在XML中,我们可以使用<import >元素来拆分XML配置。