环境抽象
Environment 接口是一个集成在容器中的抽象,建模了应用环境的两个关键方面: profiles 和 properties。
一个配置文件是一个命名的、逻辑上的 bean 定义组,仅在给定的配置文件处于活动状态时才会注册到容器中。无论是通过 XML 定义还是使用注解,bean 都可以被分配到一个配置文件中。Environment
对象在与配置文件相关的作用是确定当前哪些配置文件(如果有的话)处于活动状态,以及默认情况下应该激活哪些配置文件(如果有的话)。
属性在几乎所有应用程序中都扮演着重要角色,并且可能来自多种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数、临时 Properties
对象、Map
对象等等。Environment
对象与属性的关系是为用户提供一个便捷的服务接口,以便配置属性源并从中解析属性。
Bean 定义配置文件
Bean 定义配置文件提供了一种机制,在核心容器中允许在不同环境中注册不同的 bean。 “环境”一词对不同的用户可能意味着不同的东西,这个功能可以帮助处理许多用例,包括:
-
在开发中使用内存数据源,而在 QA 或生产环境中从 JNDI 查找相同的数据源。
-
仅在将应用程序部署到性能环境时注册监控基础设施。
-
为客户 A 和客户 B 部署注册定制的 bean 实现。
考虑在实际应用中第一个需要 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 用户想出了多种方法来实现这一点,通常依赖于系统环境变量和包含 ${placeholder}
令牌的 XML <import/>
语句的组合,这些令牌根据环境变量的值解析为正确的配置文件路径。Bean 定义配置文件是一个核心容器特性,提供了这个问题的解决方案。
如果我们将前面示例中环境特定的 bean 定义的用例进行概括,我们最终会需要在某些上下文中注册特定的 bean 定义,而在其他上下文中则不需要。你可以说你想在情况 A 中注册某个配置文件的 bean 定义,而在情况 B 中注册不同的配置文件。我们首先更新我们的配置以反映这一需求。
使用 @Profile
@Profile 注解允许您指示一个组件在一个或多个指定的配置文件处于活动状态时可以注册。使用我们之前的示例,我们可以将 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 {
Context 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 ctx.lookup("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
注解都会被跳过,除非一个或多个指定的配置文件处于活动状态。如果一个 @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 {
Context 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()
.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
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
standaloneDataSource
方法仅在development
配置文件中可用。jndiDataSource
方法仅在production
配置文件中可用。
在 @Bean
方法上使用 @Profile
时,可能会出现一个特殊场景:在同一 Java 方法名称的重载 @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。
激活一个配置文件
现在我们已经更新了配置,我们仍然需要指示 Spring 哪个配置文件是活动的。如果我们现在启动我们的示例应用程序,我们会看到抛出 NoSuchBeanDefinitionException
,因为容器无法找到名为 dataSource
的 Spring bean。
激活一个配置文件可以通过几种方式完成,但最简单的方法是通过 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 Abstraction)。在集成测试中,可以通过在 spring-test
模块中使用 @ActiveProfiles
注解来声明活动配置文件(请参见 context configuration with environment profiles)。
注意,配置文件并不是“非此即彼”的选择。您可以同时激活多个配置文件。在程序中,您可以向 setActiveProfiles()
方法提供多个配置文件名称,该方法接受 String…
可变参数。以下示例激活多个配置文件:
- 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.getProperties()
),另一个表示系统环境变量集(System.getenv()
)。
这些默认属性源存在于 StandardEnvironment
中,用于独立应用程序。StandardServletEnvironment 还包含额外的默认属性源,包括 servlet 配置、servlet 上下文参数,以及一个 JndiPropertySource,如果 JNDI 可用的话。
具体来说,当你使用 StandardEnvironment
时,调用 env.containsProperty("my-property")
如果在运行时存在 my-property
系统属性或 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
中的 my-property
属性。MutablePropertySources API 提供了多个方法,允许对属性源集合进行精确操作。
使用 @PropertySource
@PropertySource 注解提供了一种方便且声明式的机制,用于将 PropertySource
添加到 Spring 的 Environment
中。
给定一个名为 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>