介绍
根据Spring提供的多种技巧,实现更为高级的bean装配功能。
环境与profile
配置profile bean
1 | import javax.sql.DataSource; |
这三个版本的相同点是都会生成一个<javax.sql.DataSource的bean>。
XML中配置profile
1 | <?xml version="1.0" encoding="UTF-8"?> |
这里有三个bean,类型都是<javax.sql.DataSource>,并且ID都是dataSource。但运行时只会创建激活状态的profile的bean。
激活
profile激活需要依赖两个独立的属性:
<spring.profiles.active>
先查找该文件,该文件的值就可以确定哪个profile的激活状态,如果该文件没有设置则查找default。
<spring.profiles.default>
一般两个属性组合使用。
@ActiveProfiles注解:指定要激活的profile
1
2
3
4
5
6(SpringJUnit4ClassRunner.class)
(classes=TestConfig.class)
"dev") (
public class Test{
}
条件化bean
@Conditional注解:用在带有@Bean注解的方法上。如果给定的条件计算结果为true,就会创建这个bean,否则,这个bean就会被忽略。例:
条件化配置bean
1 |
|
设置给@Conditional的类可以是任意实现了Condition接口的类型。实现matches方法,返回true或者false。
1 | public class MagicExistsCondition implements Condition { |
上面使用了ConditionContext接口:
1 | public interface ConditionContext{ |
AnnotatedTypeMetadata接口:检查带有@Bean注解的方法上还有什么其他的注解
1 | public interface ConditionContext{ |
@Profile注解实现
从spring4开始,@Profile注解基于@Conditional和Condition实现。
@Profile注解如下所示:
1 | (RetentionPolicy.RUNTIME) |
@Profile注解使用了@Conditional注解,并且引用ProfileCondition作为Condition实现。
1 | public class ProfileCondition implements Condition{ |
ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该信息,它会明确地检查value属性,该属性包含了bean的profile名称,然后,它根据通过ConditionContext得到的Environment来检查该profile是否处于激活状态。
处理自动装配的歧义性
在自动装配时,如果有多个bean匹配结果时,这时就会产生自动装配歧义。Spring会抛出
标示首选的bean
@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在java配置的bean声明中。
1 |
|
xml中使用:
1 | <bean id="iceCream" class="com.desserteater.IceCream" primary="true" /> |
首选项标示多个也有可能产生歧义性。对于解决歧义性问题,限定符更强大。
限定自动装配的bean
@Qualifier注解是使用限定符的主要方式,它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪一个bean。一般将beanID作为限定符。例:
1 |
|
创建自定义限定符
自定义限定符,最佳是为bean选择特性或描述性的术语。需要做的就是在bean声明上添加@Qualifier注解,可以与@Component注解组合使用,然后在注入的地方引用自定义限定符就可以。例:
1 | //声明时指定限定符为“cold” |
当通过java配置显示定义bean时,@Qualifier可以与@Bean注解一起使用。例:
1 |
|
使用自定义的限定符
自定义的面向特性的限定符要比基于beanID的限定符好一些。但如果自定义限定符存在多个的话也是会引起歧义的,而java不允许在同一条目上重复出现相同类型的多个注解(java8允许,只要这个注解本身在定义的时候带有@Repeatable注解就可以,但是Spring的@Qualifier注解并没有在定义时添加@Repeatable注解)。
解决方法是创建自定义限定符注解,它本身要使用@Qualifier注解来标注。例@Qualifier(“cold”),我们使用自定义的@Cold注解,该注解定义为:
1 | ({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) |
同样的,可以创建@Creamy注解
1 | ({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE}) |
使用时,在声明中添加上多个自定义限定符注解,注入时引用即可:
1 | //IceCream声明 |
bean的作用域
在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。但如果你使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。这种情况将class声明为单例是不合理的,因为对象会被污染。
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
作用域 | 描述 |
---|---|
单例(Singleton) | 在整个应用中,只创建bean的一个实例 |
原型(Prototype) | 每次注入或者通过spring应用上下文获取,都会创建一个新的bean实例 |
会话(Session) | 在Web应用中,为每个会话创建一个bean实例 |
请求(Request) | 在Web应用中,为每个请求创建一个bean实例 |
可以使用@Scope注解设置bean的作用域。
组件扫描声明bean:
1
2
3
4
5
(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{
}
//也可以使用@Scope("prototype"),这样容易出错,SCOPE_PROTOTYPE是类型安全的java配置中声明bean
1
2
3
4
5
(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
return new Notepad();
}xml配置中声明bean
1
<bean id="notepad" class="com.spring.Notepad" scope="prototype" />
使用会话和请求作用域
就购物车bean来说,会话作用域最合适,它与给定的用户关联性最大。可以使用@Scope注解:
1 |
|
上面@Scope注解还有用到一个属性proxyMode,表示作用域代理。理解下这个会话作用域。例:
1 |
|
因为StoreService是一个单例的bean,会在spring应用上下文加载的时候创建。当它创建时,spring会试图将ShoppingCart注入到setShoppingCart()方法中。但是ShoppingCart bean是会话作用域的,此时并不存在,直到用户进入创建了会话才会出现ShoppingCart 的实例,此外,ShoppingCart 的实例有多个,我们不会让spring注入某个固定的ShoppingCart 的实例,我们希望注入的ShoppingCart 的实例恰好就是当前会话所对应的那一个。
Spring会注入一个ShoppingCart bean的代理,如下图:
这个代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车,但当StoreService调用ShoppingCart 的方法时,代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。
理解了上面作用域,下面看下proxyMode这个属性:
这个属性解决了将会话或请求的bean注入到单例bean中所遇到的问题。
- ScopedProxyMode.INTERFFACES:表明这个代理要实现ShoppingCart 接口,并将调用委托给实现bean。
- 如果ShoppingCart 是具体类的话,spring没办法创建基于接口的代理,它必须使用CGLib来生成基于类的代理。所以必须将proxyMode属性设置为ScopedProxyMode.TARGET_CLASS,表明要以生成目标类扩展的方式创建代理。
在xml中声明作用域代理
在xml中设置代理模式,需要使用Spring aop命名空间的一个新元素:
1 | <bean id="cart" class="com.spring.ShoppingCart" scope="session"> |
使用aop:scope-proxy元素,需要在XML配置中声明Spring的aop命名空间:
1 | <?xml version="1.0" encoding="UTF-8"?> |
运行时值注入
为了避免硬编码,让硬编码的值在运行时再确定,Spring提供了两种运行时求值的方式:
- 属性占位符(Property placeholder)
- Spring表达式语言(SpEL)
属性占位符
在Spring中,处理外部值最简单的方式就是声明属性源并通过Spring的Environment来检索属性。例:一个基本的Spring配置类:
1 | /* |
@PropertySource引用了一个app.properties文件:
1 | disc.title=Sgt. Peppers Lonely Hearts Club Band |
这个属性文件会加载到Spring的Environment中,可以调用getProperty()方法得到属性值。
深入学习Spring的Environment
1 | /* |
解析属性占位符
在Spring装配中,占位符的形式为使用“${…}”包装的属性名称。例:
XML中使用:
1 | /* |
依赖组件扫描和自动装配:
1 | /* |
Spring表达式语言
Spring表达式语言(Spring Expression Language, SpEL),它能够以一种强大和简介的方式将值装配到bean属性和构造器参数中,这个表达式会在运行时计算得到值。SpEL拥有很多特性,包括:
- 使用bean的ID来引用bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 正则表达式匹配;
- 集合操作。
SpEL样例
SpEL表达式要放到#{…}之中,例:
1 | #{1} //数字常量,值为1 |
SpEL表达式也可以引用其他bean或其他bean的属性。引用系统属性。例:
1 | #{sgtPeppers.artist}//ID为sgtPeppers的bean的artist属性 |
bean装配时使用表达式:
通过组件扫描bean,在注入属性和构造器参数时,使用@Value注解。例:
1
2
3
4
5
6//从系统属性中获取专辑和艺术家名字
public BlankDisc(@Value("#{systemProperties['disc.title']}") String title,
@Value("#{systemProperties['disc.artist']}") String artist) {
this.title=title;
this.artist=artist;
}在XML中,将SpEL表达式传入
或 的value属性中,或者将其作为p-命名空间或c-命名空间条目的值。例: 1
2
3
4<bean id="sgtPeppers" class="com.spring.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.artist']}"
/>
下面学习SpEL所支持的基础表达式。
表示字面值
SpEL不仅可以表示整数字面量,还可以表示浮点数、String值以及Boolean值。例:
1 | #{3.1415} //浮点数 |
引用bean、属性和方法
1 | //引用bean-将一个bean装配到另外一个bean的属性中,使用bean ID作为表达式 |
在表达式中使用类型
如果在SpEL中访问类作用域的方法和常量的话,要依赖T()这个关键运算符。T()运算符的结果表示一个Class对象。例:
1 | //代表java.lang.Math类 |
SpEL运算符
用来操作表达式值的SpEL运算符:
运算符类型 | 运算符 | |
---|---|---|
算术运算 | +、-、*、/、%、^ | |
比较运算 | <、>、==、<=、>=、it、gt、eq、le、ge | |
逻辑运算 | and、or、not、\ | |
条件运算 | ?: (ternary)、?: (Elvis) | |
正则表达式 | matches |
例:
1 | //计算circle bean所定义的圆的周长 |
计算正则表达式
SpEL通过matches运算符支持表达式中的模式匹配。例:
1 | #{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.com'} |
计算集合
SpEL可以操作集合和数组。例:
引用列表中的一个元素:
1
#{jukebox.songs[4].title} //计算集合中第五个元素
随机播放歌曲:
1
2//[]运算符用来从集合或数组中按照索引获取元素
#{jukebox.songs[T(java.lang.Math).random() * jukebox.songs.size()].title}从String中获取一个字符:
1
#{'This is a test'[3]}//获得“s”
查询运算符(.?[]),对集合进行过滤,得到集合的一个子集:
1
2//得到jukebox中artist属性为Aerosmith的所有歌曲
#{jukebox.songs.?[artist eq 'Aerosmith']}查询运算符(.^[])和(.$[])分别用来在集合中查询第一个匹配项和最后一匹配项:
1
2//查找列表中第一个artist属性为Aerosmith的歌曲
#{jukebox.songs.^[artist eq 'Aerosmith']}投影运算符(.![]),从集合的每个成员中选择特定的属性放到另外一个集合中。例:
1
2//获得所有歌曲名称的集合,而不是所有歌曲对象的集合
#{jukebox.songs.![title]}实际上,投影运算符可以与其他任意的SpEL运算符一起使用。例:使用如下表达式获得Aerosmith所有歌曲的名称列表:
1
#{jukebox.songs.?[artist eq 'Aerosmith'].![title]}