四、bean的作用域
默认情况下,spring
应用上下文所有bean
都是作为以单例的形式创建的。但是有时候,我们所使用的的类是易变的,因此重用是不安全的。spring
定义了多种作用域,可以基于这些作用域创建bean
,包括:
- 单例(
Singleton
):在整个应用中,只创建bean
的一个实例 - 原型(
Prototype
):每次注入或者通过Spring
应用上下文获取的时候,都会创建一个新的bean
实例。 - 会话(
Session
):在Web
应用中,为每个会话创建一个bean
实例。 - 请求(
Request
):在Web
应用中,为每个请求创建一个bean
实例。
单例是默认的作用域,但是正如之前所述,对于易变类型,这并不合适,如果要选择其他作用域,可以使用@Scope
注解,可以和@Component
或@Bean
一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
说明:当然我们可以这样使用@Scope("prototype")
,但是上面那样更为安全且不易出错。如果要在XML
中配置的话,可以这样:
<bean id="notepad" class="com.myapp.Notepad" scope="prototype" />
说明:不管使用哪种方式来声明原型作用域,每次注入或从spring
上下文中检索该bean
的时候,都会创建新的实例,于是每次操作都有自己独有的Notepad
实例。
4.1 使用会话和请求作用域
在Web
应用中,如果能够实例化在会话和请求范围内共享的bean
,那将是非常有价值的事。比如在电商网站上购买商品,就希望购物车在此会话中是共享的:
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){}
说明:
-
上述作用域是会话,于是在一个购买商品的会话中,购物车是共享的。而
@Scope
同时还有一个proxyMode
属性,其值为ScopedProxyMode.INTERFACES
。这个属性解决了将会话或请求作用域的bean
注入到单例bean
中所遇到的问题。 -
假设要将
ShoppingCart bean
注入到单例StoreService bean
的Setter
方法中:
public class StoreService {
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shopingCart = shoppingCart;
}
}
因为StoreService
是一个单例的bean
,会在Spring
应用上下文加载的时候创建,此时,Spring
会试图将ShoppingCart bean
注入到setShoppingCart()
方法中去,但是ShoppingCart bean
是会话作用域,此时并不存在(此时没有发起会话)。同时,系统中每次会话就有一个ShoppingCart
实例,我们也并不希望注入某个固定的ShoppingCart
实例到StoreService
中。我们希望的是当StoreService
处理购物车功能时,它所使用的ShoppingCart
实例恰好是当前会话所对应的那一个。
-
实际上
1Spring
并不会将实际的ShoppingCart bean
注入到StoreService
中,而是会注入一个到ShoppingCart bean
的代理,如图所示。这个代理会暴露与ShoppingCart
相同的方法,所以StoreService
会认为它就是一个购物车。但是,当StoreService
调用ShoppingCart
的方法时,代理会对其进行懒解析并调用委托给会话作用域内真正的ShoppingCart bean
。
-
如果
ShoppingCart
是接口的话,这是可以的。但是如果是一个具体的类,Spring
就没有办法创建基于接口的代理了,需要使用CGLib
来生成基于类的代理。所以,如果bean
类型是具体类的话,必须要将proxyMode
属性设置为ScopedProxyMode.TARGET_CLASS
,以此来标明要以生成目标扩展的方式创建代理(而不是基于接口的代理扩展)。
4.2 在 XML 中声明作用域代理
在XML
中要设置代理模式,需要使用Spring aop
命名空间的一个新元素:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy />
</bean>
说明:这种命名方式和@Scope
注解的proxyMode
属性功能相同的Spring XML
配置元素。默认情况下,它会使用CGLib
创建目标类的代理。但是我们也可以将proxy-target-class
属性设置为false
,进而要求它生成基于接口的代理:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false" />
</bean>
当然要使用这种配置,必须在头部声明:
xmln:aop="http://www.springframework.org/shema/aop"
五、运行时值注入
前面也讲过,有时候需要将具体的值注入。但是有时候硬编码是可以的,但是有时候却又希望这些值在运行再确定。为了实现这些功能,Spring
提供了两种在运行时求值的方式:
- 属性占位符(
Property placeholder
) -
Spring
表达式语言(SpEL
)
5.1 注入外部的值
在Spring
中,处理外部值的最简单方式就是声明属性源并通过Spring
的Environment
来检索属性。下面看一个例子:
package com.soundsystem;
import ...;
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")//声明属性源
public class ExpressiveConfig {
@Autowired
Environment env;
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getProperty("disc.title"), env.getProperty("disc.artist"));//检索属性值
}
}
说明:在@PropertySource
中配置了名为app.properties
的文件:
disc.title=Sgt. Peppers Lonely Hearts Club Band
disc.artist=The Beatles
这个属性文件会加载到Spring
的Environment
中,稍后可以从这里检索属性。于是就可以从中取得相关的 值了。
5.1.1 深入学习 Spring 的 Environment
在Environment
类中,上述的getProperty()
方法并不是获取属性值的唯一方法,还有另外三个:
String getProperty(String key);
String getProperty(String key, String defaultValue);
T getProperty(String key, Class<T> type);
T getProperty(String key, Class<T> type, T defaultValue);
使用第二个方法可以为相关属性设置一个默认值,如果在配置文件中取不到相关值,则可以使用默认值:
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));//检索属性值
}
后面两种方法与前面两种类似,但是不会将所有的值都视为String
类型。假如你想要获取的值所代表的含义是连接池中所维持的连接数量。如果从属性配置文件中得到的是一个String
类型的值,那么在使用之前还要将其转换为Integer
类型:
int connectCount = env.getProperty("db.connection.count", Integer.class, 30);
Environment
还提供了几个与属性相关的方法,如果在使用getProperty()
方法时还没有指定默认值,并且这个属性没有定义,获取到的值就是null
。可以使用getRequiredProperty()
方法指定某个属性必须要定义:
@Bean
public BleankDisc disc(){
return new BlankDisc(env.getRequiredProperty("disc.title"),
env.getRequiredProperty("disc.artist");//检索属性值
}
如果上述两个属性值没有定义的话将会抛出IllegalStateException
异常。如果想检查某个属性是否存在的话可以:
boolean titleExists = env.containsProperty("disc.title");
最后,如果想将属性解析为类的话,可以这样:
Class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class", CompactDisc.class);
除了上述的这些方法,Environment
还提供了一些方法来检查哪些profile
处于激活状态:
String[] getActiveProfiles():返回激活profile名称的数组
String[] getDefaultProfiles():返回默认profile名称的数组
boolean acceptsProfiles(String ... profiles):如果Environment支持给定的profile,返回true
5.1.2 解析属性占位符
可以在XML
中使用占位符按照如下方式解析BlankDisc
构造函数参数:
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}" />
稍后会讨论这些属性是如何解析的。如果我们依赖组件扫描和自动装配来创建和初始化应用组件,那么就不需要指定占位符的配置文件或类了。
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}") String artist){
this.title = title;
this.artist = artist;
}
为了使用占位符,必须配置一个PropertyPlaceholderConfigurer bean
或PropertySourcePlaceholderConfigurer bean
。从Spring 3.1
开始,推荐使用后者,因为它能够基于Spring Environment
及其属性源来解析占位符。
@Bean
public static PropertySourcePlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcePlaceholderConfigurer();
}
而在XML
文件应这样配置:
<context:property-palceholder />
当然必须在头中配置context
的schema
。以上就是两种解析占位符的方式。
5.2 使用 Spring 表达式语言进行装配
在Spring
表达式语言SpEL
中有很多特性:
- 使用
bean
的ID
来引用bean
; - 调用方法和访问对象的属性
- 对值进行算术、关系和逻辑运算
- 正则表达式匹配
- 集合操作
5.2.1 SpEL 样例
需要了解的第一件事就是SpEL
表达式要放到"#{...}"
之中,这和属性占位符类似。下面直接看例子:
#{1}
这就表示数字1
。
#{T(System).currentTimeMillis()}
最终结果是当前时刻的毫秒数。T()
表示会将java.lang.System
视为Java
中对应的类型,因此可以调用其static
修饰的方法。我们还可以引用其他的bean
或其他bean
的属性:
#{sgtPeppers.artist}
这表示引用ID
为sgtPeppers bean
的属性。还可以通过systemProperties
对象引用系统属性:
#{systemProperties['disc.title']}
如果将之前配置注入相关属性值应用上SpEL
表达式,则就是这样:
public BlankDisc(@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.title']}") String artist){
this.title = title;
this.artist = artist;
}
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.title']}" />
5.2.2 表示字面值
#{3.14159}//浮点值
#{9.87E4}//科学计数法
#{'Hello'}//字符串
#{false}//布尔值
5.2.3 引用bean、属性和方法
#{sgtPeppers}//使用bean ID作为SpEL表达式
#{sgtPeppers.artist}//引用bean的属性artist
#{artistSelector.selectArtist()}//引用bean中的方法
#{artistSelector.selectArtist().toUpperCase()}//前一个方法返回String,则还可以继续调用方法
#{artistSelector.selectArtist()?.toUpperCase()}//先判断前一个方法是否返回String,如果是,则调用方法,否则返回null,不调用后续方法
5.2.4 在表达式中使用类型
如果要在SpEL
中访问类作用域的方法和常量的话,要依赖T()
这个关键的运算符:
#{T(java.lang.Math)}
#{T(java.lang.Math).PI}
#{T(java.lang.Math).random()}
5.2.5 SpEL运算符
运算符类型 | 运算符 |
---|---|
算术运算 | +、-、*、/、%、^ |
比较运算 | <、>、==、<=、>=、lt、gt、eq、le、ge |
逻辑运算 | and、or、not、逻辑或 |
条件运算 | ?:(ternary)、?:(Elvis) |
正则表达式 | matches |
5.2.6 计算正则表达式
#{admin.email matches '{[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._]+\\.com}'}
5.2.7 计算集合
#{jukebox.songs[4].title}//引用集合中的一个元素
#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}//随机选取集合中的一个元素
#{'This is a test'[3]}//返回第四个位置的字符's'
5.2.8 查询运算符
#{jukebox.songs.?[artist eq 'Aerosmith']}//查询名字为Aerosmith的歌曲,返回一个集合
#{jukebox.songs.^[artist eq 'Aerosmith']}//查询集合中第一个artist属性为Aerosmith的歌曲
#{jukebox.songs.$[artist eq 'Aerosmith']}//查询集合中最后一个artist属性为Aerosmith的歌曲
#{jukebox.songs.![title]}//不想要歌曲对象的集合,而是所有歌曲名称的集合(没有artist属性的集合),返回一个新集合
注意:以上都是相关的值注入,而对于相关属性的注入是不同的,spring
是没法自动解析如Date
这种类型的,所以我们需要编写相关自定义的属性编辑器,请参看前面小节:《2.spring的IOC容器注入(spring笔记)》
网友评论