本文主要依据《Spring实战》第一、二章内容进行总结
1、概述
创建应用对象之间协作关系的行为通常称为装配,这也是依赖注入(Dependency Injection DI)的本质。通过DI,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定,对象无需自行创建或管理它们的依赖关系。
DI所带来的最大收益是——松耦合。耦合是具有两面性的:
1. 紧密耦合的代码难以测试、难以复用、难以理解
2. 一定程度的耦合又是必须的,完全没有耦合的代码什么都做不了。
通过松耦合,如果一个对象只通过接口(而不是具体的实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。
Spring为装配Bean提供了三种方案:
1. 自动化装配Bean
2. 在Java代码中显示配置
3. 在XML中显式配置
2、自动化装配Bean
Spring从两个角度来实现自动化装配:
- 组件扫描:Spring会自动发现应用上下文中所创建的bean。
- 自动装配:Spring自动满足bean之间的依赖。
Spring通过应用上下文装载bean的定义并把它们组装起来,Spring应用上下文全权负责对象的创建和组装。
2.1、组件扫描
2.1.1、@Component注解
创建bean
Spring通过@Component注解表明一个类是组件类,Spring要为这个类创建bean。我们可以参考以下实例,本章主要是以CD为例,讲述DI的概念。
首先我们在Java中创建CD的概念,定义CompactDisc接口,它定义了CD播放器对一盘CD所能进行的操作。
public interface CompactDisc {
void play();
}
我们再为CompactDisc创建一个具体实现
@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);
}
}
在这个类上我们使用了@Component注解,表明这个类是组件类,Spring将会为这个类创建bean,我们无需再手动创建这个bean了。
为创建的bean命名
Spring应用上下文会为所有的bean给定一个ID,虽然我们上面的实例没有给bean指定ID,但是Spring会默认给定一个ID,它会将类名的第一个字母变为小写,即sgtPeppers就是这个bean的ID。
如果想为这个bean设置不同的ID,只需要将期望的ID作为值传递给@Component注解即可。
@Component("lonelyHeartsClub")
public class SgtPeppers implements CompactDisc {
…
}
2.1.2、@ComponentScan注解
组件扫描默认是不启用的,所以即使我们使用@Component注解声明组件类,Spring也不会自动发现并为其创建bean,要启动组件扫描,我们还需要显式配置一下Spring,从而命令它去寻找带有@Component注解的类,请参考以下实例:
@Configuration
@ComponentScan
public class CDPlayerConfig {
}
这个实例创建了一个配置类,并使用了@ComponentScan注解,这个注解能够在Spring中启动组件扫描。
设置组件扫描的基础包
如果没有其他配置的话,@ComponentScan默认会扫描与配置类相同的包及这个包下的所有子包。如果想扫描不同的基础包,可以在@ComponentScan的value属性中指定包的名称:
@Configuration
@ComponentScan("com.test")
public class CDPlayerConfig { }
如果想更加清晰地表明所设置的是基础包,可以通过basePackages属性进行配置:
@Configuration
@ComponentScan(basePackages="com.test")
public class CDPlayerConfig { }
也可以设置多个基础包,只需要将basePackages属性设置为要扫描包的一个数组即可:
@Configuration
@ComponentScan(basePackages={"com.test","soundsystem"})
public class CDPlayerConfig { }
通过basePackages方式设置的基础包是以String类型表示的,这是类型不安全的,如果重构代码改变了包的路径的话,就可能出现错误。我们可以使用basePackageClassses属性解决此问题。
@Configuration
@ComponentScan(basePackageClasses={TestBean.class})
public class CDPlayerConfig { }
basePackageClasses属性所设置的数组包含了类,这些类所在的包将作为组件扫描的基础包。
使用XML启动组件扫描
上面的实例是通过Java代码的@ComponentScan注解启动组件扫描的,我们也可以通过XML方式启动组件扫描,可以使用Spring context命名空间的<context:component-scan>
元素
<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-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="com.test" />
</beans>
2.2、自动装配
为满足依赖关系,Spring会在应用上下文中寻找匹配某个bean需求的其他bean。为了要声明进行自动装配,我们可以使用@Autowired注解,参考以下实例:
@Component
public class CDPlayer {
private CompactDisc cd;
@Autowired
public CDPlayer(CompactDisc cd) {
this.cd = cd;
}
public void play() {
cd.play();
}
}
这个实例中定义了一个CDPlayer类,并在构造器上添加了@Autowired注解,这表明Spring创建CDPlayer bean的时候,会通过这个构造器来进行实例化并且会传入一个可设置给CompactDisc类型的bean。
@Autowired注解不仅能够用在构造器上,还能用在属性的Setter方法上,例如:
@Autowired
public void setCd(CompactDisc cd) {
this.cd = cd;
}
实际上,@Autowired注解可以用在类的任何方法上。不管是构造器、Setter方法还是其他的方法,Spring都会尝试满足方法参数上所声明的依赖。假如有且只有一个bean匹配依赖需求的话,那么这个bean将会被装配进来。
如果没有匹配的bean,那么应用上下文创建的时候,Spring会抛出一个异常,为了避免异常的出现,可以将@Autowired的required属性设置为false。
@Autowired(required=false)
public void setCd(CompactDisc cd) {
this.cd = cd;
}
将required属性设置为false时,Spring会尝试执行自动装配,但是如果没有匹配的bean的话,Spring会让这个bean处于未装配状态。
如果有多个bean满足依赖关系的话,Spring将会抛出一个异常,表明没有明确指定要选择哪个bean进行自动装配。
3、通过Java代码装配Bean
在进行显式配置时,JavaConfig是更好的方案,因为它更为强大,类型安全并且对重构友好。因为它就是Java代码,就像应用程序中的其他Java代码一样。
同时JavaConfig与其他的Java代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的,它是配置代码,它不应该包含任何业务逻辑,也不应该侵入到业务逻辑代码之中,所以通常会将JavaConfig放到单独的包中,使它与其它的应用程序逻辑分离开来。
3.1、创建配置类
创建JavaConfig类的关键在于为其添加@Configuration注解,@Configuration注解表明这个类是一个配置类,下面这个实例就是最简单的一个配置类:
@Configuration
public class CDPlayerConfig {
}
3.2、声明Bean
要在JavaConfig中声明bean,我们需要编写一个方法,这个方法会创建所需类型的实例,然后给这个方法添加@Bean注解,例如以下代码声明了CompactDisc bean:
@Bean
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
@Bean注解会告诉Spring这个方法会返回一个对象,该对象要注册为Spring应用上下文中的bean。默认情况下,bean的ID与带有@Bean注解的方法名是一样的,如果想为其设置一个不同的名字的话,可以重命名该方法,也可以通过name属性指定一个不同的名字:
@Bean(name="lonelyHeartsClub")
public CompactDisc sgtPeppers() {
return new SgtPeppers();
}
带有@Bean注解的方法可以采用任何必要的Java功能来产生bean实例。
3.3、借助JavaConfig实现注入
借助JavaConfig实现注入主要有两种方法,下面将分别来进行介绍。
首先,也是最简单的方式就是引用创建bean的方法,例如:
@Bean
public CDPlayer cdPlayer() {
return new CDPlayer(sgtPeppers());
}
看起来CompactDisc是通过调用sgtPeppers()方法得到的,但情况并非完全如此,因为sgtPeppers()方法上添加了@Bean注解,Spring将会拦截所有对它的调用,并确保直接返回该方法所创建的bean,而不是每次都对其进行实际的调用。
另外,借助JavaConfig还有一种更为简单的方式实现注入:
@Bean
public CDPlayer cdPlayer(CompactDisc cd) {
return new CDPlayer(cd);
}
在这里cdPlayer方法请求一个CompactDisc作为参数,当Spring调用cdPlayer()创建CDPlayer bean的时候,它会自动装配一个CompactDisc到配置方法中。通过这种方式引用其他的bean通常是最佳的选择,因为它不会要求将CompactDisc声明到同一个配置类中,甚至没有要求CompactDisc必须要在JavaConfig中声明,实际上它可以通过组件扫描功能自动发现或者通过XML来进行配置。可以将配置分散到多个配置类、XML文件以及自动扫描和装配bean之中,只要功能完整健全即可。
4、通过XML装配Bean
通过XML装配Bean,首先要创建一个XML文件,并以<beans>
元素为根:
<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>
4.1、声明<bean>
要在基于XML的Spring配置中声明一个bean,可以使用<bean>
元素,例如:
<bean class="soundsystem.SgtPeppers" />
Spring发现这个<bean>
元素时,会调用SgtPeppers的默认构造器来创建bean。创建这个bean的类通过class属性来指定,并且要使用全限定类名。因为没有明确给定ID,所以这个bean将会根据全限定类名来进行命名,在本例中,bean的ID将会是”soundsystem.SgtPeppers#0”,其中#0是一个计数形式,用来区分相同类型的其他bean,如果声明了另外一个SgtPeppers,没有明确地进行标示,那么它自动得到的ID将会是”soundsystem.SgtPeppers#1”。也可以借助id属性,为每个bean设置一个确定的名字:
<bean class="soundsystem.SgtPeppers" id="compactDisc"/>
4.2、构造器注入
通过XML实现依赖注入有多种可选的方案,构造器注入就是其中一种,构造器注入也有两种基本的配置方案可供选择:
1. <constructor-arg>
元素
2. 使用Spring3.0所引入的c-命名空间
4.2.1、引用注入
在上面的实例中,CDPlayer bean有一个接受CompactDisc类型的构造器,我们已经在XML中声明了SgtPeppers bean,并且SgtPeppers类实现了CompactDisc类,所以实际上我们已经有了一个可以注入到CDPlayer bean中的bean了,我们可以通过以下的方式将SgtPeppers的引用注入到CDPlayer中:
<bean class="soundsystem.CDPlayer">
<constructor-arg ref="compactDisc" />
</bean>
当Spring遇到这个<bean>
元素时,它会创建一个CDPlayer实例, <constructor-arg>
元素会告知Spring要将一个ID为compactDisc的bean引用传递到CDPlayer的构造器中。
也可以使用Spring的c-命名空间,要使用它的话,必须要在XML的顶部声明其模式:
xmlns:c="http://www.springframework.org/schema/c"
c-命名空间用来声明构造器参数,它作为<bean>
元素的一个属性而不是子元素,例如:
<bean class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />
在这个例子中,属性名以“c:”开头,也就是命名空间的前缀,接下来就是要装配的构造器参数名,在此之后是“-ref”,这是一个命名的约定,它会告诉Spring,正在装配的是一个bean的引用,这个bean的名字是compactDisc。
引用参数的名称看起来有些怪异,因为这需要在编译代码时候,将调试标志保存在类代码中,如果你优化构建过程,将调试标志移除掉,那么这种方式可能就无法正常执行了。
替代的方案是我们使用参数在整个参数列表中的位置信息:
<bean class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />
我们将参数名称替换成了“0”,也就是参数的索引,因为在XML中不允许数字作为属性的第一个字符,因此必须要添加一个下划线作为前缀。如果构造器有多个参数的话,索引可以依次排列下去,如果构造器只有一个参数,那么完全可以不用去标示参数:
<bean class="soundsystem.CDPlayer" c:_-ref="compactDisc" />
4.2.2、将字面量注入到构造器中
迄今为止,我们所做的DI通常指的是类型的装配——也就是将对象的引用装配到依赖于它们的其他对象之中,而有时候我们需要做的只是用一个字面量值来配置对象,例如我们创建一个CompactDIsc的新实现:
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
public BlankDisc(String title, String artist) {
this.title = title;
this.artist = artist;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
}
}
我们使用<constructor-arg>
来进行构造器注入:
<bean class="soundsystem.BlankDisc" id="compactDisc" >
<constructor-arg value="Test Title" />
<constructor-arg value="Test Artist" />
</bean>
在这里,我们再次使用了<constructor-arg>
元素进行构造器参数的注入,但是没有使用“ref”属性来引用其他的bean,而是使用了“value”属性,通过该属性表明给定的值要以字面量的形式注入到构造器之中。
使用c-命名空间也可以实现同样的效果:
<bean id="compactDisc" class="soundsystem.BlankDisc" c:title="Test Title" c:artist="Test Artist"/>
可以看到,装配字面量与装配引用的区别在与属性名中去掉了“-ref”后缀,也可以通过参数索引装配相同的字面量值:
<bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Test Title" c:_1="Test Artist"/>
如果构造器只有一个参数的话,也可以简单地使用下划线进行标示:
<bean id="compactDisc" class="soundsystem.BlankDisc" c:_="Test Title" />
4.2.3、装配集合
如果有下面这样的一个类,Spring将如何把集合装配到构造器中呢?
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public BlankDisc(String title, String artist, List<String> tracks) {
this.title = title;
this.artist = artist;
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for(String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
最简单的方法就是使用<null/>
元素将列表设置为null:
<bean class="soundsystem.BlankDisc" id="compactDisc" >
<constructor-arg value="Test Title" />
<constructor-arg value="Test Artist" />
<constructor-arg><null/></constructor-arg>
</bean>
使用这种方法很容易引起空指针异常,我们也可以使用<list>
元素来声明一个列表:
<bean class="soundsystem.BlankDisc" id="compactDisc" >
<constructor-arg value="Test Title" />
<constructor-arg value="Test Artist" />
<constructor-arg>
<list>
<value>Test Track1</value>
<value>Test Track2</value>
<value>Test Track3</value>
</list>
</constructor-arg>
</bean>
<list>
元素是<constructor-arg>
的子元素,它是java.util.List类型的,表示一个包含值的列表将会传递到构造器中,<value>
元素用来指定列表中的每个元素,也可以使用<ref>
元素替代<value>
,实现bean引用列表的装配。
我们也可以使用<set>
元素表示集合,它是java.util.Set类型的,它和<list>
元素区别不大,如果是Set的话,所有重复的值都会被忽略掉,存放顺序也不会得以保证。
我们也可以使用<map>
元素表示一个Map,它是java.util.Map类型的,<map>
元素的使用如下:
<map>
<entry key="Test key1" value="Test value1"/>
<entry key="Test key2" value="Test value2"/>
<entry key="Test key3" value="Test value3"/>
</map>
其中<entry>
子元素表示Map中每一个键值对,<entry>
元素的key属性表示将key的字面量值引入键值对的key,key-ref表示将bean的引用注入Map中的key,value属性将字面量的值引入键值对的value,value-ref属性表示将bean的引用注入Map中的value。
4.3、属性注入
4.3.1、引用注入
属性注入也是通过XML实现依赖注入一种方案,我们先定义一个属性注入的CDPlayer:
public class CDPlayer {
private CompactDisc cd;
public void play() {
cd.play();
}
public void setCd(CompactDisc cd) {
this.cd = cd;
}
}
对于该选择构造器注入还是属性注入,一个通用的规则是:对强依赖使用构造器注入,对可选性依赖使用属性注入。
我们可以使用<property>
元素实现属性注入:
<bean class="soundsystem.CDPlayer">
<property name="cd" ref="compactDisc" />
</bean>
<property>
元素为属性的Setter方法所提供的功能与<constructor-arg>
元素为构造器所提供的功能是一样的,它通过ref属性引用一个ID为compactDisc的bean,并通过setCd()方法将其注入到cd属性中,在这里<property>
元素的name属性必须与setXXX的XXX同名。
我们也可以使用p-命名空间作为<property>
元素的替代方案,为了启用p-命名空间,必须要在XML文件中与其他的命名空间一起对其进行声明:
xmlns:p="http://www.springframework.org/schema/p"
通过p-命名空间,我们可以按照以下的方式装配cd属性:
<bean class="soundsystem.CDPlayer" p:cd-ref="compactDisc" />
p-命名空间中属性所遵循的命名约定与c-命名空间中的属性类似,首先,属性的名字使用“p:”前缀,表明我们所设置的是一个属性,接下来就是要注入的属性名,最后属性的名称以“-ref”结尾,这会提示Spring要进行装配的是引用,而不是字面量。
4.3.2、将字面量注入到属性中
属性也可以注入字面量,这与构造器参数非常相似。我们重新定义一下BlankDisc类,完全通过属性注入进行配置,而不是构造器注入:
public class BlankDisc implements CompactDisc {
private String title;
private String artist;
private List<String> tracks;
public void setTitle(String title) {
this.title = title;
}
public void setArtist(String artist) {
this.artist = artist;
}
public void setTracks(List<String> tracks) {
this.tracks = tracks;
}
public void play() {
System.out.println("Playing " + title + " by " + artist);
for(String track : tracks) {
System.out.println("-Track: " + track);
}
}
}
我们可以借助<property>
元素的value属性将字面量的值注入到属性中,例如:
<property name="title" value="Test Title" />
另一种方案是使用p-命名空间将字面量值的注入:
<bean class="soundsystem.BlankDisc" id="compactDisc" p:title="Test Title" ……>
与c-命名空间一样,装配bean引用和装配字面量的唯一区别在于是否带有“-ref”后缀,如果没有“-ref”后缀的话,所装配的就是字面量。
我们注意到,上面的BlankDisc类中还有一个List列表,我们同样也可以使用<list>
元素来进行属性注入:
<property name="tracks">
<list>
<value>Test Track1</value>
<value>Test Track2</value>
<value>Test Track3</value>
</list>
</property>
c-命名空间和p-命名空间都不能装配集合,但是我们可以使用util-命名空间来简化BlankDisc bean,然后再使用p-命名空间进行装配。
要使用util-命名空间,首先要在XML中声明util-命名空间及其模式:
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.1.xsd"
要注意的是schemaLocation也要声明,否则运行时会报错:
SAXParseException; lineNumber: 52; columnNumber: 28; cvc-complex-type.2.4.c: 通配符的匹配很全面, 但无法找到元素 'util:list' 的声明。
Spring中util-命名空间中的元素
元素 | 描述 |
---|---|
<util:constant> | 引用某个类型的public static域,并将其暴露为bean |
<util:list> | 创建一个java.util.List类型的bean,其中包含值或引用 |
<util:map> | 创建一个java.util.Map类型的bean,其中包含值或引用 |
<util:properties> | 创建一个java.util.Properties类型的bean |
<util:property-path> | 引用一个bean的属性(或内嵌属性),并将其暴露为bean |
<util:set> | 创建一个java.util.Set类型的bean,其中包含值或引用 |
在BlankDisc这个例子中,我们可以使用<util:list>
声明如下信息:
<util:list id="trackList">
<value>Test Track1</value>
<value>Test Track2</value>
<value>Test Track3</value>
</util:list>
借助<util:list>
,我们可以将列表声明到BlankDisc bean之外,它会创建一个列表的bean,这样我们就能像使用其他的bean那样,将列表bean注入到BlankDisc bean的tracks属性中:
p:tracks-ref="trackList"
5、导入和混合配置
一个典型的Spring应用中,可能会同时使用自动化和显式配置。自动装配会考虑到Spring容器中所有的bean,不管是在JavaConfig或XML中声明还是通过组件扫描获取到的。
5.1、在JavaConfig中引用XML配置
假设我们定义了两个JavaConfig,一个为CDPlayerConfig:
@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class})
@Import(BlankDiscConfig.class)
public class CDPlayerConfig {
}
一个为BlankDiscConfig:
@Configuration
public class BlankDiscConfig {
@Bean
public CompactDisc blankDisc(){
return new BlankDisc();
}
}
我们可以使用@Import注解将两个配置类组合到一起,还有一种更好的方法,就是创建一个更高级别的SoundSystemConfig,在这个类中使用@Import将两个配置类组合在一起:
@Configuration
@Import({CDPlayerConfig.class, BlankDiscConfig.class})
public class SoundSystemConfig {
}
如果要将配置类和XML组合到一起,可以使用@ImportResource引入配置XML:
@Configuration
@ComponentScan(basePackageClasses={CDPlayer.class})
@ImportResource("classpath:spring-bean.xml")
public class CDPlayerConfig {
}
5.2、在XML配置中引入JavaConfig
我们可以使用<import>
元素将两个配置XML组合到一起:
<import resource="spring-bean.xml" />
我们也可以使用<bean>
元素将Java配置导入到XML配置中:
<bean class="soundsystem.BlankDiscConfig" />
通常情况下,我们会创建一个根配置,这个根配置会将两个或更多的配置类/XML组合到一起。