装配Bean
创建应用对象之间协作关系的行为通常被称为装配(wiring),这是依赖注入(DI)的本质。当描述bean如何进行装配时,Spring具有非常大的灵活性,它提供了三种主要的装配机制:
- 在XML中进行显式配置;
- 在Java中进行显式配置;
- 隐式的bean发现机制和自动装配
前两种都是属于显式的配置,最后一种是就是所谓的自动装配了。其实选择哪一种方案没有什么绝对的,更多的时候是根据自己的喜好。Spring的配置风格是可以相互搭配的,所以可以选择使用XML装配一些bean,使用Spring基于Java的配置(JavaConfig)来装配另一些bean,而将剩余的bean让Spring自动发现,这种混合方式也是可以的。
作者建议尽可能的使用自动装配的机制。显式配置越少越好。当你必须显式配置bean的时候(比如,有些代码不是由你进行维护或是Jar包中的类,而当你需要为这些代码配置bean的时候),推荐使用类型安全并且比XML更加强大的JavaConfig,最后只有当你想使用便利的XML命名空间,并且在JavaConfig中没有相同的实现时,才应该使用XML。(而我之前一直使用的是自动装配,接下来就是XML的方式,对于JavaConfig的方式只是知道,而没有使用过,原因在于硬编码这一点,但是因为XML没有类型检查,所以作者更多的是从后期重构的角度出发,使用JavaConfig更为安全,在以后的项目中也可以尝试使用JavaConfig的方式)
自动装配bean
Spring从两个角度实现自动装配:
- 组件扫描(component scaning):Spring会自动发现上下文所创建的bean
- 自动注入(autowiring):Spring自动满足bean之间的依赖
将主键扫描与自动注入结合,可以把显式配置降低到最少。
创建bean
我们使用@Component注解表明一个类作为组件类,并告知Spring要为这个类创建bean。
为组件扫描的bean命名
Spring应用上下文中的所有bean都会给一个ID,如果没有明确为bean指明ID,Spring会根据类名为其指定一个ID,也就是将类名首字母小写。想为这个bean设置不同的ID,你所要说的就是将期望的ID作为值传递给@Component注解:
@Component("cdplayer") public class CdPlayer implements MediaPlayer {
还有另一种为bean命名的方式,这个方式不使用@Component注解,而是使用Java依赖注入规范中所提供的@Named注解来为bean设置ID:
import javax.inject.Named; @Named("cdplayer") public class CdPlayer implements MediaPlayer
Spring支持将@Named作为@Component注解的替代方案,在大多数场景中,他们可以互相替换,但是我更加喜欢使用@Component注解。
组件扫描默认是不开启的。我们还需要显式配置一下Spring,从而命令它寻找带有@Component注解的类,看来自动装配还是需要一点显式配置的。我们先使用JavaConfig的方式开启组件扫描:
@Configuration注解是用在JavaConfig方式中,用于表明该类是一个配置类,使用JavaConfig的方式不要忘记在配置类中加这个注解。
而ComponentScan注解才是真正启用组件扫描。
设置组件扫描的基础包
如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包,并将配置类所在的包作为基础包(base package)来扫描组件,但是,如果你想扫描不同的包,或者扫描多个基础包,需要在@ComponentScan的value属性中指明包的名称:
@Configuration @ComponentScan("cn.lynu") public class CDPlayerConfig {
如果想要清晰地表明你所需要的基础包,那么可以通过basePackages属性进行配置:
@Configuration @ComponentScan(basePackages="cn.lynu") public class CDPlayerConfig {
basePackages属性使用的是复数,这意味着可以配置多个基础包,配置多个基础包,设置一个数组即可:
@Configuration @ComponentScan(basePackages= {"cn.lynu","cn.lynu2"}) public class CDPlayerConfig {
在上面的例子中,我们设置基础包都是用String类型表示的,这是类型不安全的,如果后期重构代码,所指定的基础包就有可能出现错误。所以@ComponentScan提供了另一种方法,将类或接口作为组件扫描的基础包,使用的是basePackageClasses设置的基础类:
@Configuration @ComponentScan(basePackageClasses= {CdPlayer.class,DvDPlayer.class}) public class CDPlayerConfig {
可以考虑在包中创建一个用于进行扫描的空标记接口。通过标记接口的方式,依然能够保持对重构友好的接口引用,但是可以避免引用任何实际的应用程序代码。
如果更倾向于使用XML开启组件扫描的话,使用Spring context命名空间的<context:component-scan>元素:
<?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-4.3.xsd"> <context:component-scan base-package="cn.lynu"></context:component-scan> </beans>
<context:component-scan>元素有与@ComponentScan注解相对应属性和子元素。
通过注解实现自动装配
要使用自动装配,可以借助Spring的@AutoWired注解,可以在构造器上添加@AutoWired注解,这表明当Spring创建bean的时候,会使用这个构造器进行实例化并且会传入所需的类型的bean:
@Autowired注解不仅能够用在构造器上,还能用在属性的setter方法上:
@Autowired public void setCompactDisc(CompactDisc cd) { this.cd = cd; }
实际上,Setter方法并没有什么特殊之处,@AutoWired可以用在类的任何方法上。例如:
@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,如果没有匹配的bean的话,Spring就会让这个bean处于未装配的状态,但是,正因为此,需要很谨慎地对待,因为如果没有非空检查,就会出现NullPointerException。
如果有多个bean被匹配上,Spring也会抛出一个异常,表明无法明确指定要使用哪个bean进行自动装配。
@Autowirer是Spring特有的注解,如果不想使用,我们也可以使用Java依赖注入规范中的@Inject:
import javax.inject.Named; @Named("cdplayer") public class CdPlayer implements MediaPlayer{ ... @Inject public CdPlayer(CompactDisc cd){ this.cd=cd; } }
测试自动装配
package cn.lynu; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.SystemOutRule; 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 CDPlayTest { @Rule public final SystemOutRule log = new SystemOutRule().enableLog(); @Autowired private CompactDisc compactDisc; @Autowired private MediaPlayer player; @Test public void cdShouldnotBeNull() { assertNotNull(compactDisc); } assertTrue(compactDisc==player.getCd()); } @Test public void play() { player.play(); //使用System.out.println的话,别忘了\r\n assertEquals("Playing Sgt. Pepper's Lonely Hearts Club Band by The Beatles\r\n", log.getLog()); } }
使用Spring的 SpringJUnit4ClassRunner (Spring-test.jar)以便测试开始的时候自动创建Spring上下文(而不是使用applicationContext的之子类,如ClassPathXmlApplicationContext或AnnotationConfigApplicationContext,FileSystemApplicationContext之类的显式去创建应用上下文)。注解@ContextConfiguration会告诉Spring去哪里加载配置。因为在代码中使用了System.out.println()在控制台打印东西,所以我使用了 SystemOutRule 对象来测试打印的内容时候正确,而不是通过人眼去看。SystemOutRule源于System Rules库中的一个Junit规则,该规则可以基于控制台的输出内容编写断言。值得注意的是,使用println输出,在windows环境下需要使用“\r\n”来比对。
通过Java代码装配bean
尽管通过组件扫描和自动注入的方式实现Spring的自动化配置更为推荐,但是有时候自动配置的方案行不通,比如需要将第三方库中的组件装配到你的应用中,我们没有办法在它的类上添加@Component和@Autowired注解,这个时候就需要使用显式配置。在进行显式配置的时候,有两种可选方案:Java和XML。我们先来看看基于Java的配置。
其实我们之前用的CDPlayerConfig类就是一个配置类(让我们先把CDPlayerConfig的@ComponentScan注解取消,这里不是使用组件扫描),如果使用JavaConfig,通常会将这些配置类放在单独的包中,使它们与其他应用程序逻辑分离出来。创建JavaConfig配置类的关键在于使用@Configuration注解,这个注解表明当前类是一个配置类,该类包含Spring引用上下文如何创建bean的细节。
声明简单的bean
要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解。
@Bean public CompactDisc sgtPeppers() { return new SgtPeppers(); }
@Bean注解会告诉Spring这个方法会返回一个对象,该对象要注册为Spring应用上下文的bean。默认情况下,bean的id于带有@Bean注解的方法名是一样的。如果你想为其设置成一个不同的名字的话,可以使用name属性:
@Bean(name="disc") public CompactDisc sgtPeppers() { return new SgtPeppers(); }
为什么说使用JavaConfig的方式比XML更为强大,一方面是因为有类型检查,另一方面是因为在Javc类中可以进行逻辑控制,我们可以控制创建的实例:
@Bean public CompactDisc sgtPeppers() { int choise=(int)Math.floor(Math.random()*4); if(choise==0) { return new SgtPeppers(); }else if(choise==1) { return new WhiteAlbum(); }else if(choise==2) { return new HandDayNight(); }else { return new Revolver(); } }
借助JavaConfig实现注入
在这里我们不再使用@Autowired来进行注入,我们的注入方式是这样的:
@Bean public MediaPlayer cdPlayer() { return new CdPlayer(sgtPeppers()); }
通过调用 sgtPeppers()方法实现,看起来,这种方式每次都会得到有个新的 SgtPeppers 实例,因为每次调用都返回new的新的对象,但是我们可以比较这两个 SgtPeppers 实例,发现它们竟然是同一个实例。这是因为 sgtPeppers()方法上添加了@Bean注解,Spring会拦截所有对它的调用并确保返回的是Spring所创建的bean,也就是Spring在调用 sgtPeppers()方法时,容器自己创建的,所以,可以这样说,在默认情况下,Spring中的bean都是单例的。
这里这个调用的方式会出现歧义,我们可以使用更为明确的方式:
@Bean public MediaPlayer cdPlayer(CompactDisc compactDisc) { return new CdPlayer(compactDisc); }
将所需的依赖类作为方法的参数,当Spring调用 cdPlayer()创建bean的时候,依赖类就会自动装配进方法中。这种方法也不会要求将 CompactDisc 声明在同一个配置类中,更没有要求CompactDisc必须通过JavcConfig的方式声明,我们完全可以使用自动扫描或XML方式装配这个bean,不管通过什么方式创建的CompactDisc ,Spring都会将其传入配置方法中。
上面,我们一直使用构造器创建的bean(new出来的),我们也可以使用setter方法注入:
@Bean public MediaPlayer cdPlayer(CompactDisc compactDisc) { CdPlayer player=new CdPlayer(); player.setCompactDisc(compactDisc); return player; }
通过使用XML装配bean
在Spring刚出来的时候,XML是其主要的配置方式,但现在有了强大的自动化配置和基于Java的配置,作者不在建议使用XML作为第一选择,但是还是存在大量的基于XML的Spring配置,而其我也是比较多个在使用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-4.3.xsd"> </beans>
声明一个简单的bean
使用<bean>标签类似于javaConfig中的@Bean注解,我们可以这样声明:
<bean class="cn.lynu.SgtPeppers"/>
这个bean需要通过class属性指明是哪个类,使用的是全限定名。如果没有给这个bean指明ID,就如上面这个,会默认有一个Bean的ID,在这里是“cn.lynu.Sgtpeppers#0”,其中,“#0”是一个计数的形式,如果声明了另一个Sgtpeppers也没有给ID,那么它自动得到的ID就会是"cn.lynu.Sgtpeppers#1".
在大多是情况下,我们都会给bean一个我们自定义的ID,因为如果后面需要引用它,那么自动生成的名字就没太大用处了。通常使用id属性,为每一个bean设置一个你自己选择的名字:
<bean id="compactDisc" class="cn.lynu.SgtPeppers"/>
当Spring发现<bean>这个元素时,它会调用 SgtPeppers 的默认构造器来创建bean。相比较于JavaConfig,XML创建bean的方式更为被动,它不如JavaConfig那样,我们可以通过逻辑判断来选择性创建bean。而且在<bean>标签中使用的class值是以字符串的方式设置的,如果重命名类,而忘记修改XML就会出错,XML没有在编译期进行类型检查的能力(还好的是现在的Spring开发工具,例如Eclipse中的插件,或是STS都会去检查XML中的元素与属性)。
通过构造器注入依赖
在XML中使用DI,通过使用构造器的方式基本上有两种方案可供选择:
- <constructor-arg>元素
- 使用Spring3.0之后所引入的c命名空间
构造器输入bean的引用
可以使用<constructor-arg>元素的ref属性来引用其他bean:
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <constructor-arg ref="compactDisc"/> </bean>
Spring会将Id等于 compactDisc 的bean引用传递给 CdPlayer 的参数为CompactDisc的构造器进行初始化。
作为替代方法,也可以使用Spring的c命名空间,它是在XML更为简洁的描述构造器参数的方法,要使用它,必须先引入其XML约束:
xmlns:c="http://www.springframework.org/schema/c"
声明之后,我们就可以使用它声明构造器参数了:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" c:cd-ref="cdPlayer" />
它是由命名规范的,以c:打头,也就是命名空间的前缀,接下来是构造器的参数名,在此之后使用的是"-ref",这是一个命名的约定,它会告诉Spring正在装配的是一个bean的引用,而不是字面量。因为使用的是参数名,参数名很有可能在之后被修改,所以也可以使用参数在参数列表中的位置表示:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" c:_0-ref="cdPlayer" />
因为XML不允许将数字作为属性的第一个参数,所以添加一个下划线作为前缀,多个参数以此类推即可。
因为这里只有一个参数,所以我们可以使用更为简单的方式:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" c:_-ref="cdPlayer" />
这里没有使用参数名或参数索引,而是利用一个下划线,注意:它只适用于一个参数的情况。
将字面量注入到构造器
我们使用ref将bean的引用传递给构造器,但有的时候,我们可能只是需要用一个字符串来配置对象。我们先来使用<constructor-arg>元素来进行构造器注入,但是这次不是用ref,而是value属性,通过该属性表明给定的值要以字面量的方式注入到构造器。
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <constructor-arg value="123"/> <constructor-arg value="abc"/> </bean>
如果使用c命名空间,又该如何使用呢?
<bean id="cdPlayer" class="cn.lynu.CdPlayer" c:title="123" c:artist="abc" />
仍旧是c:参数名的方式,只不过是参数名后去掉了“-ref”后缀,当然,我们也可以使用参数索引的方式装配字面量:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" c:_0="123" c:_1="abc" />
因为这里涉及到多个参数(两个或两个以上)的情况,所以这里不能再使用一个下划线的方式,除非是只有一个构造器参数。
在装配bean引用和字面量方面,<constructor-arg>元素与c命名空间作用是相同的,但是,在装配集合类型的时候,就不一样了。
装配集合
先来改改构造器,添加一个集合作为参数:
public CdPlayer(List<String> tracks) { this.tracks = tracks; }
当我们不知道传怎样的集合作为参数的时候,可以使用<null/>标签,将一个空值进行传递:
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <constructor-arg><null/></constructor-arg> </bean>
当然,这不是很好的方案,因为虽然在注入期可以正常执行,但是运行的时候会出现NullPointerException异常。
我们使用<list>标签表示一个列表:
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <constructor-arg> <list> <value>1</value> <value>2</value> <value>3</value> </list> </constructor-arg>
<value>表明这个列表的元素都是字面量,当然,我们也可以使用<ref>元素,实现bean引用列表的装配。
但参数的类型是java.util.List的时候,使用<list>元素是合情合理的,尽管如此,我们也可以按照同样的方式使用<set>元素:
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <constructor-arg> <set> <value>1</value> <value>2</value> <value>3</value> </set> </constructor-arg> </bean>
<set>和<list>元素区别不大,其中最重要的是当以String需要装配进集合,所使用的是java.util.Set还是java.util.List,如果是Set的话,所有重复的值会被忽略,存放的顺序也不会得到保证。不过无论在哪种情况下,使用<set>或<list>都可以用来装配List或Set甚至数组。
属性注入
我们注入的方式除了使用构造器,我们也可以使用setter方式,<property>元素为属性的setter方法提供与<constructor-arg>元素功能一样:
<bean id="cdPlayer" class="cn.lynu.CdPlayer"> <property name="cd" ref="compactDisc"></property> </bean>
ref属性引用Id为 compactDisc 的bean,name属性指明需要注入到参数名为cd的属性中(通过setCd()方法)。引用的注入使用ref,字面量使用value属性
Spring还提供了更为简洁的p命名空间,作为<property>属性的替代方案。与c命名空间类似,需要先导入p的约束:
xmlns:p="http://www.springframework.org/schema/p"
我们使用p命名空间进行装配:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" p:cd-ref="compactDisc"></bean>
命名规范与c一致,p:属性名+"-ref",-ref做为引用的注入,去掉-ref表示装配字面量。
我们之前说过使用命名空间比标签的缺点就是不能直接注入集合,但是Spring提供了util命名空间用于解决这个问题,还是先引入util约束:
xmlns:util="http://www.springframework.org/schema/util"
util命名空间所提供的功能之一就是<util:list>元素,它会创建一个列表的bean
<util:list id="trackList"> <value>11111</value> <value>22222</value> <value>33333</value> </util:list>
这个时候c/p命名空间才可以注入集合:
<bean id="cdPlayer" class="cn.lynu.CdPlayer" p:tracks-ref="trackList"></bean>
当然 <util:list>元素只是util命名空间中的成员之一。
混合配置
在Spring中自动装配,JavaConfig,XML的方式都不是互斥的(因为装配并不在于这个bean来自于哪里),我们可以将它们混合在一起。实际上使用自动装配还是需要一点显式配置来开启组件扫描和自动注入。
那么在显式配置(未使用自动装配)中,如何在XML配置和Java配置中引入对方的bean。
在JavaConfig中引用XML配置
在引入XML方式之前我们先来说一下如何将其他的JavaConfig配置类引入到本配置类中。使用的正是@Import注解,
@Configuration
@Import(CDConfig.class) public class CDPlayerConfig {
或者采用一个更好的方法,就是不是在某一个配置类中引入其他配置类,而是创建一个更高级别的配置类,在这个配置那种使用@Import将多个配置类组合在一起:
@Configuration @Import({CDPlayerConfig.class,CDConfig.class}) public class SoundSystemConfig {
好了,回到正题,如何在JavaConfig中引入XML配置,答案就是@ImportResource注解,来看看如何引入一个位于根类路径下的名为applicationContext.xml的配置文件:
@Configuration @Import(CDPlayerConfig.class) @ImportResource("classpath:applicationContext.xml") public class SoundSystemConfig {
Ok,不论是在JavaConfig还是在XML配置的bean都会被加载到Spring容器中。
在XML中引入JavaConfig
我们还是想来看看如何拆分XML的,使用的是<import>标签:
<?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-4.3.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"> <import resource="cd-config.xml"/> </bean>
那么我们可不可以使用这个标签导入JavaConfig类,事实上并不可以,<import>标签只能导入XML文件,想要导入Java配置,使用的是我们很熟悉的<bean>标签:
<bean class="cn.lynu.CDConfig" />
类似的,我们可以采用一个更高级的配置文件,这个文件不声明任何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:context="http://www.springframework.org/schema/context" xmlns:c="http://www.springframework.org/schema/c" 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/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"> <import resource="cd-config.xml"/> <bean class="cn.lynu.CDConfig" /> </bean>
不管是JavaConfig还是XML进行装配,都建议在用一个更高层次的配置(就如上面这样),这个配置可以将两个或多个javaConfig或XML文件组合起来。也可以在这个配置中开启组件扫描(通过<context:component-scan>或@ComponentScan)。