第二章 装配Bean
创建应用对象之间协作关系的行为通常称为装配,这也是依赖注入(DI)的本质。
Spring提供了三种主要的装配机制:
① 在XML中进行显示配置;
② 在Java中进行显示配置;
③ 隐式的bean发现机制和自动装配。
Spring从两个角度来实现自动化装配:
① 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean;
② 自动装配(autowiring):Spring自动满足bean之间的依赖。
组件扫描和自动装配组合在一起就能发挥出强大的威力,它们能够将你的显式配置降低到最少。
以下以CD播放器和CD的例子为例,进行讲解Spring如何实现自动化装配的。
先建立一个磁盘的接口,如下:
package com.chenjl.autowire; public interface CompactDisc { void play (); }
再建立一个它的实现,该类添加了@Component注解,如下:
package com.chenjl.autowire; import org.springframework.stereotype.Component; @Component public class SgtPepers implements CompactDisc{ private static String title = "最炫名族风"; private static String artist = "凤凰传奇"; @Override public void play() { System.out.println(title + " played by " + artist); } }
添加了@Component注解后,Spring会为你把事情处理妥当。不过,组件扫描默认是不启用的。我们还需要显示配置一下Spring,从而命令它去寻找带有@Component注解的类,并为其创建bean。如下所示:
package com.chenjl.autowire; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
类CDPlayerConfig通过java代码定义了Spring的装配规则。它使用了@ComponentScan注解,这个注解能够在Spring中启用组件扫描。
如果没有其它配置的话,@ComponentScan注解默认会扫描与配置类相同的包。
也可以使用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.xsd"> <context:component-scan base-package="com.chenjl.autowire"/> </beans>
为了测试组件扫描,我们创建了一个简单的JUnit测试:
package com.chenjl.test; import com.chenjl.autowire.CDPlayerConfig; import com.chenjl.autowire.CompactDisc; 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 CDPlayTest { @Autowired CompactDisc compactDisc; @Test public void cdShouldNotBeNull () { assertNotNull(compactDisc); } }
CDPlayTest使用了Spring的SpringJUnit4ClassRunner,以便在测试开始的时候自动创建Spring上下文。注解@ContextConfiguration会告诉它需要在CDPlayerConfig中加载配置。
为了证明会生成对应的bean,在测试代码中有一个@Autowired注解标注的属性,并断言其不为空。
为组件扫描的bean 命名:
Spring应用上下文中所有的bean都会给定一个ID,如果没有人为的指定其ID,那么就将类名的第一个字母变为小写作为其ID。
要想设置其ID,则如下所示:
@Component("lonelyHeartsClub")
public class SgtPepers implements CompactDisc{
}
还有另外一种bean的命名方式,如下所示:
package com.chenjl.autowire; import javax.inject.Named; @Named("lonelyHeartsClub") public class SgtPepers implements CompactDisc{ }
其实@Named注解并不是jdk自带的,而是要导入jar包的。
设置组件扫描的基础包
为了指定不同的基础包,在@ComponentScan的value属性中指明包的名称,如下所示:
package com.chenjl.autowire;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.chenjl")
public class CDPlayerConfig {
}
可能你已经注意到了,basePackages使用的是复数的形式,意味着可以设置多个基础包,如下所示:
package com.chenjl.autowire; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = {"com.chenjl.autowire","com.chenjl.test"}) public class CDPlayerConfig { }
还可以指定类,如下所示:
package com.chenjl.autowire; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = {CompactDisc.class,CDDisc.class}) public class CDPlayerConfig { }
通过为bean添加注解实现自动装配
简单来说,自动装配就是让Spring自动满足bean依赖的一种方法,在满足依赖的过程中,会在Spring应用上下文中寻找匹配某个bean需求的其他bean。为了声明要进行自动装配,我们可以借助Spring的@Autowired注解。如下代码,通过自动装配,将一个CompactDisc注入到CDPlayer之中。
package com.chenjl.autowire; import org.springframework.beans.factory.annotation.Autowired; public class CDPlayer implements MediaPlayer{ private CompactDisc cd; // 通过构造器注入就可以播放任何的CD唱片 @Autowired public CDPlayer (CompactDisc cd) { this.cd = cd; } // 也可以通过set方法注入依赖 @Autowired public void setCd(CompactDisc cd) { this.cd = cd; } // 也可以用在一个普通的方法之上 @Autowired public void insertDisc (CompactDisc cd) { this.cd = cd; } public void play () { cd.play(); } }
不管是构造器、Setter方法还是其它方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配起来。
如果没有匹配的bean,那么在应用上下文创建的时候,Spring会抛出一个异常。为了避免异常的出现,你可以将@Autowired的required属性设置为false。
如下所示:
@Autowired(required = false) public void insertDisc (CompactDisc cd) { this.cd = cd; }
默认情况下是必须要注入依赖的。而且要注意的是:把required属性设置为false时,你需要谨慎对待,如果在你的代码中没有进行Null检查的话,这个处于未装配状态的属性有可能会出现NullPointException。
如果有多个bean都能满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。
@Autowired是Spring特有的注解。可以将@Inject注解和其相互替换。
验证自动装配
代码如下:
package com.chenjl.test; import com.chenjl.autowire.CDPlayerConfig; import com.chenjl.autowire.CompactDisc; import com.chenjl.autowire.MediaPlayer; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.*; 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 CDPlayTest { @Rule public final StandardOutputStreamLog log = new StandardOutputStreamLog(); @Autowired CompactDisc compactDisc; @Autowired private MediaPlayer mediaPlayer; @Test public void cdShouldNotBeNull () { assertNotNull(compactDisc); } @Test public void play () { mediaPlayer.play(); assertEquals("最炫名族风 played by 凤凰传奇", log.getLog()); } }
在测试代码中使用System.out.println()是稍微有点棘手的事情,因此,在该样例中使用了StandardOutputStreamLog,这是来源于System Rules库的一个JUnit规则,该规则能够基于控制台的输出编写断言,在这里,我们断言SgtPepers的play方法的输出被发送到了控制台上。
通过Java代码装配bean
尽管很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和@Autowired注解的,因此就不能使用自动化装配的方案了。
在这种情况下,必须要采用显示装配的方式。在进行显示装配的时候,有两种可选方案:Java和XML。
在进行显示配置时,JavaConfig是更好的方案,因为它更强大、类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其它Java代码一样。
同时,JavaConfig与其它的java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其它的组件一样都使用相同的语言进行表述,但JavaConfig是配置代码。这意味着它不应该包含任何业务逻辑,JavaConfig也不应该侵入到业务逻辑代码之中。尽管不是必须的,但通常会将JavaConfig放到单独的包中,使它与其它的应用逻辑分离开来,这样对它的意图就不会产生困惑了。
创建配置类
声明简单的bean
要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解,比方说,下面的代码声明了CompactDisc bean 注解:
package com.chenjl.config; import com.chenjl.autowire.CDDisc; import com.chenjl.autowire.CompactDisc; import com.chenjl.autowire.SgtPepers; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackageClasses = {CompactDisc.class,CDDisc.class}) public class CDPlayerConfig { @Bean public CompactDisc sgtPeppers() { return new SgtPepers(); } }
@Bean注解会告诉Spring这个方法会返回一个对象,该对象要注册为Spring应用上下文中的bean。方法体中包含了最终产生bean实例的逻辑。
默认情况下,bean的ID与带有@Bean注解的方法名是一样的。在本例中,bean的名字将会是sgtPeppers。如果你想为其设置成一个不同名字的话,那么可以重命名该方法,也可以通过name属性指定一个不同的名字:
@Bean(name = "lonelyHeartsClubBand") public CompactDisc sgtPeppers() { return new SgtPepers(); }
不管你采用什么方法来为bean命名,bean声明都是非常简单的。方法体返回了一个新的SgtPeppers实例。这里是使用Java来进行描述的,因此我们可以发挥Java提供的所有功能,只要最终生成一个CompactDisc实例即可。
请稍微发挥下你的想象力,我们可能希望做点疯狂的事情,比如说,在一组CD中随机选择一个CompactDisc来播放:
@Bean(name = "lonelyHeartsClubBand") public CompactDisc randomBeatlesCD() { int choice = (int)Math.floor(Math.random()*4); if (choice == 0) { return new SgtPepers(); } else if (choice == 1) { return new WhiteAlbum(); } else if (choice == 2) { return new HardDaysNight(); } else { return new Revolver(); } }
借助JavaConfig实现注入
我们前面所声明的CompactDisc bean是非常简单的,它自身没有其它的依赖。但现在,我们需要声明CDPlayerbean,它依赖于CompactDisc。在JavaConfig中,要如何将他们装配在一起呢?
在JavaConfig中装配bean的最简单方式就是引用创建bean的方法。例如下面就是声明CDPlayer的可行方案:
@Bean public CDPlayer cdPlayer () { return new CDPlayer(sgtPeppers()); }
看起来,CompactDisc是通过调用sgtPepers()得到的,但情况并非完全如此。因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。
创建XML配置规范
在使用XML为Spring装配bean之前,你需要创建一个新的配置规范。在XML配置中,要创建一个XML配置文件,并且要以<beans>元素为根。
最为简单的Spring 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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
声明一个简单的bean
要在基于XML的Spring配置中声明一个bean,我们要使用spring-beans模式中的另外一个元素:<bean>。<bean>元素类似于JavaConfig中的@Bean注解。我们可以按照如下的方式声明CompactDiscbean:
<bean class="com.chenjl.autowire.SgtPepers"/>
这里声明了一个很简单的bean,创建这个bean的类通过class属性来指定的,并且要使用全限定的类名。
因为没有明确给定的ID,所以这个bean将会根据全限定类名来进行命名。在本例中,bean的ID将会是"com.chenjl.autowire.SgtPepers#0"。其中,“#0”是一个计数的形式,用来区分相同类型的其它bean。如果再声明一个相同类型的bean并且没有明确进行标识,那么它自动得到的ID将会是:“com.chenjl.autowire.SgtPepers#1”。
尽管自动化的bean命名方式非常方便,但如果你要稍后引用它的话,那自动产生的名字就没有多大用处了。因此,通常来讲更好的办法是借助id属性,为每个bean设置一个你自己选择的名字:
<bean id="sgtPepers" class="com.chenjl.autowire.SgtPepers"/>
为了减少XML中繁琐的配置,只对那些需要按名字引用的bean进行明确的命名。
当Spring发现这个bean元素时,它将会调用SgtPepers默认的构造器来创建bean。在XML配置中,bean的创建显得更加被动,不过,它并没有JavaConfig那样强大,在JavaConfig配置方式中,你可以通过任何可以想象到的方法来创建bean实例。
借助构造器注入初始化bean
在Spring XML配置中,只有一种声明bean的方式:使用<bean>元素并指定class属性。Spring会从这里获取必要的信息来创建bean。但是,在XML中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有两种基本的配置方案可供选择:
<Constructor-arg>元素
使用Spring 3.0所引入的c-命名空间
两者的区别在很大程度就是是否冗长繁琐。可以看到,<constructor-arg>元素比c-命名空间更加冗长,从而导致XML更加难以读懂。另外,有些事情<constructor-arg>可以做到,但是使用c-命名空间却无法实现。
构造器注入bean引用
<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer"> <constructor-arg ref="sgtPepers"/> </bean>
当Spring遇到这个bean元素时,它会创建一个CDPlayer实例。<constructor-arg>元素会告知Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中。
作为替代的方案,你也可以使用Spring的c-命名空间。c-命名空间是在Spring 3.0中引入的,它是在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:context="http://www.springframework.org/schema/context" 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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
在c命名空间和模式声明之后,我们就可以使用它来声明构造器参数了,如下所示:
<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer" c:cd-ref="sgtPepers"/>
在编写前面的样例时,关于c-命名空间,引用参数的名称看起来有点怪异,因为这需要在编译代码的时候,将调试标志保存在类代码中。如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。替换的方案是我们使用参数在整个参数列表中的位置信息。
<bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer" c:_0-ref="sgtPepers"/>
使用索引来识别构造器参数感觉比使用名字更好一些,即便在构建的时候移除掉了调试标志,参数却依然保持相同的顺序。如果有多个构造器参数的话,这当然是很有用处的。在这里因为只有一个构造器参数,所以我们还会有另外一个方案——根本不用去标识参数:
<!-- 我试了好像报错了 --> <bean id="cdPlayer" class="com.chenjl.autowire.CDPlayer" c:_-ref="sgtPepers"/>
将字面量值注入到构造器中
首先创建一个CompactDisc的一个新实现,如下所示:
package com.chenjl.autowire; public class BlankDisc implements CompactDisc{ private String artist; private String title; public BlankDisc(String artist, String title) { this.artist = artist; this.title = title; } @Override public void play() { System.out.println("Playing " + title + " by " + artist); } }
现在,我们将已有的SgtPeppers替换为这个类:
<bean id="blankDisc" class="com.chenjl.autowire.BlankDisc"> <constructor-arg value="The Beatles"/> <constructor-arg value="Sgt.Pepper's Lonely Hearts Club Band"/> </bean>
使用c命名空间替换如下:
<bean id="blankDisc" class="com.chenjl.autowire.BlankDisc" c:title="The Beatles" c:artist="Sgt.Pepper's Lonely Hearts Club Band"/>
可以看到,装配字面量与装配引用的区别在于属性名中去掉了“-ref”后缀。与之类似,我们也可以通过参数索引装配相同的字面量值,如下所示:
<bean id="blankDisc" class="com.chenjl.autowire.BlankDisc" c:_0="The Beatles" c:_1="Sgt.Pepper's Lonely Hearts Club Band"/>
XML不允许某个元素的多个属性具有相同的名字。因此,如果有两个或者更多的构造器参数的话,我们不能简单的使用下划线进行标示。但是如果只有一个构造器参数的话,我们就可以这样做了。
在装配bean引用和字面量值方面,<constructor-arg>和c-命名空间的功能是相同的。但是有一种情况是<constructor-arg>能够实现的,c-命名空间却无法做到的。下面让我们看一下如何将集合装配到构造器参数中。
装配集合
上代码,如下:
package com.chenjl.autowire; import java.util.List; public class BlankDisc implements CompactDisc{ private String artist; private String title; private List<String> tracks; public BlankDisc(String artist, String title, List<String> tracks) { this.artist = artist; this.title = title; this.tracks = tracks; } @Override public void play() { System.out.println("Playing " + title + " by " + artist); for (String track : tracks) { System.out.println("-Track:" + track); } } }
使用XML配置bean如下:
<bean id="compactDisc" class="com.chenjl.autowire.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> </list> </constructor-arg> </bean>
与之类似,也可以使用<ref>元素替代<value>,实现bean引用列表的装配,在此不做举例了。如果是Set集合,那元素就用<set>替代<list>。
设置属性
package com.chenjl.autowire; import com.chenjl.autowire.interfaces.CompactDisc; import com.chenjl.autowire.interfaces.MediaPlayer; import org.springframework.beans.factory.annotation.Autowired; public class CDPlayer implements MediaPlayer { private CompactDisc cd; // 也可以通过set方法注入依赖 @Autowired public void setCd(CompactDisc cd) { this.cd = cd; } }
该使用属性注入还是构造器注入呢?作为一个通用的规则,可以对强依赖使用构造器注入,对可选性的依赖使用属性注入。
如下XML设置属性:
<bean class="com.chenjl.autowire.CDPlayer"> <property name="cd" ref="compactDisc"/> </bean>
Spring提供了更为简洁的p-命名空间,作为<property>元素的替代方案。如下所示:
<bean class="com.chenjl.autowire.CDPlayer" p:cd-ref="compactDisc"/>
当然,也可以将字面量注入到属性中,在此不作示例。
可以使用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: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.xsd">
借助<util-list>可以将磁道列表转移到BlankDisc bean之外,并将其声明到单独的bean之中,如下所示:
<util:list id="trackList"> <value>Fixing a Hole</value> <value>Getting better</value> </util:list>
将磁道列表bean注入到BlankDisc bean的tracks属性中:
<bean id="compactDisc" class="com.chenjl.autowire.BlankDisc"> <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band"/> <constructor-arg value="The Beatles"/> <constructor-arg ref="trackList"/> <!--<list>--> <!--<value>Sgt.Pepper's Lonely Hearts Club Band</value>--> <!--<value>With a Little Help From My Friends</value>--> <!--</list>--> <!--</constructor-arg>--> </bean>
<util:list>只是util-命名空间中的多个元素之一,如下表列出了util-命名空间提供的所有元素:

导入和混合配置
在JavaConfig中引用JavaConfig:使用注解@Import(CDConfig.class),也可以引入多个:@Import(CDConfig.class,CDPlayerConfig.class)。
在JavaConfig中引用XML配置:使用注解@ImportResource("classpath:cd-config.xml")
在XML中引用XML配置:<import resource="cd-config.xml"/>
在XML中引用JavaConfig配置:直接将配置类作为bean注入<bean class="com.chenjl.config.CDPlayerConfig"/>
以上代码整合链接地址:https://github.com/1977288116/SpringAutowiringBean.git
浙公网安备 33010602011771号