环境与profile
迁移环境可能的问题
1、数据库配置,在开发环境中, 我们可能会使用嵌入式数据库, 并预先加载测试数据。 例如, 在Spring配置类中, 我们可能会在一个带有@Bean注解的方法上使
用EmbeddedDatabaseBuilder
@Bean(destroyMethod = "shutdown")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
这会创建一个类型为
javax.sql.DataSource
的
bean
使用 EmbeddedDatabaseBuilder 会搭建一个嵌入式的 Hypersonic 数据库, 它的模式( schema ) 定义在 schema.sql 中, 测试数据则是通过 test-data.sql 加载的。
create table Things (
id identity,
name varchar(100)
);
insert into Things (name) values ('A')
在开发环境中运行集成测试或者启动应用进行手动测试的时候,这个
DataSource
是很有用的。 每次启动它的时候, 都能让数据库处于一个给定的状态。尽管
EmbeddedDatabaseBuilder
创建的
DataSource
非常适于开发环境, 但是对于生产环境来说, 这会是一个糟糕的选择。 在生产环境的配置中, 你可能会希望使用
JNDI
从容器中获取一个
DataSource
。 在这样场景中, 如下的
@Bean
方法会更加合适
@Bean
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
通过
JNDI
获取
DataSource
能够让容器决定该如何创建这个
DataSource
, 甚至包括切换为容器管理的连接池。 即便如此,
JNDI
管理的
DataSource
更加适合于生产环境, 对于简单的集成和开发测试环境来说, 这会带来不必要的复杂性。
配置profile bean
Spring为环境相关的bean所提供的解决方案其实与构建时的方案没有太大的差别。 在这个过程中需要根据环境决定该创建哪个bean和不创建哪个bean。 不过Spring并不是在构建的时候做出这样的决策, 而是等到运行时再来确定。 这样的结果就是同一个部署单元(可能会是WAR文件) 能够适用于所有的环境, 没有必要进行重新构建。在3.1版本中,Spring引入了bean profile的功能。 要使用profile, 首先要将所有不同的bean定义整理到一个或多个profile之中, 在将应用部署到每个环境时, 要确保对应的profile处于激活(active) 的状态。
在Java配置中, 可以使用@Profile注解指定某个bean属于哪一个profile。
例如:
嵌入式数据库
@Configuration
@Profile("dev")
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
}
@Profile
注解应用在了类级别上。 它会告诉
Spring
这个配置类中的
bean
只有在
dev
profile
激活时才会创建。 如果
dev
profile
没有激活的话, 那么带有
@Bean
注解的方法都会被忽略掉。
适用于生产环境的配置
@Configuration
@Profile("prod")
public class DataSourceConfig {
@Bean
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
只有
prod
profile
激活的时候, 才会创建对应的
bean
以上为3.1中的, 只能在类级别上使用 @Profile 注解 , 3.2 开始, 可以在方法级别上使用 @Profile 注解,与 @Bean 注解一同使用。 这样的话, 就能将这两个 bean 的声明放到同
一个配置类之中
@Configuration
public class DataSourceConfig {
@Bean(destroyMethod = "shutdown")
@Profile("dev")
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:test-data.sql")
.build();
}
@Bean
@Profile("prod")
public DataSource jndiDataSource() {
JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiName("jdbc/myDS");
jndiObjectFactoryBean.setResourceRef(true);
jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
return (DataSource) jndiObjectFactoryBean.getObject();
}
}
没有指定
profile
的
bean
始终都会被创建, 与激活哪个
profile
没有关系。
在XML中配置profile
可以通过 <beans> 元素的 profile 属性, 在 XML 中配置 profile bean 。
可以如此,但会创建多个xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
profile="dev">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
如下可以合并
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<beans profile="dev">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:schema.sql" />
<jdbc:script location="classpath:test-data.sql" />
</jdbc:embedded-database>
</beans>
<beans profile="prod">
<jee:jndi-lookup id="dataSource"
lazy-init="true"
jndi-name="jdbc/myDatabase"
resource-ref="true"
proxy-interface="javax.sql.DataSource" />
</beans>
</beans>
激活profileSpring 在确定哪个 profile 处于激活状态时, 需要依赖两个独立的属性: spring.profiles.active 和 spring.profiles.default 。 如果设置了 spring.profiles.active 属性, 那么它的值就会用来确定哪个 profile 是激活的。 但如果没有设置 spring.profiles.active 属性的话, 那 Spring 将会查找 spring.profiles.default 的值。如果 spring.profiles.active 和 spring.profiles.default 均没有设置的话, 那就没有激活的 profile , 因此只会创建那些没有定义在 profile 中的 bean 。
有多种方式来设置这两个属性:
作为 DispatcherServlet 的初始化参数;
作为 Web 应用的上下文参数;
作为 JNDI 条目;
作为环境变量;
作为 JVM 的系统属性;
在集成测试类上, 使用 @ActiveProfiles 注解设置
1、使用 DispatcherServlet 的初始化参数 ,在servlet的上下文中进行设置( 兼顾到 ContextLoaderListener )
web.xml
profile 使用的都是复数形式。 这意味着你可以同时激活多个 profile , 这可以通过列出多个 profile 名称, 并以逗号分隔来实现
测试
public class DataSourceConfigTest {
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("dev")
public static class DevDataSourceTest {
@Autowired
private DataSource dataSource;
@Test
public void shouldBeEmbeddedDatasource() {
assertNotNull(dataSource);
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
List<String> results = jdbc.query("select id, name from Things", new RowMapper<String>() {
@Override
public String mapRow(ResultSet rs, int rowNum) throws SQLException {
return rs.getLong("id") + ":" + rs.getString("name");
}
});
assertEquals(1, results.size());
assertEquals("1:A", results.get(0));
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=DataSourceConfig.class)
@ActiveProfiles("prod")
public static class ProductionDataSourceTest {
@Autowired
private DataSource dataSource;
@Test
public void shouldBeEmbeddedDatasource() {
// should be null, because there isn't a datasource configured in JNDI
assertNull(dataSource);
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:datasource-config.xml")
@ActiveProfiles("dev")
public static class DevDataSourceTest_XMLConfig {
@Autowired
private DataSource dataSource;
@Test
public void shouldBeEmbeddedDatasource() {
assertNotNull(dataSource);
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
List<String> results = jdbc.query("select id, name from Things", new RowMapper<String>() {
@Override
public String mapRow(ResultSet rs, int rowNum) throws SQLException {
return rs.getLong("id") + ":" + rs.getString("name");
}
});
assertEquals(1, results.size());
assertEquals("1:A", results.get(0));
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:datasource-config.xml")
@ActiveProfiles("prod")
public static class ProductionDataSourceTest_XMLConfig {
@Autowired(required=false)
private DataSource dataSource;
@Test
public void shouldBeEmbeddedDatasource() {
// should be null, because there isn't a datasource configured in JNDI
assertNull(dataSource);
}
}
}
条件化的bean希望一个或多个bean只有在应用的类路径下包含特定的库时才创建。 或者希望某个bean只有当另外某个特定的bean也声明了之后才会创建。 我们还可能要求只有某个特定的环境变量设置之后, 才会创建某个bean
spring4给了解决方案, Spring 4 引入了一个新的 @Conditional 注解, 它可以用到带有 @Bean 注解的方法上。 如果给定的条件计算结果为 true , 就会创建这个 bean , 否则的话, 这个 bean 会被忽略@Configuration
public class MagicConfig {
@Bean
@Conditional(MagicExistsCondition.class)//条件话的创建bean
public MagicBean magicBean() {
return new MagicBean();
}
}
public class MagicExistsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("magic");
}
}
如果返回false,不会创建bean,返回true,创建bean
matches() 方法很简单但功能强大。 它通过给定的 ConditionContext 对象进而得到 Environment 对象, 并使用这个对象检查环境中是否存在名为 magic 的环境属性
public interface ConditionContext {
BeanDefinitionRegistry getRegistry();
ConfigurableListableBeanFactory getBeanFactory();
Environment getEnvironment();
ResourceLoader getResourceLoader();
ClassLoader getClassLoader();
}
通过
ConditionContext
, 我们可以做到如下几点:借助 getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义;
借助 getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性;
借助 getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么;
读取并探查 getResourceLoader() 返回的 ResourceLoade r 所加载的资源;
借助 getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。
AnnotatedTypeMetadata 则能够让我们检查带有 @Bean 注解的方法上还有什么其他的注解
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
注解的方法上其他注解的属性。
从 Spring 4 开始, @Profile 注解进行了重构, 使其基于 @Conditional 和 Condition 实现
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
String[] value();
}
@Profile
本身也使用了
@Conditional
注解, 并且引用
ProfileCondition
作为
Condition
实现。 如下所示,
ProfileCondition
实现了
Condition
接口, 并且在做出决策的过程中, 考虑到了
ConditionContext
和
AnnotatedTypeMetadata
中的多个因素。
class ProfileCondition implements Condition {
@Override
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
来检查[借助
acceptsProfiles()
方法] 该
profile
是否处于激活状态
处理自动装配的歧义性
@Autowired
public void setDessert(Dessert dessert){
this.dessert(dessert);
}
@Component
public class IceCream implements Dessert{
}
@Component
public class Cookies implements Dessert {
}
这三个实现均使用了
@Component
注解, 在组件扫描的时候, 能够发现它们并将其创建为
Spring
应用上下文里面的
bean
。 然后, 当
Spring
试图自动装配
setDessert()
中的
Dessert
参数时, 它并没有唯一、 无歧义的可选值 ,
Spring
会抛出
NoUniqueBeanDefinitionException
解决方案
1、将可选bean中的某一个设为首选(primary) 的bean,
2、或者使用限定符(qualifier) 来帮助Spring将可选的bean的范围缩小到只有一个bean
标示首选的bean在声明 bean 的时候, 通过将其中一个可选的 bean 设置为首选( primary ) bean 能够避免自动装配时的歧义性。 当遇到歧义性的时候, Spring 将会使用首选的 bean , 而不是其他可选的 bean
如:
@Primary
@Component
public class Cookies implements Dessert {
@Autowired
public void setDessert(Dessert dessert){
this.setDessert(dessert);
}
}
@Bean
@Primary
public MagicBean magicBean() {
return new MagicBean();
}
<bean id="iceCream" class="com.desserteater.IceCream" primary="true"/>
如果设置多个首选,歧义性问题还会出现
限定自动转配的bean
@Qualifier 注解是使用限定符的主要方式。 它可以与 @Autowired 和 @Inject 协同使用, 在注入的时候指定想要注入进去的是哪个 bean 。
如:
将iceCream注入到setDessert中
@Autowired
@Qualifier("iceCream")
public void setDessert(Dessert dessert){
this.setDessert(dessert);
}
所有使用
@Component
注解声明的类都会创建为
bean
, 并且
bean
的
ID
为首字母变为小写的类名,
因此,
@Qualifier("iceCream")
指向的是组件扫描时所创建的
bean
, 并且这个
bean
是
IceCream
类的实例。
@Qualifier("iceCream") 所引用的 bean 要具有 String 类型的 “iceCream” 作为限定符。 如果没有指定其他的限定符的话, 所有的 bean 都会给定一个默认的限定符, 这个限定与 bean 的 ID 相同。 因此, 框架会将具有 “iceCream” 限定符的 bean 注入到 setDessert() 方法中。 这恰巧就是 ID 为 iceCream 的 bean , 它是 IceCream 类在组件扫描的时候创建的。
对类名称的任意改动都会导致限定符失效。
创建自定义的限定符
为 bean 设置自己的限定符, 而不是依赖于将 bean ID 作为限定符。 在这里所需要做的就是在 bean 声明上添加 @Qualifier 注解
如 与 @Component 组合使用
@Qualifier("cold")
@Component
public class Cookies implements Dessert {
}
cold限定符分配给了Cookies bean,可以随便定义类名,因为没有耦合
@Autowired
@Qualifier("cold")
public void setDessert(Dessert dessert){
this.setDessert(dessert);
}
注入的地方只要引用cold限定符就可以
当通过 Java 配置显式定义 bean 的时候, @Qualifier 也可以与 @Bean 注解一起使用
@Bean
@Qualifier("cold")
public Dessert iceCream(Dessert dessert){
return new Cookies();
}
自定义名字,可以选择描述性术语
使用自定义的限定符注解
java8允许出现重复的注解,只要这个注解本身定义的时候带有@Repeatable注解就可以,但spring的@Qualifier注解并没有在定义的时候添加@Repeatable注解
面向特性的限定符要比基于 bean ID 的限定符更好一些。 但是, 如果多个 bean 都具备相同特性的话, 这种做法也会出现问题
如新的子类有相同的限定符,但我们可以创建自定义的限定符注解
@Target({ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {}
自定义注解,
在定义时添加
@Qualifier
注解, 它们就具有了
@Qualifier
注解的特性
@Cold
@Component
public class Cookies implements Dessert {
@Autowired
@Cold
public void setDessert(Dessert dessert){
this.setDessert(dessert);
}
@Bean
@Cold
public Dessert iceCream(Dessert dessert){
return new Cookies();
}
}
可以定义多个注解,放入,满足条件的就会注入成功
bean的作用域
在默认情况下, Spring 应用上下文中所有 bean 都是作为以单例( singleton ) 的形式创建的。 也就是说,不管给定的一个 bean 被注入 到其他 bean 多少次, 每次所注入的都是同一个实例。在大多数情况下, 单例 bean 是很理想的方案。 初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务, 在这些任务中, 让对象保持无状态并且在应用中反复重用这些对象可能并不合理。有时候, 可能会发现, 你所使用的类是易变的( mutable ) , 它们会保持一些状态, 因此重用是不安全的。 在这种情况下, 将 class 声明为单例的 bean 就不是什么好主意了, 因为对象会被污染, 稍后重用的时候会出现意想不到的问题
Spring 定义了多种作用域, 可以基于这些作用域创建 bean , 包括:
单例( Singleton ) : 在整个应用中, 只创建 bean 的一个实例。
原型( Prototype ) : 每次注入或者通过 Spring 应用上下文获取的时候, 都会创建一个新的 bean 实例。
会话( Session ) : 在 Web 应用中, 为每个会话创建一个 bean 实例。
请求( Rquest ) : 在 Web 应用中, 为每个请求创建一个 bean 实例。
单例是默认的作用域,对于易变的类型, 这并不合适。 如果选择其他的作用域, 要使用@Scope注解, 它可以与@Component或@Bean一起使用
如: 使用组件扫描来发现和声明 bean , 那么你可以在 bean 的类上使用 @Scope 注解, 将其声明为原型 bean
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Notepad {
}
String SCOPE_SINGLETON = "singleton";
String SCOPE_PROTOTYPE = "prototype";
也可以使用
@Scope("prototype")
, 但是使用
SCOPE_PROTOTYPE
常量更加安全并且不易出错
在 Java 配置中将 Notepad 声明为原型 bean , 可以组合使用 @Scope 和 @Bean 来指定所需的作用域
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Notepad notepad() {
return new Notepad();
}
使用
XML
来配置
bean
的话, 可以使用
<bean>
元素的
scope
属性来设置作用域
<bean class="com.myapp.Notepad" scope="prototype" />
以上都会创建新的实例
使用会话和请求作用域
在 Web 应用中, 如果能够实例化在会话和请求范围内共享的 bean , 那将是非常有价值的事情
例如, 在典型的电子商务应用中, 可能会有一个 bean 代表用户的购物车。 如果购物车是单例的话, 那么将会导致所有的用户都会向同一个购物车中添加商品。 另一方面, 如果购物车是原型作用域的, 那么在应用中某一个地方往购物车中添加商品, 在应用的另外一个地方可能就不可用了, 因为在这里注入的是另外一个原型作用域的购物车。就购物车 bean 来说, 会话作用域是最为合适的, 因为它与给定的用户关联性最大。 要指定会话作用域, 我们可以使用 @Scope 注解, 它的使用方式与指定原型作用域是相同的
@Component
@Scope(
value=WebApplicationContext.SCOPE_SESSION,
proxyMode=ScopedProxyMode.INTERFACES)
public ShoppingCart cart(){
return new ShoppingCart();
}
将
value
设置成了
WebApplicationContext
中的
SCOPE_SESSION
常量(它的值是
session
) 。 这会告诉
Spring
为
Web
应用中的每个会话创建一个
ShoppingCart
。 这会创建多个 ShoppingCart bean 的实例, 但是对于给定的会话只会创建一个实例, 在当前会话相关的操作中, 这个 bean 实际上相当于单例的。
@Scope 同时还有一个 proxyMode 属性, 它被设置成了 ScopedProxyMode.INTERFACES 。 这个属性解决了将会话或请求 作用域的 bean 注入到单例 bean 中所遇到的问题
如果 ShoppingCart 是接口而不是类的话, 这是可以的(也是最为理想的代理模式) 。 但如果 ShoppingCart 是一个具体的类的话, Spring 就没有办法创建基于接口的代理了。 此时, 它必须使用 CGLib 来生成基于类的代理。 所以, 如果 bean 类型是具体类的话, 我们必须 要将 proxyMode 属性设置为 ScopedProxyMode.TARGET_CLASS , 以此来表明要以生成目标类扩展的方式创建代理。
请求作用域的 bean 会面临相同的装配问题。 因此, 请求作用域的 bean 应该也以作用域代理的方式进行注入。
在xml中声明作用域原理
要设置代理模式, 我们需要使用 Spring aop 命名空间的一个新元素
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy>
</bean>
<aop:scoped-proxy>
是与
@Scope
注解的
proxyMode
属性功能相同的
Spring XML
配置元素。 它会告诉
Spring
为
bean
创建一个作用域代理。 默认情况下, 它会使用
CGLib
创建目标类的代理。 但是我们也可以将
proxy-target-class
属性设置为
false
, 进而要求它生成基于接口的代理:
<bean id="cart" class="com.myapp.ShoppingCart" scope="session">
<aop:scoped-proxy proxy-target-class="false">
</bean>
声明
Spring
的
aop
命名空间
运行时注入
解决硬编码问题, Spring 提供了两种在运行时求值的方式:属性占位符( Property placeholder ) 。 Spring 表达式语言( SpEL ) 。
注入外部的值
在 Spring 中, 处理外部值的最简单方式就是声明属性源并通过 Spring 的 Environment 来检索属性。
@Configuration
@PropertySource("classpath:/com/soundsystem/app.properties")
public class EnvironmentConfig {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title"),
env.getProperty("disc.artist"));
}
}
@PropertySource
引用了类路径中一个名为
app.properties
的文件
这个属性文件会加载到 Spring 的 Environment 中, 稍后可以从这里检索属性。 同时, 在 disc() 方法中, 会创建一个新的 BlankDisc ,它的构造器参数是从属性文件中获取的, 而这是通过调用 getProperty() 实现的
Environment
四种getProperty方法
String getProperty(String key);
String getProperty(String key, String defaultValue);
<T> T getProperty(String key, Class<T> targetType);
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
在值不存在时给定默认值
@Configuration
public class EnvironmentConfigWithDefaults {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getProperty("disc.title", "Rattle and Hum"),
env.getProperty("disc.artist", "U2"));
}
}
如果我们从属性文件中得到的是一个
String
类型的值, 那么在使用之前还需要将其转换为
Integer
类型。 但是, 如果使用重载形式的
getProperty()
的话, 就能非常便利地解决这个问题
int connectionCount = env.getProperty("db.connection", Integer.class, 30);
在使用
getProperty()
方法的时候没有指定默认值, 并且这个属性没有定义的话, 获取到的值是
null
。 如果你希望这个属性必须要定义, 那么可以使用
getRequiredProperty()
方法
@Configuration
public class EnvironmentConfigWithRequiredProperties {
@Autowired
Environment env;
@Bean
public BlankDisc blankDisc() {
return new BlankDisc(
env.getRequiredProperty("disc.title"),
env.getRequiredProperty("disc.artist"));
}
}
如果
disc.title
或
disc.artist
属性没有定义的话, 将会抛出
IllegalStateException
异常
检查属性是否存在
调用Environment的containsProperty()方法
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 。
解析属性占位符
在 Spring 装配中, 占位符的形式为使用 “ ${... } ” 包装的属性名称
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="${disc.title}"
c:_artist="${disc.artist}"/>
title
构造器参数所给定的值是从一个属性中解析得到的, 这个属性的名称为
disc.title
。
artist
参数装配的是名为
disc.artist
的属性值。 按照这种方式,
XML
配置没有使用任何硬编码的值, 它的值是从配置文件以外的一个源中解析得到的
依赖于组件扫描和自动装配来创建和初始化应用组件的话,那么就没有指定占位符的配置文件或类了。 在这种情况下, 我们可以使用 @Value 注解, 它的使用方与 @Autowired 注解非常相似
public BlankDisc(@Value("${disc.title}") String title, @Value("${disc.artist}")String artist) {
this.title = title;
this.artist = artist;
}
为了使用占位符, 我们必须要配置一个
PropertyPlaceholderConfigurer
bean
或
PropertySourcesPlaceholderConfigurer
bean
。 从
Spring3.1
开始, 推荐使用 PropertySourcesPlaceholderConfigurer , 因为它能够基于 Spring Environment 及其属性源来解析占位符。
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
return new PropertySourcesPlaceholderConfigurer();
}
xml配置
Spring context命名空间中的<context:propertyplaceholder>元素将会生成PropertySourcesPlaceholderConfigurer bean
解析外部属性能够将值的处理推迟到运行时, 但是它的关注点在于根据名称解析来自于Spring Environment和属性源的属性。
使用spring表达式语言进行装配
Spring 3引入了Spring表达式语言
SpEL拥有很多特性, 包括:
使用bean的ID来引用bean;
调用方法和访问对象的属性;
对值进行算术、 关系和逻辑运算;
正则表达式匹配;
集合操作。
SpEL样例
SpEL能够用在依赖注入以外的其他地方。 例如, Spring Security支持使用SpEL表达式定义安全限制规则。 另外, 如果你在Spring MVC应用中使用Thymeleaf模板作为视图
的话, 那么这些模板可以使用SpEL表达式引用模型数据
SpEL表达式要放到“#{ ... }”之中, 这与属性占位符有些类似, 属性占位符需要放到“${ ... }”之中
#{1}结果就是1
#{T(System).currentTImeMillis()}
它的最终结果是计算表达式的那一刻当前时间的毫秒数。 T()表达式会将java.lang.System视为Java中对应的类型, 因此可以调用其static修饰的currentTimeMillis()方法
SpEL表达式也可以引用其他的bean或其他bean的属性。 例如, 如下的表达式会计算得到ID为sgtPeppers的bean的artist属性
#{sgtPeppers.artist}
如果通过组件扫描创建bean的话, 在注入属性和构造器参数时, 我们可以使用@Value注解 ,类似属性占位符
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-
命名空间条目的值。 例如, 在如下
BlankDisc
bean
的
XML
声明中, 构造器参数就是通过
SpEL
表达式设置的
<bean id="sgtPeppers" class="soundsystem.BlankDisc"
c:_title="#{systemProperties['disc.title']}"
c:_artist="#{systemProperties['disc.artist']"/>
表示字面值
SpEL不仅 表示整数字面量的,它实际上还可以用来表示浮点数、 String 值以及 Boolean 值,科学计数法
使用 SpEL 将一个 bean 装配到另外一个 bean 的属性中, 此时要使用 bean ID 作为 SpEL 表达式
#{sgtPeppers}
在一个表达式中引用 sgtPeppers 的 artist 属性
#{sgtPeppers.artist}
表达式主体的第一部分引用了一个 ID 为 sgtPeppers 的 bean , 分割符之后是对 artist 属性的引用
还可以调用 bean 上的方法
#{artistSelector.selectArtist()}
可以对返回值调用方法
#{artistSelector.selectArtist().toUpperCase()}
#{artistSelector.selectArtist()?.toUpperCase()}
“?.”运算符。 这个运算符能够在访问它右边的内容之前, 确保它所对应的元素不是null ,如果selectArtist()的返回值是null的话, 那么SpEL将不会调用toUpperCase()方法。 表达
式的返回值会是null
在表达式中使用类型
在SpEL中访问类作用域的方法和常量的话, 要依赖T()这个关键的运算符
如使用java的Math类
T() 运算符的真正价值在于它能够访问目标类型的静态方法和常量
如下
T(java.lang.Math).PI
T(java.lang.Math).random()
SpEL运算符
运算符类型 | 运 算 符 |
算术运算 | +、 -、 * 、 /、 %、 ^ |
比较运算 | < 、 > 、 == 、 <= 、 >= 、 lt 、 gt 、 eq 、 le 、 ge |
逻辑运算 | and 、 or 、 not 、 │ |
条件运算 | ?: (ternary) 、 ?: (Elvis) |
正则表达式 | matches |
和java中一致
#{counter.total==100}等价于文本型eq#{counter.total eq 100}
三元运算符类似
#{100>0?100,10}
判null时
#{disc.title?:'100'},为空,值为100
计算正则表达式
#{admin.email matches 'a-zA-Z0-9._%+-+@[a-zA-Z0-9.-]+\\.com'}
计算集合
#{jukebox[4].title}“[]” 运算符用来从集合或数组中按照索引获取元素, 实际上, 它还可以从 String 中获取一个字符
#{'abcd'[3]}=d
SpEL 还提供了查询运算符( .?[] ) , 它会用来对集合进行过滤, 得到集合的一个子集
#{jukebox.songs.?[artist eq 'Aerosmith']}
另外两个查询运算符: “ .^[] ” 和 “ .$[] ” , 它们分别用来在集合中查询第一个匹配项和最后一个匹配项。
SpEL 还提供了投影运算符( .![] ) , 它会从集合的每个成员中选择特定的属性放到另外一个集合中
假设我们不想要歌曲对象的集合, 而是所有歌曲名称的集合。 如下的表达式会将 title 属性投影到一个新的 String 类型的集合中
#{jukebox.songs.![title]}
使用如下的表达式获得Aerosmith所有歌曲的名称列表
#{jukebox.songs.?[artist eq 'Aerosmoth'].![title]}