创建应用对象之间协作关系的行为通常被称为装配(wiring),这也是依赖注入的本质。本章将介绍使用Spring装配Bean的基础知识。因为依赖注入是Spring的最基本要素,所以在开发基于Spring的应用时,你无时无刻不在使用这些技术。
第二章 装配Bean
2.1 声明Bean
Spring 3.0提供两种配置Bean的方式。传统上,Spring使用一个或多个XML文件作为配置文件,而Spring 3.0还同时提供了基于Java注解的配置方式。在3.4节中我们将讲解基于Java注解的配置方式。
2.1.1 基于XML文件配置Bean
(1) Spring的核心框架自带了10个命名空间配置
在XML文件中声明Bean时,Spring配置文件的根元素来源于Spring beans命名空间所定义的
<beans>
元素。在<beans>
元素内,你可以放置所有的Spring配置信息,包括<bean>
元素的声明。但是beans命名空间并不是唯一的Spring命名空间。
(2) 在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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee-3.1.xsd" default-lazy-init="true">
<!-- 这里创建了一个由Spring容器管理的名字为duke的Bean,id属性定义了Bean的名字,也作为该Bean在Spring容器中的引用。根据class属性得知,duke是一个Juggler -->
<bean id="duke" class="com.springinaction.springidol.Juggler" />
</beans>
(3) 使用构造器注入举例:
当Spring容器加载Bean时,Spring将使用默认的构造器来实例化Bean,如上例,duke会使用
new com.springinaction.springidol.Juggler()
来创建。
<!-- 通过有int型和bean类型参数的构造器注入Bean,当Spring容器加载Bean时,Spring将使用拥有该类型参数的构造器来实例化Bean, -->
<bean id="duke" class="com.xxx.xxx.Duke">
<constructor-arg value="15" />
<constructor-arg ref="其他beanId引用" />
</bean>
**注:**如果想声明的Bean没有一个公开的构造方法,可以装配通过工厂的方法创建的Bean
(4) 通过工厂方法创建Bean
(1) 有时候静态工厂方法是实例化对象的唯一方法。Spring支持通过
<Bean>
元素的factory-method属性来装配工厂创建的Bean。(2) 如果要装配的对象需要通过静态方法来创建(例如单例模式),那么使用factory-method可指定静态方法实例化Bean。一般来说,单例类的实例只能通过静态工厂方法来创建。
<!-- 使用factory-method将单例类配置为Bean,如果要装配的对象需要通过静态方法来创建,name这种配置方式可以适用于任何场景 -->
<bean id="theStage" class="com.springinaction.springidols.Stage"
factory-method="getInstance" />
-----------------------------------------------------------
/**
*Stage没有一个公开的构造方法,取而代之的是,静态方法getInstance()每次被调用时都返回相同的实例(出于线程安全考虑,getInstance()使用了一种被称为“initialization on demand holder”的技术来创建单例类的实例)
*/
public class Stage {
private Stage(){
}
private static class StageSingletonHolder{ //延迟加载实例
static Stage instance = new Stage();
}
public static Stage getInstance(){
return StageSingletonHolder.instance; //返回实例
}
}
2.1.2 Bean的作用域
(1) 所有的Spring Bean默认都是单例。当容器分配一个Bean时(不论是通过装配还是调用容器的getBean()方法),它总是返回Bean的同一个实例。
(2) 当在Spring中配置<bean>
元素时,我们可以为Bean声明一个作用域。为了让Spring在每次请求时都为Bean产生一个新的实例,我们只需要配置Bean的scope属性为prototype即可。
例如:<bean id="ticket" class="com.xx.Ticket" scope="prototype">
除了prototype,Spring还提供了其他几个作用域选项:
2.1.3 初始化和销毁Bean
(1)为Bean定义初始化和销毁操作,只需要使用init-method和destroy-method参数来配置<bean>
元素。init-method属性指定了在初始化Bean时要调用的方法。类似地,destroy-method属性指定了Bean从容器移除前要调用的方法。
<bean id="auditorium" class="com.spring.springdemo.bean.springidol.Auditorium"
init-method="turnOnLights"
destroy-method="turnOffLights" />
———————————————————————————————————————————————————————————
public class Auditorium {
public void turnOnLights(){
System.out.println("表演大厅的灯打开了!");
}
public void turnOffLights(){
System.out.println("表演大厅的灯关闭了!");
}
}
(2)默认的init-method和destroy-method
如果在上下文中定义的很多Bean都拥有相同名字的初始化方法和销毁方法,则可以使用<beans>
元素的default-init-method和default-destory-method属性。
如图:
2.2 注入Bean属性
2.2.1 Spring构造器注入
Spring构造器注入见 2.1.1的第(3)点
2.2.2 Spring设值注入
在Spring中我们可以使用
<property>
元素配置Bean的属性。<property>
在许多方面都与<constructor-arg>
类似,只不过一个是通过构造函数来注入值,另一个是通过调用属性的setter方法来注入值。
<!-- 举例Spring的setter设值依赖注入方式 -->
<bean id="saxophone" class="com.spring.springdemo.bean.springidol.Instrument.Saxophone"></bean>
<bean id="kenny" class="com.spring.springdemo.bean.springidol.Instrumentalist">
<property name="song" value="Jingle Bells" />
<property name="instrument" ref="saxophone" />
</bean>
-------------------------------------------------------
public class Instrumentalist implements Performer{
public Instrumentalist() {
}
private String song;
private Instrument instrument;
@Override
public void perform() {
System.out.println("Playing " + song +" : ");
instrument.play();
}
//注入歌曲
public void setSong(String song){
this.song = song;
}
//注入乐器
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}
}
2.2.3 注入内部Bean
Spring设置的Bean能够被其他Bean所共享,只要将该Bean设置为属性即可,为了防止其他Bean共享,可将该Bean设置为内部Bean。
<!-- 举例Spring的内部Bean -->
<bean id="kenny2" class="com.spring.springdemo.bean.springidol.Instrumentalist">
<property name="song" value="Jingle Bells" />
<property name="instrument">
<bean class="com.spring.springdemo.bean.springidol.Instrument.Saxophone" />
</property>
</bean>
----------------------------------------------------------
<bean id="duke" class="com.spring.springdemo.bean.springidol.PoeticJuggler">
<constructor-arg value="15"></constructor-arg>
<constructor-arg>
<bean class="com.spring.springdemo.bean.springidol.Sonnect29"/>
</constructor-arg>
</bean>
2.2.4 使用Spring的命名空间p装配属性
Spring的命名空间p提供了另一种Bean属性的装配方式,该方式不需要配置如此多的尖括号。命名空间p与<property>
等价,其最主要的优点是更简洁。
命名空间p的schema URI为http://www.springframework.org/schema/p。如果你想使用命名空间p,只需要在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"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
</bean>
通过次声明,我们可以使用p:作为<bean>
元素所有属性的前缀来装配Bean的属性。举例如下:
<!-- 使用Spring的命名空间p装配属性 -->
<bean id="saxophone1" class="com.spring.springdemo.bean.springidol.Instrument.Saxophone" />
<bean id="kenny3" class="com.spring.springdemo.bean.springidol.Instrumentalist"
p:song = "Jingle Bells" p:instrument-ref="saxophone1" />
2.2.5 装配集合
使用value属性和ref属性仅在Bean的属性值为单个值的情况下才有用。当Bean的属性值是复数时,即属性的类型是集合,Spring提供了4种类型的集合配置元素。
集合元素 | 用于 |
---|---|
<list> | 装配list类型的值,允许重复 |
<set> | 装配set类型的值,不允许重复 |
<map> | 装配map类型的值,名称和值可以是任意类型 |
<props> | 装配properties类型的值,名称和值必须都是String型 |
(1)装配List、Set、Array
当装配类型为数组或者java.util.Collection任意实现的属性时,
<list>
和<set>
元素非常有用。其实属性实际定义的集合类型与选择<list>
或者<set>
没有任何关系。如果属性为任意的java.util.Collection类型时,这两个配置元素在使用时几乎可以完全转换。
<map>
和<props>
这两个元素分别对应java.util.Map和java.util.Properties。当我们需要由键-值对组成的集合时,这两种配置元素非常有用。这两种配置元素的关键区别在于,<props>
要求键和值都必须为String类型,而<map>
允许键和值可以是任意类型。
public class OneManBank implements Performer {
public OneManBank() {
}
private Collection<Instrument> instruments;
public void setInstruments(Collection<Instrument> instruments) {
this.instruments = instruments;
}
@Override
public void perform() {
for(Instrument instrument : instruments){
instrument.play();
}
}
}
-----------------------------------------------------------
<!-- 举例Spring装配集合类型属性 -->
<!-- list元素包含一个或多个值。这里的ref元素用来定义Spring上下文中的其他Bean引用,配置了Hank可以演奏吉他、钹和口琴。当然还可以使用其他的Spring设值元素作为list的成员,包括<value>、<bean>、和<null/>。实际上,<list>可以包含另外一个<list>z作为其成员,形成多维列表。
使用List<Instrument> instruments或Instrument[] instruments配置instruments属性<list>元素也一样有效。
-->
<bean id="hank" class="com.spring.springdemo.bean.springidol.OneManBank">
<property name="instruments">
<list>
<ref bean="guitar" />
<ref bean="cymbal" />
<ref bean="harmonica" />
</list>
</property>
</bean>
---------------------------------------------------------
<!--同样,也可以使用<set>元素来装配集合类型或者数组类型的属性。
再次说明,无论<list>还是<set>都可以用来装配类型为java.util.Collection的任意实现或者数组的属性。不能因为属性为java.util.Set类型,就表示用户必须使用<set>元素来完成转配。使用<set>元素配置java.util.List类型的属性是可以的,但需要确保List中的每一个成员都必须是唯一的。
-->
<bean id="hank" class="com.spring.springdemo.bean.springidol.OneManBank">
<property name="instruments">
<set>
<ref bean="guitar" />
<ref bean="cymbal" />
<ref bean="harmonica" />
</set>
</property>
</bean>
(2)装配Map集合
<map>
中的<entry>
元素由一个键和一个值组成,键和值可以是简单类型,也可以是其他Bean的引用。这些属性将帮助我们指定<entry>
的键和值。当键和值都不是String类型时,将键-值对注入到Bean属性的唯一方法就是使用<map>
元素。
public class OneManBank1 implements Performer {
public OneManBank1() {
}
private Map<String,Instrument> instruments;
public void setInstruments(Map<String, Instrument> instruments) {
this.instruments = instruments;
}
public void perform() {
for(String key : instruments.keySet()){
System.out.println(key + " : ");
Instrument instrument = instruments.get(key);
instrument.play();
}
}
}
----------------------------------------------------------
<!-- <map>元素声明了一个java.util.Map类型的值。每个<entry>元素定义Map的一个成员。Key属性指定了entry的键,而value-ref属性定义了entry的值,病引用了Spring上下文中的其他Bean。
-->
<bean id="hank1" class="com.spring.springdemo.bean.springidol.OneManBank.OneManBank1">
<property name="instruments">
<map>
<entry key="GUITAR" value-ref="guitar" />
<entry key="CYMBAL" value-ref="cymbal" />
<entry key="HARMONICA" value-ref="harmonicas" />
</map>
</property>
</bean>
(3)装配Properties集合
如果所配置的Map的每一个entry的键和值都为String类型时,我们可以考虑使用java.util.Properties代替Map。Properties类提供了和Map大致相同的功能,但是它限定键和值必须为String类型。
public class OneManBank2 implements Performer {
public OneManBank2() {
}
private Properties instruments;
public void setInstruments(Properties instruments) {
this.instruments = instruments;
}
@Override
public void perform() {
Enumeration enum1 = instruments.propertyNames();//得到配置文件的名字
while(enum1.hasMoreElements()){
String strKey = (String) enum1.nextElement();
String strValue = instruments.getProperty(strKey);
System.out.println(strKey + "=" + strValue);
}
}
}
-----------------------------------------------------------
<!-- <props>元素构建了一个java.util.Properties值,这个Properties的每个成员都由<prop>元素定义。每个<prop>元素都有一个key属性,其定义了Properties每个成员的键,而每一个成员的值由<prop>元素的内容所定义。 -->
<bean id="hank2" class="com.spring.springdemo.bean.springidol.OneManBank.OneManBank2">
<property name="instruments">
<props>
<prop key="GUITAR">STRUM STRUM STRUM</prop>
<prop key="CYMBAL">CRASH CRASH CRASH</prop>
<prop key="HARMONICA">HUM HUM HUM</prop>
</props>
</property>
</bean>
(4)装配空值
<property name="someNonNullProperty"><null/></property>
2.3 使用表达式装配(SpEL)
Spring 3 引入了Spring表达式语言(Spring Expression Language,SpEL)。SpEL是一种强大、简洁的装配Bean的方式,它通过运行期执行的表达式将值装配到Bean的属性或构造器参数中。使用SpEL,可以实现超乎想象的装配效果,这是使用传统的Spring装配方式难以做到的(甚至是不可能的)。
SpEL拥有许多特性,包括:
- 使用Bean的ID来引用Bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 正则表达式匹配;
- 集合操作。
注: 因为SpEL表达式最终是一个字符串,不易于测试,也没有IDE的语法检查的支持,所以建议在使用传统方式很难进行装配,而使用SpEL却很容易实现的场景下才使用SpEL。要小心,不要把过多的逻辑放入SpEL表达式中。
2.3.1 SpEL的基本原理
SpEL表达式的首要目标是通过计算获得某个值。在计算这个数值的过程中,会使用到其他的值并会对这些值进行操作。最简单的SpEL求值或许是对字面值、Bean的属性或者某个类的常量进行求值。
(1)字面值
<!-- #{}标记会提示Spring这个标记里的内容是SpEL表达式。 -->
<property name="count" value="#{5}" />
<!-- 与非SpEL表达式混用、浮点型、科学计数法、字符串型、布尔型 -->
<property name="message" value="The value is #{5}" />
<property name="frequency" value="#{89.7}" />
<property name="capacity" value="#{1e4}" />
<property name="name" value="#{'Chuck'}" />
<property name="name" value='{"Chuck"}' />
<property name="enabled" value="#{false}" />
(2)引用Bean、Properties和方法
<!-- 使用SpEL将ID为saxophone的Bean装配到instrument属性中 -->
<property name="instrument" value="#{saxophone}" />
<!-- 配置id为"carl"的Bean,carl的song属性值为id="kenny" 的Bean的song属性值,需要类中有getSong()方法 -->
<bean id="carl" class="com.spring.springdemo.bean.springidol.Instrumentalist">
<property name="song" value="#{kenny.song}" />
</bean>
<!-- 调用id="songSelector"的bean中的selectSong()方法,并使返回的字符串大写 -->
<bean id="carl" class="com.spring.springdemo.bean.springidol.Instrumentalist">
<property name="song" value="#{songSelector.selectSong().toUpperCase()}" />
</bean>
<!-- 使用null-safe存储器,防止因songSelector.selectSong()方法返回null时报空指针异常。使用"?."来代替"."来访问toUpperCase()方法。如果selectSong()返回null值,SpEL就不再尝试调用toUpperCase()方法 -->
<bean id="carl" class="com.spring.springdemo.bean.springidol.Instrumentalist">
<property name="song" value="#{songSelector.selectSong()?.toUpperCase()}" />
</bean>
(3)操作类
在SpEL中,使用 T() 运算符会调用类的作用域的方法和常量。例如T(java.lang.Math)
,会返回一个Math的类对象。该运算符可以访问指定类的静态方法和常量。
<!-- 将PI的值装配到Bean的一个属性中 -->
<property name="multiplier" value="#{T(java.lang.Math).PI}" />
<!-- 将一个0到1之间的随机数装配到Bean的一个属性中 -->
<property name="multiplier" value="#{T(java.lang.Math).random()}" />
2.3.2 在SpEL值上执行操作
SpEL提供了几种运算符,这些运算符可以用在SpEL表达式中的值上。
(1)使用SpEL进行数值运算
<!-- 把id="counter"的Bean的total属性值加42赋值给adjustedAmount属性 -->
<property name="adjustedAmount" value="#{counter.total + 42}" />
<!-- "+"号的字符串连接 -->
<property name="fullName" value="#{performer.firstName + ' ' + performer.lastName}" />
(2)比较值
<!-- 比较两个值是否相等,如果相等,Spring会将true装配给equal属性 -->
<property name="equal" value="#{counter.total == 42}" />
在Spring的XML配置文件中使用<=和>=符号时,会报错,这是因为这两个符号在XML中有特殊含义。当在XML中使用SpEL时,最好对这些运算符使用SpEL的文本代替方式(textual alternatives),如下所示:
<!-- 这里le运算符代表<=号 -->
<property name="hasCapacity" value="#{counter.total le 100000}" />
(3)逻辑表达式
<!-- 使用and运算符 -->
<property name="largeCircle" value="#{sharp.kind == 'circle' and shape.perimeter gt 10000}" />
<!-- 布尔类型的表达式求反,运算符为!或not -->
<property name="outOfStock" value="#{!product.available}" />
(4)条件表达式
使用SpEL的三元运算符”?:”
<property name="instrument" value="#{songSelector.selectSong() == 'Jingle Bells' ? piano : saxophone}" />
<!-- 三元运算符的变体,如果kenny.song不为null,那么表达式的求值结果是kenny.song,否则就是"Greensleeves"。当我们以这种方式使用时,"?:"通常被称为elvis运算符 -->
<property name="song" value="#{kenny.song ?: 'Greensleeves'"} />
(5)SpEL的正则表达式
SpEL通过matches运算符支持表达式中的模式匹配。
matches运算符对String类型的文本(左参数)应用正则表达式(右参数)。matches的运算结果将返回一个布尔类型的值。
<property name="validEmail" value="#{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'} />
2.3.3 在SpEL中筛选集合
举例:
public class City{
private String name;
private String state;
private int population;
...
getter/setter方法
...
}
-------------------------------------------------
<!-- `<util:list>`元素是由Spring的util命名空间所定义的。它创建了一个java.util.List类型的Bean,这个集合中包含了所有参数配置的Bean -->
<util:list id="cities">
<bean class="com.spring.springdemo.bean.springidol.City"
p:name="Chicago" p:state="IL" p:population="2853114" />
<bean class="com.spring.springdemo.bean.springidol.City"
p:name="Atlanta" p:state="GA" p:population="537958" />
<bean class="com.spring.springdemo.bean.springidol.City"
p:name="Dallas" p:state="TX" p:population="1279910" />
<bean class="com.spring.springdemo.bean.springidol.City"
p:name="Houston" p:state="TX" p:population="2242193" />
</util:list>
(1)访问集合成员
中括号”[]”运算符会始终通过索引访问集合中的成员。
例如:#{‘This is a test’[3]},将返回’s’。
<!-- 从集合中提取一个成员,并将它装配到某个属性中 -->
<property name="chosenCity" value="#{cities[2]}" />
<!-- 随机选择一个city -->
<property name="chosenCity" value="#{cities[T(java.lang.Math).random()*cities.size()]}" />
<!-- "[]"运算符同样可以用来获取java.util.Map集合中的成员。假设City对象以其名字作为键放入Map集合中。在这种场景下,我们可以像下面所展示的那样获取键为Dallas的entry -->
<!-- 从集合中提取一个成员,并将它装配到某个属性中 -->
<property name="chosenCity" value="#{cities['Chicago']}" />
“[]”运算符的另一种用法是从java.util.Properties集合中获取值。
<!-- 假设我们需要通过`<util:properties>`元素在Spring中加载一个properties配置文件。-->
<util:properties id="settings"
location="classpath:settings.properties" />
<!--settings Bean是一个java.util.Properties类,加载了名为settings.properties的文件。使用SpEL从settings Bean中访问一个名为twitter.accessToken的属性。
-->
<property name="accessToken" value="#{settings['twitter.accessToken']}" />
Spring还为SpEL创造了两种特殊的选择属性的方式:systemEnvironment 和 systemProperties。
- systemEnvironment 包含了应用程序所在机器上的所有环境变量。它恰巧是一个java.util.Properties集合,所以我们可以使用”[]”运算符通过键来访问Properties集合中的成员。
<!-- 在MacOS X 电脑上,将用户home目录的路径注入到一个bean的属性中 -->
<property name="homePath" value="#{systemEnvironment['HOME']}" />
- systemProperties 包含了Java应用程序启动时所设置的所有属性(通常通用 -D 参数)。
<!-- 如果使用 -Dapplication.home=/etc/myapp,来启动JVM,那么你就可以通过以下SpEL表达式将该值注入homePath属性中。 -->
<property name="homePath" value="#{systemProperties['application.home']}" />
(2)查询集合成员
可以使用查询运算符 “?.[]” 可以简单的查询满足某项条件的成员。查询运算符会创建一个新的集合,新的集合中只存放符合中括号内表达式的成员。
<!-- 查询人口多于10000的城市 -->
<property name="bigCities" value="#{cities.?[population gt 10000]}"
SpEL同样提供了两种其他查询运算符:”.^[]” 和 “.$[]” ,从集合中查询出第一个匹配项和最后一个匹配项。
<!-- 查询第一个符合条件人口多于10000的城市 -->
<property name="bigCities" value="#{cities.^[population gt 10000]}"
(3)投影集合
集合投影是从集合的每一个成员中选择特定的属性放入一个新的集合中。SpEL的投影运算符 “.![]”完全可以做到这点。
<!-- 将cities中的城市名抽取出来形成一个新的集合赋值给cityNames属性 -->
<property name="cityNames" value="#{cities.![name]}"
<!-- 将cities中的城市名和国家名以"name,state"的格式抽取出来形成一个新的集合赋值给cityNames属性 -->
<property name="cityNames" value="#{cities.![name+','+state]}"
<!-- 将cities中符合条件城市的城市名和国家名以"name,state"的格式抽取出来形成一个新的集合赋值给cityNames属性 -->
<property name="cityNames" value="#{cities.?[population gt 10000].![name+','+state]}"