一、环境与profile
开发环境和QA环境,很多时候需要不同的配置。即在不同的环境中某个bean会有所不同,我们必须用某种方法来配置这个bean,使其在每种环境下都会选择最为合适的配置。其中一种方式是在单独的配置类(或者XML文件)中配置每个bean,然后在构建阶段(可能用到Maven的profiles)确定将哪个配置编译到可部署的应用中。
配置profile bean
利用Spring配置时,需根据环境决定该创建哪个bean和不创建哪个bean,而且Spring是在运行时做出这样的决策。这样的好处是,同一个部署单元能够适用所有的环境,没有必要进行重新构建。
要使用profile,先要将不同的bean定义整理到一个或多个profile之中,再将应用部署到每个环境时,要确保对应的profile处于激活(active)的状态。
在Java配置中,可以用@Profile注解指定某个bean属于哪一个profile。
@Profile(“dev”)该注解应用在类级别上,它会告诉Spring这个配置类中的bean只有在dev profile激活时才会创建,否则带有bean注解的方法都会被忽略掉。
但是从Spring 3.2开始,也可以在方法级别上使用@Profile注解,与@Bean注解一同使用。但是如果同一个类中的有的bean并没有声明在一个给定的profile范围内,那么这些类始终都会被创建,与激活哪个profile没有关系。
在XML中配置profile
我们可以通过<beans>元素的profile属性,在XML中配置profile bean,只有当profile属性与当前激活profile相匹配时,配置文件才会被用到。
还可以在根<beans>元素中嵌套定义<beans>元素,而不是为每个环境都创建一个profile XML文件,能够将所有的profile bean的定义放到一个XML文件中。
激活profile
Spring在确定哪个profile处于激活状态时,需要依赖两个独立的属性:Spring.profiles.active 和 Spring.profiles.default。如果设置了Spring.profiles.active 属性的话,那么它的值就会用来确定哪个profile是激活的。但若没设置该属性的话,那么Spring就会查找Spring.profiles.default 的值。如果两者都没有设置的话,那么就没有激活profile,因此只会创建那些没有定义在profile中的bean。
有多种方式来设置这两个属性:
- 作为DispatcherServlet的初始化参数;
- 作为Web应用的上下文参数;
- 作为JNDI条目;
- 作为环境变量;
- 作为JVM的系统属性;
- 在集成测试类上,使用@ActiveProfile注解设置。
可以同时激活多个profile,即可以通过列出多个profile名称并以逗号间隔来实现(当你设置多个彼此不相关的profile)。
使用profile进行测试
当运行集成测试时,通常希望采用与生产环境相同或者生产环境的部分子集的配置进行测试。但是若配置中的bean定义在profile中,在运行测试时,我们需要一种方式来启动合适的profile。
可以通过@ActiveProfiles注解来指定运行测试时要激活哪个profile。
二、条件化的bean
Spring 4 引入了一个新的@Conditional注解,它可以用到带有@Bean 注解的方法上,如果给定的条件为true,就会创建这个bean,否则的话,这个bean会被忽略。
@Bean
@Conditional(MagicExistsCondution.class)
public MagicBean magicBean(){
return new MagicBean();
}
public interface Condition{
boolean matches(ConditionContext ctxt,AnnotatedTypeMetadata metadata){
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
@Conditional接口会通过Condition接口进行对比。这个接口只需提供matches()方法的实现即可。如果matches()方法返回true,那么就会创建带有@Conditional注解的bean,否则将不会创建这些bean。
ConditionContext接口:
public interface ConditionContext{
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
- 借助getRegistry()返回的BeanDefinitionRegistry检查bean定义;
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性。
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么;
- 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata接口:
public interface AnnotatedTypeMetadata{
boolean isAnnotated(String annotationType);
Map<String,Object> getAnnotationAttributes(String annotationType);
Map<String,Object> getAnnotationAttributes(String annotationType,boolean classValueAsString);
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType);
MultiValueMap<String,Object> getAllAnnotationAttributes(String annotationType,boolean classValueAsString);
}
借助isAnnotated()方法,能够判断带有@Bean注解的方法是不是还有其他特定的注解。
从Spring 4 开始,@Profile注解进行了重构,使其基于@Conditional和Condition实现。
@Profile注解如下所示:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile{
String[] value();
}
@Profile 本身也使用了@Condition注解,并且引用ProfileCondition作为Condition实现。
class ProfileCondition implements Condition{
public boolean matches(
ConditionContext context,AnnotatedTypeMetadata metadata){
if(context.getEnvironment() != null){
MultiValueMap<String,Object> attrs =
metadata.getAllAnnotationAttributes(Profile.class.getName());
if(attrs != null){
for(Object value : attrs.get("value")){
if(context.getEnvironment().acceptsProfiles(((String []) value))){
return true;
}
}
return false;
}
}
return true;
}
}
ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助value属性获得bean的profile名称。然后根据通过ConditionContext达到的Environment来检查该profile是否处于激活状态。
三、处理自动装配的歧义性
如果不仅有一个bean能够匹配结果的话,这种歧义性会阻碍Spring自动装配属性、构造器参数或方法参数。
Spring不知道将哪个装配进去,只好宣告失败并抛出异常。
可以将可选bean中的某一个设为首选的bean,或者使用限定符来帮助Spring将可选的bean的范围缩小到只有一个bean。
标示首选的bean
在声明bean的时候,通过将其中一个可选的bean设置为首选bean能够避免自动装配时的歧义性。当遇到歧义性的时候,Spring将会使用首选的bean,而不是其他可选的bean。
可以用@Primary来设置首选bean。@Primary能够与@Component组合用在组件扫描的bean上,也可以与@Bean组合用在Java配置的声明中。
利用@Component注解进行配置
@Component
@Primary
public class Dog implements Animal{ . . . }
利用Java配置显式地声明
@Bean
@Primary
public Animal dog(){
return new Dog();
}
用XML配置bean
<bean id="dog" class="com.Animal.Dog" primary="true" />
无论用哪种方式来标示首选bean,效果都是一样的。但是如果标示了两个或者更多的首选bean,那么它就无法正常工作了,因为这时出现了新的歧义性,Spring不知道如果做出选择。
限定自动装配的 bean
标示首选bean只能标示一个优先的可选方案,当首选bean的数量超过一个时,没有其他方法进一步缩小可选范围。
这时就要用到限定符了,Spring的限定符能够在所有可选的bean上进行缩小范围操作,最终能够达到只有一个bean满足所规定的限制条件。如果将所有的限定符都用上后依然存在歧义性,可以继续使用更多的限定符来缩小选择范围。
@Qualifier 注解是使用限定符的主要方式。它可以与@Autowired和@Inject协同使用,在注入的时候指定想要注入进去的是哪个bean。
@Autowired
@Qualifier("dog")
public void setAnimal(Animal animal){
this.animal = animal;
}
为@Qualifier 注解所设置的参数就是想要注入的bean的ID。更准确地说,@Qualifier(“dog”)所引用的bean要具有String类型的“dog”限定符。在bean中,如果没有其他限定符的话,所有的bean都会给一个默认的限定符,这个限定符与bean的ID相同。
在setAnimal()方法上所指定的限定符与要注入的bean的名称是紧耦合的,对类名称的任意改动都会导致限定符失效。
创建自定义限定符
可以为bean设置自己的限定符,而不是依赖于将bean ID作为限定符。
这里要在bean声明上添加@Qualifier注解
@Component
@Qualifier("faith")
public class dog implements Animal {...}
这时,在注入的地方,只要引用faith限定符就可以了。
@Autowired
@Qualifier("faith")
public void setAnimal(Animal animal){
this.animal = animal;
}
在通过Java配置显式定义bean定义bean的时候,@Qualifier也可以与@Bean注解一起使用。
@Bean
@Qualifier("faith")
public Animal dog(){
return new Dog();
}
当使用自定义Qualifier值时,最佳的方式是为bean选择特征性或描述性术语,而不是随意的名字。
使用自定义的限定符注解
面向特性的限定的限定符要比基于bean ID的限定符更好一些。但是如果多个bean具备相同的特性的话,这样也会产生歧义性。
或许你认为可以在@Qualifier注解后面再加一个@Qualifier注解来进一步缩小范围,但是Java不允许在同一个条目上重复出现相同类型的多个注解,如果有多个相同的注解,编译器会报错。使用@Qualifier注解并没有办法将自动装配的可选bean缩小范围至仅有一个可选的bean。
这种情况下,要创建自定义注解,借助这样的注解来表达bean所希望限定的特性。创建的这个注解,它本身要使用@Qualifier注解来标注,比如我们定义一个Faith注解,如下所示:
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
@public @interface Faith{ }
同理,可以再定义一个Lovely注解。
使用方式为:
@Component
@Faith
@Lovely
public class Dog implements Animal(Animal animal){
this.animal = animal;
}
通过声明自定义的限定符注解,我们可以同时使用多个限定符,不会再有Java编译器的限制或者错误。而且相对于使用原始的@Qualifier并借助String类型来指定限定符,自定义的注解也更为类型安全。
四、bean的作用域
Spring应用上下文中所有的bean默认都是以单例的形式创建的。但是如果所使用的类是易变的,会保持一些状态,这是使用单例的bean时不安全的,bean对象是会被污染的。
Spring中的作用域
- 单例(Singleton):在整个应用中,只创建一个bean实例。
- 原型(Prototype):每次注入或通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
- 会话(Session):在Web应用中,为每个会话创建一个bean实例。
- 请求(Request):在Web应用中,为每个请求创建一个bean实例。
单例是默认的作用域,如果要选择其他的作用域,可以用@Scope注解,它可以与@Component或@Bean一起使用。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {...}
这里使用ConfigurableBeanFactory类中SCOPE_PROTOTYPE常量设置了原型作用域。也可以使用@Scope(“prototype”),但是使用SCOPE_PROTOTYPE更加安全不易出错。
同理,与@Bean组合使用
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad(){
return new Notepad();
}
XML
<bean id="notepad" class="com.myapp.Notepad" scope="prototype"/>
不管使用哪种方式来声明原型作用域,每次主语或从Spring应用上下文检索该bean时,都会创建新的实例。
使用会话和请求作用域
在的购物网站中,可能会有一个bean代表用户的购物车。如果购物车是单例的话,那么所有用户都会向同一个购物车中添加商品;如果购物车时原型作用域的,那么在应用中某个地方添加商品,在应用的另一个地方是不可用的,每次注入的购物车都是独立的。
对于这样的一个需求,会话作用域是最适合的,它与给定的用户关联性最大,要指定会话作用域,我们可以使用@Scope注解:
@Component
@Scope(value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart{...}
Spring为Web应用中每个会话创建一个ShoppingCart,这会创建多个ShoppingCart bean实例,但是对于给定的会话,只会创建一个实例,即在该会话中,bean相当于是单例的。
我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中,如下所示:
@Component
public class StoreService{
@Autowired
public void setShoppingCart(ShoppingCart shoppingCart){
this.shoppingCart = shoppingCart;
}
...
}
StoreService是单例的bean,会在Spring应用上下文加载的时候创建。当它创建的时候,Spring会试图将ShoppingCart注入到setShoppingCart()方法中。但是直到某个用户进入了系统,创建了会话之后,才会出现ShoppingCart实例。
我们不希望StoreService中是一个固定的ShoppingCart;而是希望StoreService处理购物车功能时,它所使用的ShoppingCart恰好是当前会话中对应的那一个。
事实上,Spring会注入一个到ShoppingCart的代理,该代理会暴露与ShoppingCart相同的方法,所以StoreService会认为它就是一个购物车。当调用这些方法时,代理会对其进行懒解析并将调用委托给当前会话作用域中真正的ShoppingCart bean。
proxyMode属性被设置成ScopedProxyMode.INTERFACES,这表明代理要实现ShoppingCart接口,并将调用委托给实现bean。但是如果ShoppingCart是一个具体的类,这时必须使用CGLib来生成基于类的代理,要将proxyMode属性设置成ScopedProxyMode.TARGET_CLASS,来声明要以生成目标类扩展的方式创建代理。
同理
请求作用域的bean应该也以作用域代理的方式进行注入。
作用域代理能够延迟注入请求和会话作用域的bean
在XML中声明作用域代理
<bean>元素的scope属性能够设置bean的作用域,但是不能直接指定代理模式。需要用到Spring aop:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session" >
<aop:scoped-proxy />
</bean>
<aop:scoped-proxy>会告诉Spring为bean创建一个作用域代理,默认情况下,会使用CGLib创建目标类的代理。但是也可以将proxy-target-class 属性设置为false,进而要求它生成基于接口的代理:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session" >
<aop:scoped-proxy proxy-target-class = "false" />
</bean>
为了使用<aop:scoped-proxy>,必须在XML配置中声明Spring的aop命名空间。
五、运行时值注入
依赖注入通常是将一个bean引用注入到另一个bean的属性或构造器参数中,通常讲的是将一个对象与另一个对象进行关联。
但是bean装配的另一个方面是将一个值注入到bean的属性或者构造器参数中。
利用硬编码可以实现一些需求,但是有时更希望这些值在运行时再确定。为此,Spring提供了两种方式:
- 属性占位符
- Spring表达式语言
注入外部的值
在Spring中,处理外部值的最简单方式就是声明属性源并通过Spring的Environment来检索属性。
@PropertySource要引用一个properties文件,该文件的大致内容为:
disc.title=test
disc.artist=Jay
这个属性文件会加载到Spring的Environment中,稍后可以用getProperty()来实现的。
深入学习Spring的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)
前两种形式的getProperty()方法都会返回String类型的值,而且第二个方法可以在指定属性不存在时,会使用一个默认值。
而后面两种方法不会简化所有的值都视为String类型。如果你想要获取的值代表的含义是连接池中所维持的连接数量,需要将String转化为Integer类型。
int connectionCount = env.getProperty("db.connection.count",Integer.class,30);
getRequiredProperty()要求获取的属性必须要有定义,否则会抛出IllegalStateException异常。
containsProperty()检查某一个属性是否存在。
getPropertyAsClass()可以将属性解析为类。
class<CompactDisc> cdClass = env.getPropertyAsClass("disc.class",CompactDisc.class)
关于profile的一些方法:
- String[] getActiveProfiles():返回激活profile名称的数组;
- String[] getDefaultProfiles():返回默认profile名称的数组;
- boolean acceptsProfiles(String … profiles):如果environment支持给定的profile,就返回true。在bean创建之前,使用acceptsProfiles()方法来确保给定bean所需的profile处于激活状态。
解析属性占位符
Spring一直支持将属性定义到外部的属性文件中,并使用占位符值将其插入到Spring bean中。在Spring装配中,占位符的形式为使用“${…}” 包装属性名称。
<bean id="sgtPeppers"
class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}" />
title和artist参数所给定的值都是从一个属性中解析得到的。以这种方式,XML配置没有使用任何硬编码设置,值都是从配置文件以外解析得到的。
如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。这时,我们可以使用@Value注解
public BlankDisc(
@value("${disc.title}")String title,
@value("${disc.artist}") String artist){
this.title = title;
this.artist = artist;
}
要想使用占位符,必须要配置一个PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用后者,以为它能够基于Spring Environment及其属性源来解析占位符。
在Java中配置PropertySourcesholderConfigurer
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
在XML中配置
Spring Context命名空间中的元素会为你生成PropertySourcesPlaceholderConfigurer bean。
使用Spring表达式语言进行装配
Spring表达式语言(SpEL)能够以一种强大和简洁的方式将值装配到bean属性和构造器参数中,在这个过程中所使用的表达式会在运行时计算得到的值。
特性:
- 使用bean的ID来引用bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 正则表达式匹配;
- 集合操作。
SpEL样例
SpEL表达式要放到“#{ … }”中。
#{1}数字常量1
#{T(System).currentTimeMillis()}计算表达式的那一刻当前时间的毫秒数。T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用其static修饰的currentTimeMillis()方法。
#{A.B}计算得到ID为A的bean的B属性
#{systemProperties[‘disc.title’]}通过systemProperties对象引用系统属性
在bean装配时使用这些表达式
使用组件扫描的话,在注入属性和构造器参数时,可使用@Value注解,与属性占位符很类似。
@Bean
public BlankDisc(
@value("#{systemProperties['disc.title']}")String title,
@value("#{systemProperties['disc.artist']}") String artist){
this.title = title;
this.artist = artist;
}
XML
可以将SpEL表达式传入<property>或<constructor-arg>的value属性中,或者将其作为p-命名空间或c-命名空间条目的值。
表示字面值
SpEL不仅可以表示整数,还可以表示浮点数、String值以及Boolean值。
#{3.1415926}
#{9.8E4}科学计数法,结果为98700
#{‘Hello World’}
#{false}
引用bean、属性和方法
通过ID引用其他bean。
一个bean ID为sgtPeppers
#{sgtPeppers}
#{sgtPeppers.artist}
除了调用bean及其属性,还可以调用bean上的方法。对于被调用方法的返回值,我们同样可以调用它的方法。为了避免出现NullPointerException,可以使用类型安全的运算符,即返回值为空的话,不会调用后面的方法:
#{artistSelector.selectArtist()?.toUpperCase()}
在表达式中使用类型
通过T()运算符方法类作用的方法和常量。如果要使用Java的Math类,只需这样使用运算符:T(java.lang.Math)。我们甚至可以把它装配到一个Class类型的bean属性,但是T()的真正的价值在于它能够访问目标类型的静态方法和常量。
SpEL运算符
SpEL的运算符可以用在SpEL表达式上,运算符如下所示:
运算符类型 | 运算符 |
---|---|
算术运算 | +、-、*、/、%、^ |
比较运算 | <、>、==、<=、>=、lt、gt、eq、le、ge、 |
逻辑运算 | and、or、not、| |
条件运算 | ?: (ternary)、?: (Elvis) |
正则表达式 | matches |
- 计算圆的面积 #{T(java.lang.Math).PI * circle.radius ^ 2}
当使用String类型时,’+’运算执行的是连接操作。
比较两个两个数字是否相等,可以使用双等号运算符(==)或文本型的eq运算符。计算结果是个Boolean值,相等为true,否则为false。
SpEL中的三元运算符与Java中的三元运算符非常相似。#{scoreboard.score > 1000 ? “winner” : “loser”}如果scoreboard.score > 1000 的话,计算结果为String类型的”winner”,否则结果为”loser”。同时,三元运算符还可以检查null值,并使用一个默认值来代替null。#{disc.title ?: ‘DJ’}即是判断disc.title的值是不是null,如果时null的话,结果就是”DJ”。
计算正则表达式
SpEL通过matches运算符支持表达式中的模式匹配。matches运算符对String类型的文本应用正则表达式,如果相匹配,返回true;否则返回false。
计算集合
- 引用列表中的一个元素:#{jukebox.songs[4].title}计算songs集合中第五个元素的title属性,该集合来源于ID为jukebox bean。也可以从集合中随机选取一首歌:
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
可以利用”[]”从String中获取一个字符:#{‘This’[3]}引用该String中的第四个字符,也就是”s”。
查询运算符(.?[ ]):对集合进行过滤,得到集合的一个子集。”.\^[ ]”和”.$[ ]”分别用来在集合中查询第一个匹配项和最后一个匹配项。#{jukebox.songs.?[artist eq ‘Jay’]}即查询得到Jay的所有歌曲。选择运算符在它的方括号内接受另一个表达式,迭代列表时,每一个条目都计算这个表达式,如果表达式为true将该条目放到新集合中,否则不会放到新集合中。
投影运算符(.![ ]):从集合的每个成员中选择特定的属性放到另外一个集合。#{jukebox.songs.?[artist eq ‘Jay’].![title]}即用来获取Jay所有歌曲的名称列表。