第二章 装配Bean(Spring in action,3th)
第二章 装配Bean
创建应用对象之间协作关系的行为通常被称为装配(wiring),是依赖注入的本质。
XML方式声明Bean:
Spring配置文件的根元素是来源于Spring beans命名空间所定义的<beans>元素。在<beans>元素内,放置所有的Spring配置信息,包括<bean>元素的声明。beans非Spring的唯一命名空间,Spring核心框架自带10个命名空间。
此外,Spring Portfolio的许多成员,如Spring security、Spring Web Flow和Spring Dynamic Modules,也提供了它们自己的Spring命名空间配置。
声明一个简单的Bean:
在配置文件中声明<bean id = "duke" class="cn.song9989.Juggler"/>。id属性定义了Bean名字,也作为该Bean在Spring容器中的引用。
public interface Performer {
void permform() throws PerformanceException;
}
public class Juggler implements Performer {
private int beanBags = 3;
public Juggler() {
}
public Juggler(int beanBags) {
this.beanBags = beanBags;
}
@Override
public void permform() throws PerformanceException {
System.out.println("JUGGLER " + beanBags + " BEANBAGS" );
}
}
通过构造器注入:
<bean id = "duke" class="cn.song9989.Juggler">
<constructor-arg name="beanBags" value="16"/>
</bean>
通过构造器注入对象应用:
<!-- 有参注入,对象引用注入 -->
<bean class="cn.song9989.PoeticJuggler" id="poeticJuggler">
<constructor-arg name="beanBags" value="12"/>
<constructor-arg name="poem" ref="sonnet29"/>
</bean>
<bean id="sonnet29" class="cn.song9989.Sonnet29"/>
public interface Poem {
void recite();
}
public class Sonnet29 implements Poem {
private static String[] LINES = {
"大家准备好,",
"我要开始歌唱啦。",
"lalalalala"};
@Override
public void recite() {
for(String line : LINES){
System.out.println(line);
}
}
}
public class PoeticJuggler extends Juggler {
private Poem poem;
public PoeticJuggler(Poem poem) {
super();
this.poem = poem;
}
public PoeticJuggler(Poem poem, int beanBags) {
super(beanBags);
this.poem = poem;
}
@Override
public void permform() throws PerformanceException {
super.permform();
System.out.println("While reciting...");
poem.recite();
}
}
通过工厂方法创建Bean:
<!-- 通过工厂方法创建实例 -->
<bean class="cn.song9989.Stage" id="stage" factory-method="getInstance"/>
Factory-method允许调用一个指定的静态方法,从而代替构造方法来创建一个类的实例。
public final class Stage {
private Stage(){}
private static class StageSingletonHolder{
static Stage stage = new Stage();
}
public static Stage getInstance(){
return StageSingletonHolder.stage;
}
}
Bean的作用域:
Spring Bean默认都是单例。当容器分配一个Bean时(不论是通过装配还是调用容器的getBean()方法),总是返回Bean的同一个实例。若需每次请求时获得唯一的Bean实例,覆盖Spring默认的单例配置,需要再配置<bean>元素时,为其声明一个作用域。设置<bean>的属性scope为prototype,可保证配一个装配的Bean获取到不用的实例。此外,Spring还提供其他几个作用域选项,如下:
<!-- 声明此Bean作用域为prototpe,可任意实例化多次 -->
<bean class="cn.song9989.Ticket" id="ticket" scope="prototype"/>
Spring相关单例概念限于Spring上下文范围内(真正的单例,在每个类加载器中保证只有一个实例)。Spring的单例Bean只能保证在每个应用上下文中只有一个Bean的实例。
初始化和销毁Bean:
当实例化一个Bean时,可能需要初始化一些操作来确保Bean处于可用状态。同样,当不需要Bean,将其从容器中移除时,还可能需要按顺序执行一些清楚工作。为满足初始化和销毁Bean的需求,Spring提供了Bean生命周期的钩子方法。
在<bean>元素上配置属性init-method和属性destory-method参数。init-method属性指定初始化Bean需要调用的方法,destory-method属性指定Bean从容器中移除前调用的方法。
<!-- 初始化和销毁方法调用 -->
<bean class="cn.song9989.Auditorium" id="auditorium" init-method="turnOnLingts" destroy-method="turnOffLingts"/>
public class Auditorium {
public Auditorium() {
System.out.println("实例化...");
}
public void turnOnLingts(){
System.out.println("实例化后初始化方法...");
}
public void turnOffLingts(){
System.out.println("实例销毁前调用方法...");
}
}
public class AppCtx {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext-test.xml");
// 从容器中获取bean
Auditorium auditorium = ac.getBean("auditorium", Auditorium.class);
ConfigurableApplicationContext context = (ConfigurableApplicationContext) ac;
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
// 从容器中移除bean
defaultListableBeanFactory.removeBeanDefinition("auditorium");
}
}
默认的init-method和destory-method。在上下文中定义的很多Bean都拥有相同名字的初始化方法和销毁方法,可在<beans>元素上使用default-init-method和default-destory-method属性指定。如果上下文中的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"
default-init-method="turnOnLingts" default-destroy-method="turnOffLingts">
...
</beans>
注入Bean属性:
对于有有参构造器的Bean可以使用构造器方式注入,对于没有有参构造器的Bean可通过下面方式注入。
注入简单值:
在Spring中我们可以使用<property>元素配置Bean的属性。<property>许多方面和<constructor-arg>类似,只不过一个通过构造参数注入值,一个通过调用功能属性的setter方法来注入值。
<!-- 通过属性注入(setter方法) -->
<bean class="cn.song9989.Instrumentalist" id="instrumentalist">
<property name="song" value="My Heart Will Go On"/>
<property name="age" value="32"/>
</bean>
public interface Instrument {
void paly();
}
public class Instrumentalist implements Performer{
/**
* 歌曲
*/
private String song;
/**
* 年龄
*/
private int age;
/**
* 乐器
*/
private Instrument instrument;
public String getSong() {
return song;
}
public void setSong(String song) {
this.song = song;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Instrument getInstrument() {
return instrument;
}
public void setInstrument(Instrument instrument) {
this.instrument = instrument;
}
@Override
public void permform() throws PerformanceException {
System.out.println("Playing "+song+" : ");
instrument.paly();
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Instrumentalist{");
sb.append("song='").append(song).append('\'');
sb.append(", age=").append(age);
sb.append(", instrument=").append(instrument);
sb.append('}');
return sb.toString();
}
}
引用其他Bean:
<!-- 使用对象引用属性注入 -->
<bean class="cn.song9989.Instrumentalist" id="kenny">
<property name="song" value="Tomorrow Is Anthor Day"/>
<property name="instrument" ref="saxophone"/>
</bean>
<bean id="saxophone" class="cn.song9989.Saxophone"/>
public class Saxophone implements Instrument {
@Override
public void paly() {
System.out.println("TOOT TOOT TOOT");
}
}
注入内部bean:
内部Bean是通过直接声明一个<bean>元素作为<property>元素的子节点而定义的。
<!-- 使用内部Bean注入 -->
<bean class="cn.song9989.Instrumentalist" id="kenny">
<property name="song" value="Tomorrow Is Anthor Day"/>
<property name="instrument">
<bean class="cn.song9989.Saxophone"/>
</property>
</bean>
内部Bean并不仅仅限于setter注入,内部Bean装配到构造方法的入参中也可。
<!-- 构造器使用内部Bean注入 -->
<bean class="cn.song9989.PoeticJuggler" id="poeticJuggler">
<constructor-arg name="beanBags" value="12"/>
<constructor-arg name="poem">
<bean class="cn.song9989.Sonnet29"/>
</constructor-arg>
</bean>
注意内部Bean没有ID属性。它可以有配置一个ID属性,但我们不会通过名字来引用内部Bean。这突出了内部Bean的最大缺点:不能被复用。内部Bean仅适用于一次注入,而不能被其他Bean所引用。
使用Spring的命名空间p装配属性:
命名空间p的schema URI为http://www.springframework.org/schema/p。使用命名空间p,需要再Spring的xml配置中添加声明。
<bean class="cn.song9989.Instrumentalist" id="kenny" p:song="昨日重现" p:instrument-ref="saxophone"/>
装配集合:
使用Spring配置简单属性值(使用value属性)和引用其他Bean的属性(使用ref属性)。当Bean的属性值是复数时(属性的类型是集合),Spring提供了4中类型的集合配置元素。
public class OneManBand implements Performer {
private Collection<Instrument> instruments;
public Collection<Instrument> getInstruments() {
return instruments;
}
// 注入instrument集合
public void setInstruments(Collection<Instrument> instruments) {
this.instruments = instruments;
}
@Override
public void permform() throws PerformanceException {
for (Instrument instrument : instruments) {
instrument.paly();
}
}
}
装配List和Set集合:
OneManBand的instrument属性为java.util.Collection类型,使用了Java 5泛型来限制集合中的元素必须为Instrument类型。如果Bean的属性类型为数组类型或者java.util.Collection接口的任意实现都可以使用<list>元素。即向下面配置instruments属性,<list>元素也一样有效:
java.util.List<Instrument> instruments;
或者:
Instrument[] instruments;
同样,可以使用使用<set>元素来装配集合类型或者数组类型的属性:
<bean class="cn.song9989.OneManBand" id="oneManBand">
<property name="instruments">
<set>
<ref bean="guitar"/>
<ref bean="cymbal"/>
<ref bean="harmonica"/>
</set>
</property>
</bean>
无论<list>还是<set>都可以用来装配类型为java.util.Collection的任意实现或者数组属性。不能因为属性为java.util.Set类型,就表示用户必须使用<set>元素来完成装配。
装配Map集合:
public class OneManBand implements Performer {
private Map<String,Instrument> instruments;
public Map<String, Instrument> getInstruments() {
return instruments;
}
public void setInstruments(Map<String, Instrument> instruments) {
this.instruments = instruments;
}
@Override
public void permform() throws PerformanceException {
for (String key : instruments.keySet()) {
System.out.println(key+" : ");
instruments.get(key).paly();
}
}
}
装配Properties集合:
如果配置Map的每一个entry的键和值都为String类型时,可考虑使用java.util.Properties代替Map。Properties提供和Map大致相同功能,但限定键和值必须为String类型。
public class OneManBand implements Performer {
private java.util.Properties instruments;
public Properties getInstruments() {
return instruments;
}
public void setInstruments(Properties instruments) {
this.instruments = instruments;
}
@Override
public void permform() throws PerformanceException {
Enumeration<?> propertyNames = instruments.propertyNames();
while(propertyNames.hasMoreElements()){
String propertyName = propertyNames.nextElement().toString();
System.out.println("key: "+propertyName+" value:"+instruments.getProperty(propertyName));
}
}
}
装配空值:
某些场景需要将属性值设置为null,那么得显式地为该属性装配一个null值。设置属性为null值,只需要使用<null/>元素。
<property name="someNoneProperty"><null/></property>
使用表达式装配:
Spring3引入了Spring表达式语言(Spring Expression Language,SpEL)。SpEL是一种强大、简介的装配bean的方式。通过运行期执行的表达式将值装配到Bean的属性或者构造参数中。
SpEL拥有许多特性,包括:
- 使用Bean的ID来引用Bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 正则表达式匹配;
- 集合操作。
SpEL基本原理:
字面值:
可在<property>元素的value属性中使用#{}界定符把值装配到Bean属性中。如:
整数:
<property name="count" value="#{5}"/>
#{}标记会提示Spring这个标记里面的内容是SpEL表达式。他们还可与非SpEL表达式混用。
<property name="message" value="this is number #{123}"/>
字符串:
<property name="name" value="#{'zhangsan'}"/>
<property name='name1' value='#{"zhangsan"}'/>
浮点数字:
<property name="frequency" value="#{3.1415926}"/>
科学计数法:
<property name="capacity" value="#{1e4}"/>
布尔型:
<property name="flag" value="#{true}"/>
引用Bean、Properties和方法:
SpEL表达式可以通过ID引用其他Bean。如在SpEL表达式中使用Bean ID
将一个Bean装配到另一个Bean的属性中。
<bean class="cn.song9989.Instrumentalist" id="kenny4">
<!-- 使用SpEL注入歌曲名(字符串) -->
<property name="song" value="#{'My Heart Will Go On'}"/>
<!-- 使用SpEL注入乐器 -->
<property name="instrument" value="#{saxophone}"/>
</bean>
等同于:
<!-- 使用命名空间p注入属性 -->
<bean class="cn.song9989.Instrumentalist" id="kenny4" p:song="昨日重现" p:instrument-ref="saxophone"/>
也等同于:
<!-- 使用对象引用属性注入 -->
<bean class="cn.song9989.Instrumentalist" id="kenny4">
<property name="song" value="Tomorrow Is Anthor Day"/>
<property name="instrument" ref="saxophone"/>
</bean>
还等同于:
<!-- 使用内部Bean注入 -->
<bean class="cn.song9989.Instrumentalist" id="kenny4">
<property name="song" value="Tomorrow Is Anthor Day"/>
<property name="instrument">
<bean class="cn.song9989.Saxophone"/>
</property>
</bean>
结果是相同的,我们的确不需要SpEL来做这个,但可以再SpEL表达式中使用Bean的引用来获取Bean的属性。如:
<!-- 使用SpEL获取其他Bean的属性值并注入carl中 -->
<bean class="cn.song9989.Instrumentalist" id="carl">
<property name="song" value="#{kenny4.song}"/>
<property name="instrument" value="#{kenny4.instrument}"/>
</bean>
对Bean所能做到的不仅仅引用它的属性,还可以调用它的方法。假设有一个songSelect的Bean,该Bean有一个selectSong()的方法,该方法返回一首歌曲。
<bean class="cn.song9989.Instrumentalist" id="carl">
<property name="song" value="#{songSelect.selectSong()}"/>
</bean>
Carl希望song的歌词都是大写,可调用toUpperCase()方法。
<property name="song" value="#{songSelect.selectSong().toUpperCase()}"/>
在SpEL中避免抛出空指针异常的方式是使用null-safe存取器,使用?.运算符代替(.)来访问toUpperCase()方法。
<property name="song" value="#{songSelect.selectSong()?.toUpperCase()}"/>
操作类:
在SpEL中,使用T()运算符会调用类作用域的方法和常量。如:T(java.lang.Math)
在上面实例中,T()运算符的结果会返回一个java.lang.Math的类对象。如需的话,可以将它装配到Bean的一个Class类型的属性中。但是T()运算符正真的价值在于,通过该运算符可以访问指定类的静态方法和常量。举例:假设需要把PI的值装配到Bean的一个属性中,则只需要简单引用Math类的PI常量即可。如:
<property name="multiplier" value="#{T(java.lang.Math).PI}"/>
同样,使用T()运算符也可调用静态方法。例如,将一个随机数装配到Bean的一个属性中。
<property name="randomNumber" value="#{T(java.lang.Math).random()}"/>
当启动应用时,Spring开始装配randomNumber属性,它将Math.random()方法的返回值赋给该属性。
在SpEL值上执行操作:
SpEL提供了集中运算符,这些运算符可以作用在SpEL表达式中的值上。
使用SpEL进行数值运算:
SpEL提供所有Java支持的基础算术运算符,也增加(^)运算符来执行乘方运算。
+ 运算符 执行加法运算
<property name="adjustedAmount" value="#{apple.num + 42}"/>
+运算如果两边均为数字型,并不表示它们必须是字面值,可以是SpEL表达式,但表达式计算后的值必须是数字型。
- 运算符 执行减法运算
<property name="adjustedAmount" value="#{apple.num - 20}"/>
* 运算符 执行乘法运算
<property name="circumference" value="#{2 * T(java.lang.Math).PI * circle.radius}"/>
/ 运算符 执行除法运算
<property name="average" value="#{counter.total / counter.count}"/>
% 运算符 执行求余运算
<property name="remainder" value="#{counter.total % counter.count}"/>
^ 运算符 执行乘方运算
<property name="area" value="#{T(java.lang.Math).PI * circle.radius ^ 2}"/>
特别说明 + 运算符可以执行字符串拼接
<property name="fullName" value="#{performer.firstName + '' + performer.lastName}"/>
比较值
判断两个值是否相等或者两者之间那个更大,SpEL提供了Java所支持的比较运算符。
== 运算符 比较两个值是否相等
<property name="flag" value="#{apple.price == 100}"/>
等价于
<property name="flag" value="#{apple.price eq 100}"/>
逻辑表达式
需要机遇两个比较表达式进行求值,或相对某些布尔类型的值进行非运算,可适用逻辑运算。SpEL支持的所有逻辑运算符如下:
<property name="flag" value="#{apple.price eq 100 and apple.name == 'smallApple'}"/>
条件表达式
当某个条件为true时,SpEL表达式的求值结果是某个值。如果条件为false时,它的求值结果为另一个值。
<property name="name" value="#{apple.name == 'smallApple'?'smallApple':'smallRedApple'}"/>
下面情况判断值是否为null。如果apple.name是null,则设置smallApple给name,否则设置smallRedApple给name。
<property name="name" value="#{apple.name?'smallApple':'smallRedApple'}"/>
SpEL的正则表达式
当处理文本时,检查文本是否匹配某种模式,SpEL通过matches运算符支持表达式中的模式匹配。
matches运算符对String类型的文本(作为左边参数)应用正则表达式(右边参数)。matches的运算结果将返回一个布尔类型的值:如果与正则表达式相匹配,则返回true,否则返回false。
假设验证一个字符串是否为有效的邮件地址。可使用matches运算符。如下:
<property name="valiEmail" value="#{admin.email matches '[a-z-A-Z0-9._+-]aaa@qq.com[a-z-A-Z0-9._+-]+\\.com'}"/>
在SpEL中筛选集合
SpEL可进行集合操作。可以引用集合中的某个成员,也可基于属性值来过滤集合成员的能力,还可从集合的成员中提取某些属性放到新的集合中。
<!--通过Spring的<util:list>元素定义一个City的List集合-->
<util:list id="cities">
<bean class="cn.song9989.City" p:name="Chicago" p:state="IL" p:population="2853116"/>
<bean class="cn.song9989.City" p:name="Atlanta" p:state="GA" p:population="54329"/>
<bean class="cn.song9989.City" p:name="Dallas" p:state="TX" p:population="1279910"/>
<bean class="cn.song9989.City" p:name="Beijing" p:state="BJ" p:population="912930218"/>
<bean class="cn.song9989.City" p:name="Chongqing" p:state="CQ" p:population="873127912"/>
</util:list>
public class City {
// 城市名称
private String name;
// 城市Code
private String state;
// 人口
private int population;
// 省略getter/setter/toString方法
...
}
访问集合成员
可以通过下标(从0开始)直接挑选出一个城市,可以随机选择一个城市。
public class ChosenCity {
private City chosenCity;
// 省略getter/setter/toString方法
...
}
<bean class="cn.song9989.ChosenCity" id="chosenCity">
<!-- 直接挑选出第3个城市 -->
<!--<property name="chosenCity" value="#{cities[2]}"/>-->
<!-- 随机选择一个城市 -->
<property name="chosenCity" value="#{cities[T(java.lang.Math).random() * cities.size()]}"/>
</bean>
public class AppCtx {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext-test.xml");
ChosenCity chosenCity = ac.getBean("chosenCity", ChosenCity.class);
System.out.println(chosenCity.getChosenCity().toString());
}
}
[]运算符汇市中通过索引访问集合中的成员。
[]运算符同样可以用来获取java.util.Map集合中的成员。假设City对象以其名字作为键放入Map集合中,此情况下可通过键获取对象,如下。
<util:map id="cityMap">
<entry key="Chicago">
<bean class="cn.song9989.City" p:name="Chicago" p:state="IL" p:population="2853116"/>
</entry>
<entry key="Atlanta">
<bean class="cn.song9989.City" p:name="Atlanta" p:state="GA" p:population="54329"/>
</entry>
<entry key="Dallas">
<bean class="cn.song9989.City" p:name="Dallas" p:state="TX" p:population="1279910"/>
</entry>
<entry key="Beijing">
<bean class="cn.song9989.City" p:name="Beijing" p:state="BJ" p:population="912930218"/>
</entry>
<entry key="Chongqing">
<bean class="cn.song9989.City" p:name="Chongqing" p:state="CQ" p:population="873127912"/>
</entry>
</util:map>
<bean class="cn.song9989.ChosenCity" id="chosenCity">
<property name="chosenCity" value="#{cityMap['Chongqing']}"/>
</bean>
[]运算符量也可从java.util.Properties集合中获取值。方法和map取值一样。
<!-- 也可不写在properties配置文件里面,直接在util:properties中配置prop -->
<util:properties id="prop" location="classpath:config.properties"/>
或
<util:properties id="prop">
<prop key="city.name">ChongQing</prop>
<prop key="city.state">CQ</prop>
<prop key="city.population">910239123</prop>
</util:properties>
<bean id="city" class="cn.song9989.City" >
<property name="name" value="#{prop['city.name']}"/>
<property name="state" value="#{prop['city.state']}"/>
<property name="population" value="#{prop['city.population']}"/>
</bean>
除了<util:properties>所声明集合中的属性,Spring还为SpEL创建了两种特殊的选择属性方式:systemEnvironment和systemProperties。
[]运算符可铜鼓哦索引来得到字符串的某个字符。下面表达式返回“C”
<property name="name" value="#{'This city name is ChongQing'[18]}"/>
查询集合成员
想从城市集合中查询人口多余100000的城市,一种方式将所有cities Bean都装配到Bean属性中,然后在该Bean中增加过滤不符合条件的城市逻辑。但在SpEL中,只需要使用一个查询运算符(.?[])可做到。
public class Cities {
private Set<City> citySet;
// 省略getter/setter/toString方法
...
}
<bean class="cn.song9989.Cities" id="cities2">
<property name="citySet" value="#{cities.?[population > 100000]}"/>
</bean>
查询运算符会创建一个新的集合,新的集合中只存放符合中括号内的表达式成员。SpEL同样提供两种其他查询运算符:“.^[]”和“.$[]”,从集合中查询出第一个匹配项和最后一个匹配项。
投影集合
集合投影是从集合的每一个成员中选择特定的属性放入到一个新的集合中。SpEL的投影运算符(.![])可以做到。
例如,将上面Bean名称为cities的所有城市名变为一个城市名的集合。
<property name="cityNames" value="#{cities.![name]}"/>
投影不一定是投影的单一属性,可以是被投影对象的属组合,如下:
<property name="cityNames" value="#{cities.!['城市名:'+name+' 简称:'+state+' 人口:'+population]}"/>
建议:在使用传统方式很难或者不可能进行装配,而使用SpEL却很容易实现的场景下才使用SpEL。但要小心,不要把过多的逻辑放到SpEL表达式中。