3.1环境与profile
开发环境、测试环境、生产环境可能需要的配置都不一样。
一种方案是通过XML文件分别来配置不同环境对应的bean,在系统构建的时候,需要什么配置就加载对应的文件。但是这种方式在测试阶段迁移到生产阶段时,重新构建可能还是会出现bug。
3.1.1配置profile bean
Spring采用的方式类似于上面,也是根据环境需要创建对应的bean。但是Spring不是在构建的时候确定,而是在运行的时候才确定。这样保证了同一个部署文件(比如war)能够适用于所有的环境,不再需要重新构建。
Spring3.1开始引入了Bean profile功能,可以将不同的bean定义整理到一个或者多个profile中,部署到不同的环境时,激活对应profile下的bean。
@Profile注解指定了bean属于哪一个profile。Spring3.1中,@Profile只能声明在类上面,这意味着这个类下的所有@Bean都将一同激活。Spring3.2支持@Profile声明在方法上面。
没有用@Profile指定的Bean在任何情况下都会被创建
在XML中配置profile
在标签中指定profile属性就可以为整个XML配置Profile,如下图指定了profile为"dev"。
在JavaConfig中可以为一个配置中的bean单独指定Profile。同样xml配置也是可以做到的。在根元素中嵌套并且为嵌套的指定profile属性,就能灵活配置bean。
<beans xmln="..." ...
xsi:schemaLocation="... ...">
<!-- 为dev profile装配的bean -->
<beans profile="dev">
<bean id="...">
</bean>
</beans>
<!-- 为prod profile装配的bean -->
<beans profile="">
<bean id="">
</bean>
</beans>
</beans>
3.1.2 激活profile
Spring通过两个对立的属性spring.profiles.active和spring.profiles.default来确定哪profile处于激活状态。
- 首先如果一个profile设置了spring.profiles.active,那么他将会被激活。
- 如果他没有设置spring.profiles.active,spring会检查它是否设置了spring.profiles.default值,如果设置了,同样激活。
- 两个值都没有设置的profile不会被激活。
当然,可以有很多种方式来设置这两个值
- 作为DispatcherServlet的初始化参数
- 作为Web引用的上下文参数
- 作为JNDI条目
- 作为环境变量
- 作为JVM的系统属性
- 在集成测试类上 ,使用@ActivityProfiles注解设置
在web.xml中可以这样指定上下文默认的profile
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
然后在servlet中可以这样设置默认的profile
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>
org.springframwork.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
这样设置之后,开发人员可以使用spring.profiles.default配置开发环境,当项目投入生产时,部署人员可以使用系统属性、环境变量或者JNDI设置spring.profiles.active配置profiles,开发过程中的spring.profiles.default会被覆盖掉。
使用profile进行测试
当进行测试时,使用@ActivityProfiles注解指定测试时激活哪一个profile。
下面测试代码使用@ActivityProfiles激活了dev profiles:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={PersistenceTestConfig.class}
@ActivityProfiles("dev")
public class PersistenceTest(){
...
}
3.2 条件化的bean
有时候会需要在特定的条件下创建一些bean,比如希望一些bean在应用的类路径下包含特定的库时才创建。或者希望一些bean只有当其他某个特定的bean声明了之后才会创建,抑或是希望只有某个特定的环境变量设置之后,才会创建某个bean。
Spring4之前很难实现这样的条件话配置。Spring4的@Conditional注解可以加在@Bean注解的方法上,如果给定的条件计算结果为true,就会创建这个Bean,反之,会忽略他。
这里的MagicBean使用了@Conditional指定了一个类MagicExistsCondition。这个类包含了能否创建MagicBean的条件。
@Bean
@Conditional(MagicExistsCondition.class)
public MagicBean magicBean(){
return new MagicBean();
}
MagicExistsCondition类实现了condition接口,condition接口只有一个matches方法,返回boolean。
public interface Condition{
boolean matches(conditionContext ctxt,AnotatedTypeMetadata metadata)
}
MagicExistsCondition实现了这样的功能,检查环境中是否存在magic属性,存在返回true,反之返回false。
所以,MagicBean的创建条件就是:环境中存在magic属性,创建该Bean,反之屏蔽。
public class MagicExistsCondition implements Condition{
public boolean matches(
conditionContext context,AnnotatedTypeMetadata metadata){
Environment env = context.getEnvironment();
//检查magic属性
return env.containsProperty("magic");
}
}
matches方法中的ConditionContext是一个接口
public interface ConditionContext{
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
通过ConditionContext这个接口,可以做到:
- 借助getRegistry()返回的BeanDefinitionRegistry检查Bean定义
- 借助getBeanFactory()返回的ConfigurableListableBeanFactory检查bean是否存在,甚至探查bean的属性。
- 借助getEnvironment()返回的Environment检查环境变量是否存在以及它的值是什么; * 读取并探查getResourceLoader()返回的ResourceLoader所加载的资源;
- 借助getClassLoader()返回的ClassLoader加载并检查类是否存在。
AnnotatedTypeMetadata则能够让我们检查带有@Bean注解的方法上还有什么其他的注解。像ConditionContext一样,它也是一个接口。
public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType);
Map<String, Object> getAnnotationAttributes(String annotationType, boolean classValuesAsString);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType);
MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationType, boolean classValuesAsString);
}
通过isAnnotated()方法,能判断@Bean注解的方法是不是还有其他特定的注解,借助其他方法可以检查@Bean注解的方法上是否还有其他注解的属性。
Spring4开始,@Profile基于@conditional和Condition进行了重构。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @Interface Profile{
String[] value();
}
@profile本身也是用了@Conditional注解。引用了ProfileCondition作为实现。
ProfileCondition.class会决定Profile是否可用
class ProfileCondition implements Condition {
ProfileCondition() {
}
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
Iterator var4 = ((List)attrs.get("value")).iterator();
Object value;
do {
if (!var4.hasNext()) {
return false;
}
value = var4.next();
} while(!context.getEnvironment().acceptsProfiles((String[])((String[])value)));
return true;
}
}
return true;
}
}
ProfileCondition通过AnnotatedTypeMetadata得到了用于@Profile注解的所有属性。借助该属性,它会明确的检查value属性,该属性包含了bean的profile名称。然后,它会根据ConditionContext得到的Environment[借助acceptsProfiles()方法]来检查该file是否处于激活状态。
3.3 处理自动装配的歧义性
自动装配时可能会同时有多个bean满足条件,Spring会抛出NoUniqueBeanDefinitionException异常。 怎样消除这种情况出现的歧义呢?
- 可以将bean中某一个设为首选(primary)的bean
- 使用限定符(qualifier)来将bean的选择范围缩小到只有一个
3.3.1 标示首选的bean
假设有这样几个类
@Component
public class Cake implements Dessert{...}
@Component
public class IceCream implements Dessert{...}
这两个类当Spring进行自动装配时会出现问题,因为他们注入的是同一个接口Dessert
这时候使用了@Primary对某一个实现类进行申明,比如
@Component
@Primary
public class Cake implements Dessert{...}
那么Spring在自动装配的时候就会使用Cake。
但是,如果这时候如果你给IceCream也加上了@Primary注解,又会出现歧义导致报错。这时候可以使用限定符,限定符的功能更强大。
3.3.2 限定自动装配的bean
@Qualifier注解可以直接指定某个具体的Bean的Id,这样就可以将bean锁定到一个,而@Primary可能无法锁定到只有一个bean。
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.dessert = dessert
}
当@Qualifier(“iceCream”)声明之后,Spring会在容器中查找id为iceCream的bean进行装配。
Spring默认会给IceCream声明一个id为iceCream,但是如果你为它指明了其他的id,@Qualifier的值当然也要换成你指明的id。
当然,你也可以不使用bean的id进行装配,@Qualifier注解同样可以使用在你需要装配的bean上,同时为它指明一个装配时使用的值。
假设这里为IceCream用@Qualifier指明一个值"cold"。那么使用@Qualifier(“cold”)进行装配时,就会匹配到IceCream这个bean。
@Component
@Qualifier("cold")
public class IceCream implements Dessert{...}
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.dessert = dessert
}
那么问题来了,假设Cake这时候也使用了@Qualifier(“cold”),又会出现装配歧义。这个时候@Qualifier无法将bean锁定到只有一个。
这时候可以使用自定义的注解,下面声明了一个@Cold。当自定义的注解上面声明了@Qualifier注解后,它本身就成为了一个限定符注解。
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,
ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @Interface Cold{ }
同样的,可以创建一个@Creamy注解。这时候就可以在一个bean上声明多个限定符注解,比如:
@Component
@Cold
@Creamy
public class IceCream Implements Dessert{...}
这时候就可以装配同时使用@Cold和@Creamy定位IceCream这一个bean。同理可以创建更多的限定符注解面对更复杂的情况。
不能同时使用@Qualifier(“Cold”)+@Qualifier(“Creamy”)来实现定位。因为java不允许同样的注解在一个条目上出现两次。
使用自定义注解类型更加安全,因为@Qualifier使用的是String类型来指定限定符。
3.4 bean的作用域
默认情况下,bean都是作为单例形式创建的。所以一个bean无论被注入多少次,每次注入的都是同一个实例。
大部分情况下,单例bean是很理想的方案。但是也有不合适的时候,有些类可能会发生变化,不能重用。
Spring定义了多个作用域,包括:
- 单例(Singleton):在整个应用中,只创建bean的一个实例
- 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建bean一个新的实例
- 会话(Session):在Web应用中,为每个会话创建一个bean实例
- 请求(Request):在Web应用中,为每个请求创建一个bean实例
如果你不想使用单例作为作用域,要使用@Scope注解。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad{...}
这里使用了ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置作用域为原型。等效于@Scope(“protorype”),但是SCOPE_PROTOTYPE更加安全,不易出错。
在XML文件中配置
<bean id="notepad" class="com.myapp.Notepad" scope="prototype"/>
3.4.1 使用会话和请求作用域
这里声明一个bean,将其作用域声明为了SESSION,在一个对话中,这个bean相当于是单例的。这里设置了一个proxyMode属性,Spring在注入是,不会真的将这个类的实例进行注入,而是注入一个这个类的代理。proxyMode属性为ScopedProxyMode.INTERFACE,表示这个代理要实现ShoppingCart接口。如果ShoppingCart不是接口而是类的话,Spring无法创建基于接口的代理,此时,必须使用CGLib来生成类的代理,proxyMode = ScopedProxyMode.TARGET_CLASS。请求(request)作用域和会话(SESSION)情况相同。
@Component
@Scope(
value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.INTERFACE)
public ShoppingCart cart(){...}
3.4.2 在XML中声明作用域代理
在XML中设置代理模式,需要使用Spring AOP命名空间的一个新元素:
<bean id="card" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy/>
</bean>
aop:scoped-proxy让Spring为bean创建一个作用域代理。默认情况下shiyongCGLib创建目标类的代理。也可以将proxy-target-class属性设置为false,让它生成基于接口的代理。
<bean id="card" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
3.5 运行时注入
装配bean时,可能需要动态的值,这些值是运行后才确定的。Spring提供了两种方案:
- 属性占位符(Property placeholder)
- Spring表达式(SpEL)
3.5.1 注入外部的值
最直接的方式是声明属性源并通过Spring的Environment来检索属性。
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties") //声明属性源
public class ExpressiveConfig{
@Autowired
Environment env;
@Bean
public BlankDisc disc(){
return newBlankDisc(
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中,可以通过它的getProperty()来获得值。
Environment
getProperty()在Environment中有四种重载形式
String getProperty(String key)
//声明一个个默认的值defaultvalue,当指定的的属性不存在的时候,使用默认值
String getProperty(String key,String defaultValue)
//指定类型
T getProperty(String key,Class<T> type)
//指定类型,并且同时指定一个默认值
T getProperty(String key,Class<T> type,T defaultValue)
如果你使用getProperty没有取得值,也没有声明默认值,就会自动设置值为null。为保证值不为null,可以使用getRequiredProperty方法,该方法和getProperty使用方法相同,不同的是,该方法没有设置值得时候,不会自动设置null,而是抛出IllegalStateExcption异常。
如果想检查某个属性是否存在,使用Environment的containsProperty()方法
如果将属性解析为类,可以使用getPropertyAsClass()方法:
Class<CompactDisc> cdClass =
env.getPropertyAsClass("disc.class",CompactDisc.class);
Environment还提供了了一些方法来检查哪些profile处激活状态:
- Stirng[] getActiveProfiles():返回机会profile名称的数组;
- Stirng[] getDefaultProfiles():返回默认profile名称的数组;
- boolean acceptsProfiles(String… profiles):如果environment支持给定profile的 话,就返回true
Spring装配中,可以使用占位符来装配属性,占位符的形式为"${…}",下面这个例子中title构造器参数所给定的值是从一个属性中解析得到的,属性名字叫做disc.title,artist装配的是disc.artist的属性值。
<bean id="sgtPeppers"
class="soundsystem.BlankDisc"
c:_titile="${disc.title}"
c:_artist="${disc.artist}"/>
如果使用了组件扫描和自动装配来创建和初始化组件,就没有指定占位符的配置文件或者类。这时候可以使用@Value注解。
public BlankDisc(
@Value("${disc.title}")String title,
@Value("${disc.artise}")String arties){
this.title = title;
this.artist = artist;
}
为了开启使用占位符这个功能,需要配置一个个PropertyPlaceholderConfigurer bean 或PropertySourcesPlaceholderConfigurer bean。从Spring 3.1开始,推荐使用PropertySourcesPlaceholderConfigurer,因为它能够基于Spring Environment及其属性源来解析占位符。
java中使用:需要在你使用占位符的类里面加上这个bean
@Bean
public static PropertySourcesPlaceholderConfigurer placeholdConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
xml中使用:加上
<contextLproperty-placeholder/>
3.5.2使用Spring表达式语言进行装配
SpEL有很多特性,包括:
- 使用bean的ID来引用bean
- 调用方法和访问对象的属性
- 对值进行算术、关系和逻辑运算
- 正则表达式匹配
- 集合操作
Spring表达式的结构为"#{…}"
//T()表达式会将java.lang.System视为Java中对应的类型,因此可以调用它的方法
#{T(System).currentTimeMillis()}
//引用了id为sgtPeppers的bean的artist属性
#{sgtPeppers.artist}
//通过systemProperties对象引用系统属性
#{systemProperties['disc.title']}
之前使用占位符获取disc.title和disc.artist的例子,现在可以写成这样
public BlankDisc(
@Value("#{systemProperties['disc.title']}")String title,
@Value("#{systemProperties['disc.artist']}")String artist){
this.title = title;
this.artist = artist;
}
在xml中,则可以写成这样:
<bean id="sgtPeppers"
class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.artist']}"/>
SpEL支持的基础表达式
- 表示字面值
整数值:#{100}
浮点值:#{3.1415926}
科学计数法:#{9.87E4}
String类型:#{‘Hello’}
布尔类型:#{true},#{false}- 引用bean、属性和方法
引用id为sgtPeppers的bean:#{sgtPeppers}
引用sgtPeppers的artist属性:#{sgtPeppers.artist}
引用sgtPeppers的方法:#{sgtPeppers.selectArtist()}
selectArtist()返回了一个字符串,那么可以继续调用这个字符串的方法:
#{sgtPeppers.selectArtist().toUpperCase()}
如果selectArtist()返回了null,那么要保证类型安全:
#{sgtPeppers.selectArtist()?.toUpperCase()},问号保证只有不为null才能访问右边- 使用类型
T(java.lang.Math):T()运算符的结果会是一个Class对象,最大的意义是可以访问到
类的静态方法和常量- SpEL运算符
匹配正则表达式:
- 计算集合
引用jukebox这个bean的songs集合的一个元素的title属性:#{jukebox.songs[4].title}
可以进行更复杂的操作,从这个集合中随机挑选一个title
#{jukebox.songs[T(java.lang.Math).random()*jukebox.songs.size()].title}
"[ ]“可以从集合中取元素,同样也可以在String中取字符。
#{“This is a test”[3]}就会取到这个String中的第四个字符"s”(.?[])操作符支持对集合进行过滤,得到一个子集。
#{jukebox.songs.?[artist eq ‘Aerosmith’]}(.1)和(.$[])是两个查询运算符,分别用来在集合中查询第一个匹配项和最后一个匹配项
匹配artist等于’Aerosmith’的第一个结果
#{jukebox.songs.2}(.![])投影运算符,可以从集合中每个成员中选择特定的属性发明回到礼物奶一个集合中
将集合的title属性投影到一个新的String类型集合中
#{jukebox.songs.![title]}也可以多个运算符一起使用,先过滤在进行投影
#{jukebox.songs.?[artist eq ‘Aerosmith’].![title]}
虽然SpEL非常强大,有时候可能会想去编写很复杂的表达式,但是SpEL是字符串类型的,越是复杂,测试起来就越是麻烦,所以编写SpEL的时候应当尽可能保持简介。