【Core Spring】二、装配beans
在Spring中,对象不负责寻找和创建它们需要的其他对象。创建两个应用对象之间关联的动作是依赖注入的核心功能,通常称为装配。
创建beans和构建它们之间的关系是Spring的责任,但是告诉Spring哪些bean需要被创建并且怎样将它们装装配到一起是开发者的责任。Spring提供了三种基本的装配机制。
-
-
- 显式地通过XML配置
- 显式地通过Java配置
- 隐式地发现bean并且自动装配
-
自动装配beans
Spring从两个角度解决自动装配
-
-
- Component scanning----Spring自动发现beans并在容器中创建
- Autowiring-----Spring自动构建bean的依赖
-
组件扫描
组件扫描不是默认开启项,需要显式配置以便Spring寻找带有@Component注解的类并创建它们。@Component注解可以在它的属性中标注bean的ID。例如,@Component("id"),如果没有显式标注ID,那么Spring会将这个bean的ID设为类名并将首字母小写。基于Java配置如下:
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class CDPlayerConfig { }
除此之外,可以用Java Dependency Injection specification(JSR-330)中的注解@Named为bean提供ID。Spring支持将@Named替换@Component。
如果没有更多配置@ComponentScan将会默认扫描与配置类所在包相同的类。但是,应该显式地设置扫描的base package,因为那样你可以将所有的配置代码归置到一个包中,区别于应用代码。例如这样@ComponentScan("basepackagename")、或者@ComponentScan(basePackages="soundsystem"),多个包的情况,@ComponentScan(basePackages={"soundsystem", "video"})。用上述String形式配置base package时,并不是类型安全的,如果重构包名,指定的base package将会出错,因此@ComponentScan可以通过base package中的类或者接口定义base package,如:@ComponentScan(basePackageClasses={CDPlayer.class, DVDPlayer.class})
基于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" 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>
自动装配
如果应用程序中所有的对象都是独立的,没有依赖的,那么组建扫描就能完成所需功能。但是如果对象包含对其他对象的依赖,那么自动装配就能派上用场了。
@Autowired注解可以用在构造函数、属性的setter方法甚至任何方法上。无论构造函数、setter方法或其他任何方法,Spring将会尝试构建参数的依赖。仅当有且仅有一个bean匹配,才会将这个bean装配。如果没有bean匹配,Spring会在容器创建时抛出一个异常。为了避免这个异常你可以设置@Autowired的required属性为false,@Autowired(required=false)。当required属性为false时,Spring将会进行自动装配,但是如果没有bean匹配,会使需要注入依赖关系的bean未装配。你应该十分谨慎地设置required属性为false。bean的属性未被装配,有可能发生空指针异常。如果有多个bean匹配,Spring将会抛出异常表示出现歧义现象。
同样的,可以用Java Dependency Injection specification中的注解@Inject代替@Autowired。
通过Java装配bean
尽管通过自动装配的方式构造依赖很方便,但是有时我们必须显式地配置Spring。比如,需要从第三方类库中装配一些组件进入你自己的应用。因为你无法获取源码,所以没有机会在类上标记@Component和@Autowired注解。
Java方式配置优于XML方式的配置,它更强大,类型安全,方便重构。
创建一个JavaConfig类的关键在于用@Configuration注解标记它。通过编写一个创建所需类型实例的方法并且用@Bean标记它的方式在JavaConfig中声明一个bean。
@Bean public CompactDisc sgtPeppers() { return new SgtPeppers(); }
默认地,bean的ID会被设置为同@Bean注解标识的方法名称一样。也可以指定ID,@Bean(name="lonelyHeartsClubBand")。
如果一个bean需要依赖其他对象,最简单的装配方法是引用被依赖对象的方法,如下:
@Bean public CDPlayer cdPlayer() { return new CDPlayer(sgtPeppers()); }
cdPlayer方法与sgtPeppers方法有着微妙的不同,CDPlayer对象是通过调用接受CompactDisc对象作为参数的构造方法来创建的,而不是通过默认构造方法。貌似CompactDisc对象是通过调用sgtPeppers来构造的,但并不是这样。因为sgtPeppers()方法被注解了@Bean,Spring将会拦截任何对它的调用,以确保由sgtPeppers()方法生成的对象已经被返回了,而不是允许sgtPeppers()又一次被调用。例如:
@Bean public CDPlayer cdPlayer() { return new CDPlayer(sgtPeppers()); } @Bean public CDPlayer anotherCDPlayer() { return new CDPlayer(sgtPeppers()); }
如果对sgtPeppers()方法的调用与其他Java方法一样,那么每一个CDPlayer将会拥有自己的SgtPeppers实例。默认情况下,Spring的beans都是单例的。所以Spring拦截对sgtPeppers()方法的调用以确保返回的是由Spring自己调用sgtPeppers()方法生成的CompactDisc Bean。因此,两个CDPlayer Bean拥有相同的SgtPeppers实例。
这样做法很迷惑,下面是一种更简单和容易理解的方法。
@Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { return new CDPlayer(compactDisc); }
cdPlayer方法接受一个CompactDisc对象作为参数。当Spring调用cdPlayer方法生成CDPlayer Bean时,将会自动装配一个CompactDisc对象进入cdPlayer方法。
如果需要通过setter方法的方式实现DI,如下:
@Bean public CDPlayer cdPlayer(CompactDisc compactDisc) { CDPlayer cdPlayer = new CDPlayer(compactDisc); cdPlayer.setCompactDisc(compactDisc); return cdPlayer; }
通过XML装配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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context"> <!-- configuration details go here --> </beans>
相当于@Configuration
<bean class="soundsystem.SgtPeppers" />
相当于@Bean,如果没有指定ID会被默认命名为全称限定名,此例中为soundsystem.SgtPeppers#0。#0是用来区分类型相同bean的编号。最好指定ID
<bean id="compactDisc" class="soundsystem.SgtPeppers" />
首先值得注意的是,不像在JavaConfig中,你直接负责创建一个SgtPeppers实例。当Spring看见<bean>元素,它会通过默认构造函数帮你创建一个SgtPeppers bean。用XML配置Bean创建更加被动。
另外一件值得注意的是,bean的类型通过class属性的字符串设置。但是谁能保证class属性指向一个真的类?Spring的XML配置无法在编译期验证指向的Java类型。甚至于,如果你重命名了这个类,它将指向一个不存在的类。
在SpringXML配置中,只有一种方法声明bean,通过<bean>元素。但是当通过XML声明DI,将会有几种方式和风格。具体到构造器注入,主要有两种选择:
-
-
- <constructor-arg>元素
- 使用Spring3.0中引入的c-namespace
-
两者之间的差异是非常复杂的。如你所见,<constructor-arg>元素通常比c-namespace更冗长,在XML中也更难阅读。另一方面,<constructor-arg>元素可以做一些c-namespace做不到的。在构造器注入中,我们将两者作为并列选项。
<bean id="cdPlayer" class="soundsystem.CDPlayer"> <constructor-arg ref="compactDisc" /> </bean>
当Spring容器见到<bean>元素,它会创建一个CDPlayer实例。<constructor-arg>元素告诉它传递一个ID是compactDisc的bean的引用给CDPlayer的构造器。当然也可以使用Spring的c-namespace。c-namespace是Spring3.0中引入的一种更简洁的XML中构造器注入方式。如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c" 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"> <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" /> </beans>
c代表命名空间,cd代表构造器参数名称,-ref是一种命名约定,引入一个名为compactDisc的bean。但是通过参数名称引用bean会产生问题,指向参数位置会更好。
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />
这个比上一个XML看起来还古怪,由于XML不允许数字作为属性的第一个字符,所以用了_作为前缀。如果只有一个参数,可以根本不制定参数
<bean id="cdPlayer" class="soundsystem.CDPlayer" c:_-ref="compactDisc" />
通过构造器注入基本类型如下
<bean id="compactDisc" class="soundsystem.BlankDisc"> <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" /> <constructor-arg value="The Beatles" /> </bean>
c-namespace方式
<bean id="compactDisc" class="soundsystem.BlankDisc" c:_title="Sgt. Pepper's Lonely Hearts Club Band" c:_artist="The Beatles" /> 或 <bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles" />
<constructor-arg>元素可以做但是c-namespace方式无法实现的是注入容器
<bean id="compactDisc" class="soundsystem.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> <value>Lucy in the Sky with Diamonds</value> <value>Getting Better</value> <value>Fixing a Hole</value> <!-- ...other tracks omitted for brevity... --> </list> </constructor-arg> </bean> 或 <constructor-arg> <list> <ref bean="sgtPeppers" /> <ref bean="whiteAlbum" /> <ref bean="hardDaysNight" /> <ref bean="revolver" /> ... </list> </constructor-arg>
setter方法注入
<bean id="cdPlayer" class="soundsystem.CDPlayer"> <property name="compactDisc" ref="compactDisc" /> </bean>
同样的,Spring提供p-namespace代替<property>元素。需要引入如下命名空间。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:p="http://www.springframework.org/schema/p" 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"> ... </bean>
用p-namespace装配属性,具体含义同c-namespace相同。
<bean id="cdPlayer" class="soundsystem.CDPlayer" p:compactDisc-ref="compactDisc" />
p-namespace无法装配容器。