环境抽象
配置文件是命名的、逻辑上的 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 用户已经设计了许多方法来完成此操作,通常依赖于系统环境变量和 XML <import/>
语句的组合,这些语句包含 ${placeholder}
令牌,这些令牌根据环境变量的值解析为正确的配置文件路径。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");
}
}
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”,否则该类不会被注册或处理。如果给定配置文件以 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");
}
}
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 配置文件中可用。
|
当在 如果要定义具有不同配置文件条件的备选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对应的部分不支持前面描述的配置文件表达式。但是,可以通过使用
在前面的示例中,如果 |
激活配置文件
现在我们已经更新了配置,我们仍然需要指示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
抽象)。在集成测试中,可以通过在spring-test
模块中使用@ActiveProfiles
注解来声明活动配置文件(参见使用环境配置文件的上下文配置)。
请注意,配置文件不是“非此即彼”的命题。您可以同时激活多个配置文件。以编程方式,您可以向接受String…
可变参数的setActiveProfiles()
方法提供多个配置文件名称。以下示例激活多个配置文件
-
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上下文参数,以及如果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
注解提供了一种方便且声明性的机制,用于将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 也可以用作元注解,用于创建具有属性覆盖的自定义组合注解。 |