环境抽象
Environment
接口是一个集成在容器中的抽象,用于建模应用程序环境的两个关键方面: profile 和 properties 。
Profile 是一个命名的逻辑 Bean 定义组,只有当给定的 profile 处于激活状态时,才会注册到容器中。无论是在 XML 中定义还是使用注解定义,Bean 都可以分配给一个 profile。Environment
对象与 profile 相关的作用在于确定哪些 profile(如果有)当前处于激活状态,以及哪些 profile(如果有)应该默认处于激活状态。
Properties 在几乎所有应用中都扮演着重要角色,并且可以来自各种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、servlet 上下文参数、临时的 Properties
对象、Map
对象等等。Environment
对象与 properties 相关的作用在于为用户提供一个便捷的服务接口,用于配置属性源并从中解析属性。
Bean 定义 Profile
Bean 定义 Profile 在核心容器中提供了一种机制,允许在不同的环境中注册不同的 Bean。“environment” 这个词对不同的用户可能意味着不同的东西,这个特性可以帮助处理许多用例,包括
-
在开发环境中使用内存数据库,而在 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 定义 Profile 是一个核心容器特性,为这个问题提供了解决方案。
如果我们概括前面示例中特定于环境的 Bean 定义的用例,我们会发现需要在某些上下文中注册某些 Bean 定义,而在其他上下文中则不需要。你可以说你想在情况 A 中注册某种 Bean 定义 Profile,在情况 B 中注册另一种 Profile。我们首先更新配置以反映此需求。
使用 `@Profile`
`@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 类型。 |
profile 字符串可以包含简单的 profile 名称(例如 production
)或 profile 表达式。profile 表达式允许表达更复杂的 profile 逻辑(例如 production & us-east
)。profile 表达式支持以下运算符
-
!
: profile 的逻辑非 -
&
: profile 的逻辑与 -
|
: profile 的逻辑或
不能不使用括号混合使用 & 和 | 运算符。例如,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`,除非指定的一个或多个 profile 处于激活状态,否则与该类关联的所有 `@Bean` 方法和 `@Import` 注解都将被跳过。如果 `@Component` 或 `@Configuration` 类标记了 `@Profile({"p1", "p2"})`,除非 profile 'p1' 或 'p2' 被激活,否则该类将不会被注册或处理。如果给定的 profile 前缀有 NOT 运算符 ( ! ),则只有当该 profile 不处于激活状态时,带注解的元素才会被注册。例如,给定 `@Profile({"p1", "!p2"})`,如果 profile 'p1' 处于激活状态或 profile '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 Profile 中可用。 |
2 | jndiDataSource 方法仅在 production Profile 中可用。 |
@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 Profile 中可用。 |
2 | jndiDataSource 方法仅在 production Profile 中可用。 |
在 `@Bean` 方法上使用 `@Profile` 时,可能会出现一种特殊情况:对于同名 Java 方法的 `@Bean` 方法重载(类似于构造函数重载),需要在所有重载方法上一致地声明 `@Profile` 条件。如果条件不一致,则只有重载方法中第一个声明上的条件有效。因此,不能使用 `@Profile` 来选择具有特定参数签名的一个重载方法而非另一个。同一 Bean 的所有工厂方法之间的解析遵循 Spring 在创建时的构造函数解析算法。 如果你想定义具有不同 Profile 条件的备选 Bean,请使用不同的 Java 方法名,并通过 `@Bean` 的 name 属性指向同一个 Bean 名,如前面示例所示。如果参数签名都相同(例如,所有变体都有无参数工厂方法),那么这是在有效的 Java 类中表示这种安排的唯一方法(因为特定名称和参数签名只能有一个方法)。 |
XML Bean 定义 Profile
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 对应方不支持前面描述的 profile 表达式。然而,可以使用
在前面的示例中,如果 |
激活 Profile
现在我们已经更新了配置,仍然需要指示 Spring 哪个 Profile 处于激活状态。如果立即启动示例应用程序,我们会看到抛出 NoSuchBeanDefinitionException
,因为容器找不到名为 dataSource
的 Spring Bean。
激活 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
属性声明性地激活 Profile,该属性可以通过系统环境变量、JVM 系统属性、web.xml
中的 servlet 上下文参数甚至 JNDI 中的条目来指定(参见 PropertySource
抽象 )。在集成测试中,可以使用 spring-test
模块中的 `@ActiveProfiles` 注解来声明活动的 Profile(参见 使用环境 Profile 的上下文配置 )。
注意,Profile 并非“非此即彼”的选择。你可以同时激活多个 Profile。通过编程方式,你可以向接受 String…
可变参数的 setActiveProfiles()
方法提供多个 Profile 名称。以下示例激活了多个 Profile
-
Java
-
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
声明性地,spring.profiles.active
可以接受逗号分隔的 Profile 名称列表,如下例所示
-Dspring.profiles.active="profile1,profile2"
默认 Profile
默认 Profile 表示在没有 Profile 激活时启用的 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()
}
}
如果 没有 Profile 处于激活状态 ,则会创建 dataSource
。你可以将其视为为一或多个 Bean 提供默认定义的一种方式。如果任何 Profile 被启用,默认 Profile 将不适用。
默认 Profile 的名称是 default
。你可以通过在 Environment
上使用 setDefaultProfiles()
方法来更改默认 Profile 的名称,或者通过 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 config、servlet context 参数以及如果 JNDI 可用时的 JndiPropertySource 。 |
具体来说,当你使用 StandardEnvironment
时,如果在运行时存在 my-property
系统属性或 my-property
环境变量,调用 env.containsProperty("my-property")
将返回 true。
执行的搜索是分层次的。默认情况下,系统属性优先于环境变量。因此,如果在调用 对于常见的
|
最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想将其集成到这个搜索中。要做到这一点,实现并实例化你自己的 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
注解提供了一种方便的声明机制,用于向 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>