3.1 环境与profile
对于某些特定的bean或配置,在开发环境、QA环境和生产环境所需要的配置是不一致的,例如数据库配置、加密算法、外部系统的集成等
对于这个问题,经典的解决方案是:
在单独的配置类或配置文件中配置每个可能变化的bean,然后在构建阶段决定要将哪一个配置编译到可部署的应用中
这种方式的问题是在于要为每种环境重新构建应用
从开发阶段迁移到QA阶段时,重新构建也许不是什么大问题,但是,从QA阶段迁移到生产阶段时,重新构建可能引入bug并且会在QA团队的成员中带来不安的情绪
对于这个问题,Spring提供了不需要重新构建的解决方案
1.配置profile bean
Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别,还是根据环境决定该创建那个bean和不创建哪个bean
不过,Spring并不是在构建的时候做出这样的决策,而是等到运行时再来确定
这样的结果就是一个部署单元能够适用于所有的环境,没有必要进行重新构建
这种思想有点类似静态联编与动态联编的区别、又类似于自己new一个对象和利用反射构造对象的区别
Spring3.1引入了bean profile的功能
要使用profile,首先要将所有不同的bean整理到一个或多个profile之中,然后在将应用部署到每个环境时,要确保对应的profile处于激活状态即可
可使用@Profile指定某个bean所对应的环境,只有对应的环境被激活时,才会构造对应的Bean,否则@Bean注解会被忽略掉
若在XML中,可以重复利用<beans>的profile属性定义对应的环境
2.激活profile
Spring根据spring.profiles.active和spring.profiles.default两个变量来确定哪个profile处于激活状态
如果设置了spring.profiles.active属性,就会用它来确定哪个profile是激活的
如果没有设置spring.profiles.active,那么会去查找spring.profiles.default的值
如果二者都没有设置,表示没有激活的profile,那么只会创建那些没有在profile中的bean
有多种方式来设置这两个属性:
DispatcherServlet的初始化参数:<init-param>标签
Web应用上下文参数:<context-param>标签
JNDI条目
作为环境变量
作为JVM的系统属性
在测试类上,使用@ActiveProfiles注解设置
建议的做法:
开发阶段使用DispatcherServlet的参数将spring.profiles.default设置开发环境的profile
一般为了兼顾ContextLoaderListener,还会在Web上下文中设置相同的profile
当应用部署到QA、生产或者其他生产环境时,负责部署的人根据情况使用系统属性、环境变量或者JNDI设置spring.profiles.active即可
当设置了spring.profiles.active后,spring.profiles.default的值已经无所谓了,系统会优先使用spring.profiles.active的值
执行单元测试时,可以使用@ActiveProfiles注解设置profile,已激活相应的生产环境
3.2 条件化的bean
Spring4引入@Conditional注解,用于满足某些条件时,才实例化某些Bean
@Conditional参数为一个Class,且这个类要求实现Condition接口,当调用matches方法的返回值为true时,才创建bean,否则不差UN高进
编写matches方法时,涉及对ConditionContext好AnnotatedTypeMetadata对象的使用
Spring4开始,对@Profile注解进行了重构,使其基于@Conditional和Condition实现
3.3 处理自动装配的歧义性
当有多个同类型的bean,Spring无法按类型自动装配时(可能会尝试按名称),会抛出NoUniqueBeanDefinitionException异常
可以使用@Primary注解标注首选Bean,告知Spring在歧义时首选这个Bean
在XML中可使用bean的primary="true"完成相同的功能
如果配置了多个首选Bean,那么仍然会进一步导致歧义问题
此时可以考虑使用@Qualifier注解为bean设置自己的限定符,然后在注入时用@Qualifier定义要注入的bean的id
其实,@Autowired会先尝试按类型装配,若有多个同类型,则尝试按变量名自动装配,若都没有,自动装配失败
因此,只需@Qualifier定义的名字和@Autowired的变量名称一致即可省略装配时的@Qualifier注解
还可以使用自定义注解,同时添加多个限定条件,来表示某个Bean可以同时具有多个名字
3.4 bean的作用域
Spring中定义了多种作用域,可以基于这些作用域创建bean:
单例:singleton,在整个应用中,只创建bean的一个实例
原型:prototype,每次注入或者获取这个bean时,都会创建一个新的bean实例(如和Struts2整合时就要使用这个作用域)
会话:session,在web应用中,为每个会话创建一个bean实例
请求:在Web应用中,为每个请求创建一个bean实例
默认情况下,Spring应用上下文中的bean都是以单例的形式创建的,如果要使用其他的作用域,可使用@Scope注解
@Scope注解可以与@Component或@Bean注解一起使用
普通工程常见作用域
@Scope("singleton") = @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON),这个也是默认值
@Scope("prototype") = @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
web工程常见的作用域
@Scope("request") = @Scope(WebApplicatonContext.SCOPE_REQUEST)
@Scope("session") = @Scope(WebApplicatonContext.SCOPE_SESSION)
在XML中,可以使用<bean>元素的scope属性来设置作用域
1.使用会话和请求作用域
注意使用请求会话作用域时,要设置proxyMode属性,该属性用于解决将会话或请求作用域的bean注入到单例的bean中所导致的问题
对于一个单例的bean,Spring上下文加载时就会进行创建,这个bean可能要求注入一个机遇请求或会话作用域的bean
但容器启动吧阶段,用于没有发出请求,这个bean是不存在的,因此Spring注入的是一个代理的Bean,这个bean暴露的接口和真正的bean的接口是一致的
当proxyMode设置为ScopedProxyMode.INTERFACES,该代理技术要求这个bean要实现接口,以把接口委托给代理类
我猜测底层估计使用的是JDK的动态代理实现的,JDK的动态代理技术要求被代理的类必须实现接口
当proxyMode设置为ScopedProxyMode.TARGET_CLASS时,采用CGLIB来生成代理
由于CGLib是采用继承技术实现的代理,因此对于被代理的类只要不是final,能被继承即可
2.在XML中生命作用域代理
可以使用<bean>标签的子标签<aop:scoped-proxy/>设置代理技术
默认情况下,它会使用CGLib创建目标类的代理,若想更改,可以将proxy-target-class属性设置为false
<aop:scoped-proxy proxy-target-class="false"/>
3.5 运行时注入
有时可能希望避免硬编码,而是在运行时再确定,Spring提供两种在运行时求值的方式
可使用注解@PropertySource("classpath:/db.properties")引入相关配置文件,然后在代码中自动注入Environment,获取properties
Environment简单介绍
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)
前两个方法以String类型返回,且第二个具有默认值
后两个可以指定目标类型,例如连接池数量,可以指定为Integer类型,并且可以设置一个默认值
如果对应的key不存在,则返回null,如果希望属性必须定义,可以使用getRequiredProperty()方法
此时如果对应的属性没有定义,会抛出IllegalStateException
可以使用containsProperty检查属性是否存在
Environment还提供了一系列方法来检查哪些profile处于激活状态
String[] getActiveProfiles():返回激活profile名称的数组
String[] getDefaultProfiles():返回默认profile名称的数组
boolean acceptsProfiles(String... profiles):如果environment支持给定的profile,就返回true
解析属性占位符
在XML中,占位符的使用形式为${...}
在JavaConfig中,使用@Value注解使用占位符设置资源
为了使用占位符,必须配置一个PropertyPlaceholderConfigurer或PropertySourcesPlaceholderConfigurer
Spring3.1后推荐使用PropertySourcePlaceholderConfigurer,因为它能基于Environment及其属性源来解析占位符
如果使用XML配置,<context:property-placeholder/>会自动生成PropertySourcePlaceholderConfigurer
使用Spring表达式语言进行装配
Spring3引入了Spring表达式语言,SpEL,它能以一种强大和简介的方式将值装配到bean属性和构造器参数中,使用的表达式会在运行时计算得到值
SpEL有很多特性,包括:
使用bean的ID来引用bean
调用方法和访问对象的属性
对值进行算术、关系和逻辑运算
正则表达式匹配
集合操作
与属性占位符${}类似,SpEL表达式要放到#{...}之中
T()表达式会将java.lang下的包作为基础路径,如#(System)表示java.lang.System类,T(String)表示String类
在SpEL表达式中可以使用ID引用其他bean
可以通过systemProperties对象引用系统属性
如果使用JavaConfig配置,则利用@Value注解填写SpEL表达式
如果使用XML,则一般填写在property或construct-arg的value属性中
常见字面值:
#{3.1415}为浮点数
#{9.87E4}为科学计数法表示的浮点数
#{‘Hello‘}为字符串
#{true}为Boolean类型
可以使用.来引用属性或者调用方法
此外,?.运算符可以确保对象的非空性,若为空则不引用属性或者调用方法,表达式返回null
例如,#{artist.selectArtist()?.toUpperCase()}
在SpEL中使用类型
T()运算符的结果其实是一个Class对象,如果需要的话,甚至可以装配到一个Class类型的bean属性中
但T()真正的价值在于可以访问目标类型的静态方法和常量,例如#{T(java.lang.Math).random()}
SpEL常用运算符
*, ^, +, ==, eq, ? :, ?:
计算正则表达式
spEL通过matches运算符支持表达式中的模式匹配,返回true或者false
#{admin.email matches ‘[a-zA-Z0-9._%+-][email protected][a-zA-Z0-9.-]+\\.com‘}
计算集合
选取集合中的元素:#{jukebox.song[4].title}
选取String中给定下标字符:#{‘This id a test‘[3]}
.?[]查询运算符,可用于对集合元素进行过滤:#{jukebox.songs.?[artist eq ‘Aerosmith‘]}
.^[]在集合中查询第一个匹配的项
.$[]在集合中查询最后一个匹配的项
.![]可用于投影操作:#{jukebox.song.![title]}