Spring容器-环境抽象
读Spring框架官方文档记录。
1. 简介
环境接口是整合在容器中的抽象,为应用环境中的两个关键方面:profiles及properties进行建模。
profile是一个有关bean定义的命名、逻辑组。只有给定的profile是active的时候,才会被注册到容器中。不论是定义在XML中的bean还是使用注解定义的bean都有可能分配给profile。与profile相关的环境对象决定哪些profiles现在是active的,哪些profiles是默认应该激活的。
(说了半天我也不知道profile是干啥的,找了博客理解如下:)
属性几乎在所有的应用程序中起到重要作用。属性可能产生于以下属性源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、 属性对象(Properties)、Map对象等等。与属性相关的环境对象会为用户提供一个便利的服务接口去配置属性源并冲属性源中解析属性。
2. bean定义配置
bean定义配置在核心容器中提供了一种机制:允许在不同环境中注册不同的bean。“环境”对于不同的用户意味着不同的含义,比如以下三种情况:
- 在开发中使用内存中的数据源,而在QA或生产中从JNDI查找相同的数据源。
- 仅在将应用程序部署到性能环境时才注册监视基础设施。
- 注册客户A和客户B部署的自定义bean实现。
比如第一种情况,在测试环境中如下所示:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
现在考虑如何将此应用程序部署到QA或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的JNDI目录中。如下所示:
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
问题是如何根据当前环境在两种变体中进行切换。现在已经有许多的方式,通常是用系统环境变量和包含${}标记的XML<import>
语句结合来解析正确的配置文件路径。使用bean定义配置也可以解决这个问题。
我们一般化前面所示的用例,我们最终需要在特定上下文中而不是在其他上下文中注册特定的bean定义。即在情况a中注册bean定义的特定概要文件,在情况b中注册不同的概要文件。
(1) 使用@Profile
当一个或多个特定的配置是active时,@Profile注解可以指定那个配置是允许注册的。如下面的例子所示:
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Profile注解中的字符串可能包含一个简单的配置名或者一个配置表达式。配置表达式能够表达更加复杂的配置逻辑。配置表达式中支持如下几种操作符:
- !:逻辑非
- &:逻辑与
- |:逻辑或
注意:不能在不使用括号的情况下混合使用&和|,如:production & us-east | eu-central是无效的,production & (us-east | eu-central)是可以的。
可以使用@Profile作为元注解来自定义组合注解。如下所示可以使用@Production来代替@Profile(“production”):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果@Configuration类被@Profile标记,那么类中所有的@Bean方法及@Import注解将会被绕过,除非一个或多个特定的配置被激活。如:一个@Component或@Configuration类被@Profile(“p1”,“p2”)标记,那么这个类不会被注册或者处理直到配置"p1"或者"p2"被激活。
@Profile也可在方法上声明,如下:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") //standaloneDataSource()只在development环境中可用
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production") //jndiDataSource()只在production环境中可用
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
注意:如果在重载@Bean方法中,@Profile要一致的声明。
(2) XMLbean定义配置
XML中可以使用<beans>
标签的profile属性。(1)中的配置可以进行如下重写:
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
<!--或者如下面所示也可以-->
<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"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
XML中不支持使用配置表达式,但是能够通过嵌套来定义"and"逻辑,如下所示:
<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"
xsi:schemaLocation="...">
<!-- other bean definitions -->
<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>
(3) 激活配置
配置完配置后,我们需要指示Spring去激活哪个配置。激活一个配置可以通过很多方式,最直接的是通过ApplicationContext调用Environment API,如下所示:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
也可以通过spring.profiles.active属性来激活,可以通过系统环境变量,JVM系统属性,web.xml中的servlet上下文参数,或者甚至是JNDI里的一个条目。在集成测试中,可以使用spring-test模块中的@ActiveProfiles注释声明活动概要文件。
注意:配置不是一个非此即彼的量,可以同时激活多个配置。如下所示:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
//或者如下表达
-Dspring.profiles.active="profile1,profile2"
(4) 默认配置
默认配置文件表示默认启用的配置文件。考虑如下例子:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
如果没有配置被激活,则dataSource会被激活。如果有任何其它配置被激活则默认配置不会被激活。可以通过Environment中的setDefaultProfiles()修改该默认配置的名字或者声明式中的spring.profiles.default属性来进行修改。
3. PropertySource抽象
Spring的环境抽象提供了可配置的属性源层次结构上的搜索操作。考虑如下例子:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
该代码片段询问Spring my-property属性是否在当前的环境中被定义。为了回答这个问题,环境对象在一组PropertySource对象上进行搜索。一个PropertySource是任何键值对源的简单抽象。Spring的StandardEnvironment由两个属性源对象配置,一个表示JVM系统属性集合(System.getProperties),一个表示系统环境变量(System.getenv())。
默认的属性源就是StandardEnvironment,用于独立程序。StandardServletEnvironment包括了附加的默认属性源,包括servlet配置和servlet上下文参数。该可以选择性地启用JndiPropertySource。
当使用StandardEnvironment时,如果系统属性或者环境变量my-property在运行时可用,使用env.containsProperty(“my-property”)可返回ture。注意:搜索是分级的,系统属性的优先级大于环境变量,即在使用env.containsProperty(“my-property”)调用时,系统属性和环境变量my-property都存在,返回的是系统属性确认的true。
对于StandardServletEnvironment,层次结构如下:
- servletConfig参数(如:DispatcherServlet上下文)
- ServletContext参数(web.xml 上下文参数条目)
- JNDI环境变量(java:comp/env/ entries)
- JVM系统属性(-D 命令行参数)
- JVM系统环境(操作系统环境变量)
支持自定义的属性源,可以配置属性源并添加到PropertySource源集合中。举例如下:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
MyPropertySource以最高的优先级添加到搜索项中。MutablePropertySources API中包含很多方法去精确设置属性源集合。
4. 使用@PropertySource
@PropertySource注解为向Spring环境添加PropertySource提供了一种方便的声明性机制。
给定的文件app.properties中包含键值对testbean.name=myTestBean,下面例子展示了如何把添加属性源并调用属性:
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
出现在@PropertySource资源位置中的任何${}占位符都将根据环境中已经注册的属性源集进行解析。如下例子:
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
假设my.placeholder出现在已经注册的属性源中,那么就按照对应的值进行解析,如果不存在,就使用默认值default/path,如果默认值也无法被解析,将会抛出IllegalArgumentException异常。
注意:不要混合使用@ProperSource和其作为元注解的自定义注解。
5. 语句中的占位符解析
最开始,语句中占位符的值只能根据JVM系统属性或环境变量来解析。但是现在可以通过环境抽象来解析。因为环境抽象集成在整个容器中,所以很容易通过它路由占位符的解析。这意味着我们可以以任何喜欢的方式配置解析过程。可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们,还可以根据需要将自己的属性源添加到混合源中。即下面的语句不管customer属性在哪里定义,只要它在环境中可用就可以工作。
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>