环境抽象
Environment接口是容器中集成的一种抽象层,它模拟了应用程序环境的两个关键方面:配置文件和属性。
配置文件(profile)是一组具有名称的、逻辑上相关的Bean定义,只有当指定的配置文件处于激活状态时,这些Bean定义才会被注册到容器中。无论是通过XML定义还是使用注解定义的Bean,都可以被分配给某个配置文件。Environment对象在配置文件中的作用是确定当前哪些配置文件处于激活状态(如果有的话),以及默认情况下哪些配置文件应该处于激活状态(如果有的话)。
属性在几乎所有应用程序中都扮演着重要角色,其来源可能多种多样:属性文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、临时创建的Properties对象、Map对象等等。Environment对象与属性相关的角色是为用户提供一个便捷的服务接口,以便配置属性源并从中解析属性。
Bean 定义配置文件
Bean定义配置文件(Bean definition profiles)为核心容器提供了一种机制,允许在不同的环境中注册不同的Bean。对于不同的用户来说,“环境”(environment)这个词可能有着不同的含义,而这一特性可以帮助解决许多使用场景中的问题,包括:
- 在开发阶段使用内存数据源,而在质量保证(QA)或生产环境中则从JNDI中查找相同的数据源。
- 仅在将应用程序部署到性能环境时才注册监控基础设施。
- 针对客户A的部署注册定制的bean实现,而对于客户B的部署则使用不同的实现。
考虑在实际应用中首次使用DataSource的用例。在测试环境中,配置可能类似于以下内容:
- Java
- Kotlin
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build()
}
现在考虑如何将此应用程序部署到QA或生产环境中,假设应用程序的数据源已注册在生产应用程序服务器的JNDI目录中。我们的dataSource bean现在如下所示:
- Java
- Kotlin
@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
问题在于如何根据当前环境在这两种配置变体之间进行切换。随着时间的推移,Spring 用户想出了多种方法来实现这一目标,通常是通过结合系统环境变量和 XML 中的 <import/> 语句来完成的,这些语句中包含 ${placeholder} 占位符,它们会根据环境变量的值来解析出正确的配置文件路径。Bean 定义配置文件(Bean definition profiles)是 Spring 容器的核心功能之一,它为这个问题提供了解决方案。
如果我们推广前面示例中展示的特定环境bean定义的用法,那么就出现了这样一种需求:需要在某些上下文中注册特定的bean定义,而在其他上下文中则不需要。可以说,在情况A下你希望注册某种类型的bean定义,在情况B下则希望注册另一种类型的bean定义。我们首先更新配置来反映这一需求。
使用 @Profile
@Profile 注解允许你指定,当一个或多个指定的配置文件(profiles)处于激活状态时,某个组件才有资格被注册。以我们之前的例子为例,我们可以将 dataSource 的配置重新写成如下形式:
- Java
- Kotlin
@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("development")
class StandaloneDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build()
}
}
- Java
- Kotlin
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod = "") 1
public DataSource dataSource() throws Exception {
contexts ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Bean(destroyMethod = "")禁用默认的销毁方法推断。
@Configuration
@Profile("production")
class JndiDataConfig {
@Bean(destroyMethod = "") 1
_fun dataSource(): DataSource {
val ctx = InitialContext()
return ctxlookup("java:comp/env/jdbc/datasource") as DataSource
}
}
@Bean(destroyMethod = "")禁用默认的销毁方法推断。
如前所述,对于@Bean方法,通常会选择使用程序化的JNDI查找,可以通过Spring的JndiTemplate/JndiLocatorDelegate辅助类来实现,或者直接使用前面展示的JNDI InitialContext方式,但不建议使用JndiObjectFactoryBean变体,因为那样会强制你将返回类型声明为FactoryBean类型。
配置文件字符串可以包含一个简单的配置文件名称(例如,production)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。在配置文件表达式中支持以下运算符:
!:该配置文件的逻辑“非”(NOT)&:这些配置文件的逻辑“与”(AND)|:这些配置文件的逻辑“或”(OR)
如果不使用括号,就不能混合使用&和|运算符。例如,production & us-east | eu-central不是一个有效的表达式。它必须表示为production & (us-east | eu-central)。
你可以使用 @Profile 作为 元注释,以便创建一个自定义的组合注释。以下示例定义了一个自定义的 @Production 注释,你可以用它来直接替代 @Profile("production"):
- Java
- Kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果一个@Configuration类被标记了@Profile,那么与该类关联的所有@Bean方法和@Import注解都将被忽略,除非一个或多个指定的配置文件(profile)处于激活状态。如果一个@Component或@Configuration类被标记为@Profile({"p1", "p2"}),那么除非“p1”或“p2”配置文件被激活,否则该类将不会被注册或处理。如果某个配置文件前缀带有NOT运算符(!),那么只有在该配置文件未激活时,带有该注解的元素才会被注册。例如,对于@Profile({"p1", "!p2"})的标注,当“p1”配置文件处于激活状态或“p2”配置文件未激活时,该类才会被注册。
@Profile 也可以在方法级别进行声明,以便仅包含配置类中的某个特定 Bean(例如,某个特定 Bean 的不同变体),如下例所示:
- Java
- Kotlin
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") 1
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") 2
public DataSource jndiDataSource() throws Exception {
Ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
standaloneDataSource方法仅在development配置文件中可用。jndiDataSource方法仅在production配置文件中可用。
@Configuration
class AppConfig {
@Bean("dataSource")
@Profile("development") 1
fun standaloneDataSource(): DataSource {
return EmbeddedDatabaseBuilder()
ertype(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build()
}
@Bean("dataSource")
@Profile("production") 2
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
standaloneDataSource方法仅在development配置文件中可用。jndiDataSource方法仅在production配置文件中可用。
当在@Bean方法上使用@Profile时,可能会出现一种特殊情况:如果存在同名的@Bean方法的重载版本(类似于构造函数的重载),那么需要在所有重载方法上都一致地声明@Profile条件。如果这些条件不一致,那么只有第一个声明中的条件才会被考虑。因此,不能通过@Profile来选择具有特定参数签名的重载方法而非另一个方法。对于同一个bean的所有工厂方法之间的选择,Spring会在创建时遵循其构造函数解析算法。
如果你想定义具有不同配置文件条件的备用bean,可以使用不同的Java方法名,并通过@Bean的名称属性来指向相同的bean名,如前面的示例所示。如果所有参数签名都相同(例如,所有变体都没有参数的工厂方法),那么从一开始这就是在有效的Java类中表示这种安排的唯一方式(因为一个特定的方法名和参数签名只能对应一个方法)。
XML Bean 定义配置文件
在XML中,对应的属性是<beans>元素的profile属性。我们之前的示例配置可以重写为两个XML文件,如下所示:
<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/> 元素,如下例所示:
<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>
spring-bean.xsd 已被限制为只允许这样的元素出现在文件的末尾。这应该有助于提供灵活性,同时不会在 XML 文件中造成混乱。
XML版本不支持前面提到的配置文件表达式。不过,可以使用!操作符来否定某个配置文件。此外,还可以通过嵌套配置文件来实现逻辑“与”操作,如下例所示:
<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="...">
<!-- 其他bean定义 -->
<beans profile="production">
<beans profile="us-east">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
</beans>
在上面的例子中,只有当production和us-east这两种配置文件都处于激活状态时,dataSource bean才会被创建和使用。
激活配置文件
现在我们已经更新了配置,但我们仍需要指定哪个配置文件是激活的。如果我们现在启动示例应用程序,将会看到一个NoSuchBeanDefinitionException被抛出,因为容器找不到名为dataSource的Springbean。
激活一个配置文件(profile)可以通过几种方式来完成,但最直接的方法是通过ApplicationContext提供的Environment API以编程方式来实现。以下示例展示了如何操作:
- Java
- Kotlin
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
environment.setActiveProfiles("development")
register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
refresh()
}
此外,您还可以通过spring.profiles.active属性来声明性地激活配置文件,该属性可以通过系统环境变量、JVM系统属性、web.xml中的servlet上下文参数,甚至作为JNDI中的一个条目来指定(参见PropertySource抽象)。在集成测试中,可以使用spring-test模块中的@ActiveProfiles注解来声明活跃的配置文件(参见使用环境配置文件的上下文配置)。
请注意,配置文件(profiles)并不是一个“非此即彼”的选择。你可以同时激活多个配置文件。通过编程方式,你可以向setActiveProfiles()方法提供多个配置文件名称,该方法接受String…类型的可变参数(varargs)。以下示例展示了如何同时激活多个配置文件:
- Java
- Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
如以下示例所示,spring.profiles.active可以接受用逗号分隔的配置文件名称列表:
-Dspring.profiles.active="profile1,profile2"
默认配置文件
默认配置文件表示在没有激活任何配置文件时所使用的配置文件。请考虑以下示例:
- Java
- Kotlin
@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();
}
}
@Configuration
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
如果没有活动配置文件,则会创建dataSource。可以将其视为为一个或多个bean提供默认定义的一种方式。如果有任何配置文件被启用,则默认配置文件将不会生效。
默认配置文件的名称是default。你可以通过在Environment上使用setDefaultProfiles()来更改默认配置文件的名称,或者通过声明式的方式,使用spring.profiles.default属性来更改。
PropertySource 抽象
Spring的Environment抽象提供了一个可配置的属性源层次结构上的搜索操作。请考虑以下列表:
- Java
- Kotlin
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);
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("Does my environment contain the 'my-property' property? $containsMyProperty")
在前面的代码片段中,我们看到了一种高层次的方法来询问Spring当前环境中是否定义了my-property属性。为了回答这个问题,Environment对象会在一组PropertySource对象上进行搜索。PropertySource是对任何键值对来源的简单抽象,而Spring的StandardEnvironment配置了两个PropertySource对象——一个代表JVM系统属性集(System_properties()),另一个代表系统环境变量集(System.getenv())。
这些默认的属性源存在于StandardEnvironment中,用于独立应用程序。StandardServletEnvironment还包含了其他默认的属性源,包括servlet配置、servlet上下文参数,如果可用的话,还包括一个JndiPropertySource。
具体来说,当您使用StandardEnvironment时,如果运行时存在名为my-property的系统属性或环境变量,那么调用env.containsProperty("my-property")将返回true。
执行的搜索是分层的。默认情况下,系统属性的优先级高于环境变量。因此,如果在调用 env.getProperty("my-property") 时,my-property 属性同时在两个地方被设置了,那么系统属性的值会被优先返回。需要注意的是,属性值并不会被合并,而是会被前面的设置完全覆盖。
对于常见的 StandardServletEnvironment,其完整层次结构如下(优先级最高的设置在最上面):
- ServletConfig 参数(如果适用——例如,在
DispatcherServlet的上下文中) - ServletContext 参数(
web.xml中的context-param设置) - JNDI 环境变量(以
java:comp/env/开头的设置) - JVM 系统属性(通过
-D命令行参数设置) - JVM 系统环境(操作系统环境变量)
最重要的是,整个机制是可配置的。也许你有一个自定义的属性来源,希望将其集成到这个搜索中。为此,实现并实例化你自己的 PropertySource,然后将其添加到当前 Environment 的 PropertySources 集合中。以下示例展示了如何操作:
- Java
- Kotlin
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在上面的代码中,MyPropertySource 被设置为搜索中的最高优先级。如果它包含一个名为 my-property 的属性,那么这个属性将会被检测到并返回,优先于其他任何 PropertySource 中的相同名称的属性。MutablePropertySources API 提供了多种方法,可以用来精确地操作这些属性源的集合。
使用 @PropertySource
@PropertySource 注解提供了一种方便且声明式的机制,用于向 Spring 的 Environment 中添加 PropertySource。
给定一个名为app.properties的文件,其中包含键值对testbean.name=myTestBean,以下@Configuration类以这样的方式使用@PropertySource:调用testBean.getName()时会返回myTestBean:
- Java
- Kotlin
@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;
}
}
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
在@PropertySource资源位置中出现的任何 ${…}占位符,都会根据环境中已经注册的属性源集合进行解析,如下例所示:
- Java
- Kotlin
@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;
}
}
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
假设 my.placeholder 已经存在于已注册的属性源之一中(例如,系统属性或环境变量),则该占位符将被解析为相应的值。如果不存在,则使用 default/path 作为默认值。如果没有指定默认值且无法解析某个属性,将会抛出一个 IllegalArgumentException。
@PropertySource 可以用作可重复的注解。@PropertySource 也可以用作元注解,用于创建具有属性覆盖功能的自定义组合注解。
语句中的占位符解析
历史上,元素中占位符的值只能通过JVM系统属性或环境变量来解析。但现在情况不再如此。由于Environment抽象层在整个容器中得到了集成,因此可以很容易地通过它来路由占位符的解析过程。这意味着你可以以任何你喜欢的方式配置解析过程。你可以改变搜索系统属性和环境变量的优先级,或者完全不使用它们。根据需要,你还可以添加自己的属性源。
具体来说,以下代码无论customer属性在何处定义,只要它在Environment中是可用的,就能正常工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>