Environment Abstraction
Environment
接口是集成在容器中的一个抽象,它对应用程序环境的两个关键方面进行建模: profiles 和 properties。
配置文件是一个具名的逻辑 bean 定义组,仅在给定的配置文件处于活动状态时才向容器注册。Bean 可以分配给配置文件,无论是否在 XML 中定义或使用注解定义。Environment
对象与配置文件之间的关系在于确定当前哪些配置文件(如果有)处于活动状态,以及哪些配置文件(如果有)应默认处于活动状态。
属性在几乎所有应用程序中都起着重要作用,并且可能源自各种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、Servlet 上下文参数、特别 Properties
对象、Map
对象等等。与属性相关的 Environment
对象的作用是为用户提供一个便捷的服务接口,用于配置属性源并从中解析属性。
Bean Definition Profiles
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 用户已经设计了许多实现此操作的方法,通常依赖于系统环境变量和 XML <import/>
语句的组合,其中包含根据环境变量的值解析为正确配置文件路径的 ${placeholder}
令牌。Bean 定义配置文件是一项核心容器功能,它提供了解决此问题的方法。
如果我们对前一个示例中显示的环境特定 bean 定义用例进行概括,最终会得出需要在特定上下文中注册某些 bean 定义但在其他上下文中不需要注册某些 bean 定义。您可以说您希望在情况 A 中注册某个 bean 定义配置文件,在情况 B 中注册另一个配置文件。我们首先更新我们的配置以反映此需求。
Using @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
-
@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 = "")`禁用了默认销毁方法推断。
|
2 | `@Bean(destroyMethod = "")`禁用了默认销毁方法推断。 |
如前文所述,对于 @{48} 方法,你通常选择使用Spring的@{49}/@{50}帮助程序或前面演示了直接JNDI @{51}使用法,通过编程的方式JNDI 查找,但不是@{52}变量,它会强制你将返回类型声明为@{53}类型。 |
配置文件字符串可能包含一个简单的配置文件名(例如,production
)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east
)。配置文件表达式中支持以下运算符:
-
!
:概要 `NOT`概要 -
&
:概要 `AND`概要 -
|
:概要 `OR`概要
你不能混合使用@{54}和@{55}运算符而不使用括号。例如,@{56}不是一个有效的表达式。它必须表示为@{57}。 |
你可以使用 @Profile
作为 meta-annotation 以便创建一个自定义组合注释。以下示例定义了一个自定义 @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
如果一个@{58}类用@{59}标记,则除非激活了一个或多个指定的配置文件,否则将绕过与该类关联的所有@{60}方法和@{61}注释。如果一个@{62}或@{63}类用@{64}标记,则除非激活了配置文件“p1”或“p2”,否则不注册或处理该类。如果给定的配置文件添加了NOT运算符前缀(@{65}),则仅当该配置文件未激活时才注册注释的元素。例如,给定@{66},如果配置文件“p1”处于活动状态或配置文件“p2”处于非活动状态,则会进行注册。 |
@Profile
也可以在方法级别声明,以便仅包含一个配置类的特定 Bean(例如,对于特定 Bean 的备用变体),如下例所示:
- Java
-
@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`概要中可用。
|
3 | `standaloneDataSource`方法仅在 `development`概要中可用。 |
4 | `jndiDataSource`方法仅在 `production`概要中可用。 |
使用 |
XML Bean Definition Profiles
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 等效项不支持前面描述的配置文件表达式。但是,可以通过使用
在前面的示例中,如果 |
Activating a Profile
现在我们已经更新了配置,我们仍然需要指示 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
模块(见 context configuration with environment profiles)中的 @ActiveProfiles
注释来声明活动概要文件。
请注意,配置文件不是一个“要么-要么
”命题。你可以同时激活多个配置文件。以编程方式,你可以向接受 String…
vararg 的 setActiveProfiles()
方法提供多个配置文件名称。以下示例激活多个配置文件:
-
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()
}
}
如果 [没有处于活动状态的配置文件,beans-definition-profiles-enable],则创建 dataSource
。你可以将其视为提供一个或多个 Bean 的默认定义的方法。如果启用了任何配置文件,则默认配置文件不适用。
默认配置文件的名称是 default
。你可以通过对 Environment
使用 setDefaultProfiles()
或通过声明方式使用 spring.profiles.default
属性来更改默认配置文件的名称。
PropertySource
Abstraction
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
时,如果在运行时存在 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`属性。https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/env/MutablePropertySources.html[`MutablePropertySources
] API 公开了许多方法,允许对属性源的集合进行精确控制。
Using @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
。
|
Placeholder Resolution in Statements
过去,元素中的占位符的值只能针对 JVM 系统属性或环境变量进行解析。现在不再是这样。由于 Environment
抽象集成在整个容器中,因此很容易通过它来路由占位符解析。这意味着您可以随心所欲地配置解析过程。您可以更改通过系统属性和环境变量进行搜索的优先级,或完全删除它们。您还可以根据需要将自己的属性源添加到组合中。
具体来说,以下语句无论 customer
属性在何处定义都没有关系,只要它在 Environment
中可用:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>