|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
环境抽象
Environment 接口是集成在容器中的一个抽象,用于建模应用环境的两个关键方面:配置文件(profiles)和 属性(properties)。
配置文件(profile)是一个命名的、逻辑上的 Bean 定义分组,仅在指定配置文件处于激活状态时才会向容器注册。无论 Bean 是通过 XML 还是注解定义的,都可以将其分配给某个配置文件。Environment 对象与配置文件相关的作用在于:确定当前哪些配置文件(如果有的话)处于激活状态,以及哪些配置文件(如果有的话)应默认处于激活状态。
属性在几乎所有应用程序中都扮演着重要角色,其来源多种多样:属性文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、临时的Properties对象、Map对象等等。Environment对象与属性相关的作用是为用户提供一个便捷的服务接口,用于配置属性源并从中解析属性。
Bean 定义配置文件
Bean 定义配置文件(Bean definition profiles)在核心容器中提供了一种机制,允许在不同的环境中注册不同的 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 用户已经设计出多种实现方式,通常依赖于系统环境变量与包含 <import/> 占位符的 XML ${placeholder} 语句相结合,这些占位符会根据环境变量的值解析为正确的配置文件路径。Bean 定义 Profiles 是 Spring 核心容器提供的一项功能,用于解决这一问题。
如果我们对前面示例中所示的环境相关 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");
}
}
| 1 | @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
}
}
| 1 | @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' 已被激活,否则该类不会被注册或处理。如果给定的配置文件前缀带有非运算符(!),则仅当该配置文件未激活时,带注解的元素才会被注册。例如,给定 @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");
}
}
| 1 | standaloneDataSource 方法仅在 development 配置文件中可用。 |
| 2 | 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
}
| 1 | standaloneDataSource 方法仅在 development 配置文件中可用。 |
| 2 | jndiDataSource 方法仅在 production 配置文件中可用。 |
|
在 如果你想定义具有不同 profile 条件的替代 bean,可以使用不同的 Java 方法名,并通过 |
XML Bean 定义配置文件
其对应的 XML 形式是 profile 元素的 <beans> 属性。我们前面的示例配置可以重写为以下两个 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 对应的配置不支持前面描述的 profile 表达式。然而,可以通过使用
在前面的示例中,如果 |
激活配置文件
现在我们已经更新了配置,但仍需告诉 Spring 哪个配置文件(profile)处于激活状态。如果我们现在启动示例应用程序,将会看到抛出一个 NoSuchBeanDefinitionException 异常,因为容器找不到名为 dataSource 的 Spring Bean。
可以通过多种方式激活一个配置文件(profile),但最直接的方式是通过 Environment 获取的 ApplicationContext 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 注解来声明活跃的配置文件(请参阅 使用环境配置文件的上下文配置)。
请注意,Profile 并非“非此即彼”的选项。你可以同时激活多个 Profile。在编程方式下,你可以向 setActiveProfiles() 方法传入多个 Profile 名称,该方法接受 String… 可变参数。以下示例激活了多个 Profile:
-
Java
-
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
以声明方式,spring.profiles.active 可以接受一个以逗号分隔的配置文件名称列表,如下例所示:
-Dspring.profiles.active="profile1,profile2"
默认配置档案
默认配置文件(default profile)表示默认启用的配置文件。请考虑以下示例:
-
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。你可以通过在 setDefaultProfiles() 上调用 Environment 方法,或者以声明式的方式使用 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 上下文参数,以及(如果 JNDI 可用)JndiPropertySource。 |
具体来说,当你使用 StandardEnvironment 时,如果在运行时存在名为 env.containsProperty("my-property") 的系统属性或 my-property 环境变量,则调用 my-property 将返回 true。
|
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 对于一个常见的
|
最重要的是,整个机制是可配置的。也许你有一个自定义的属性来源,希望将其集成到此搜索中。为此,请实现并实例化你自己的 PropertySource,并将其添加到当前 PropertySources 的 Environment 集合中。以下示例展示了如何实现这一点:
-
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 注解提供了一种便捷且声明式的机制,用于向 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 注解根据 Java 8 的约定是可重复的。
然而,所有此类 @PropertySource 注解必须在同一层级上声明,
要么直接声明在配置类上,要么作为元注解包含在同一个自定义注解中。
不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解。 |
语句中的占位符解析
历史上,元素中占位符的值只能通过 JVM 系统属性或环境变量进行解析。如今情况已不再如此。由于 Environment 抽象在整个容器中得到了集成,因此很容易通过它来路由占位符的解析过程。这意味着你可以按任意方式配置解析流程:可以更改系统属性和环境变量的搜索优先级,甚至完全移除它们;也可以根据需要将自己的属性源添加到解析过程中。
具体来说,只要 customer 属性在 Environment 中可用,以下语句无论该属性在何处定义都能正常工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>