Environment 接口
Environment
接口是集成在容器中的一个抽象,
用于模拟应用程序环境的两个关键方面:profiles
和 properties。
配置文件是一个命名的、逻辑上的 bean 定义组,只有在给定配置文件处于活动状态时才会向容器注册。
无论是在 XML 中定义还是使用注解,都可以将 bean 分配给配置文件。Environment
对象与配置文件的关系在于确定当前激活的配置文件(如果有)以及默认情况下应该激活的配置文件(如果有)。
属性在几乎所有应用程序中都起着重要作用,可以来自各种来源:属性文件、JVM 系统属性、系统环境变量、
JNDI、mockApi 上下文参数、临时 Properties
对象、Map
对象等等。Environment
对象与属性的关系在于为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。
Bean Definition Profiles
Bean 定义配置文件提供了一个机制,允许在不同的环境中注册不同的 bean。“environment” 这个词对不同的用户可能有不同的含义,而这个功能可以帮助解决许多用例,包括:
-
在开发环境中使用内存数据源,而在 QA 或生产环境中从 JNDI 中查找相同的数据源。
-
仅在将应用程序部署到性能环境时注册监控基础设施。
-
为客户 A 与客户 B 的部署注册定制的 bean 实现。
考虑到第一个用例,在需要 DataSource
的实际应用程序中。在测试环境中,配置可能如下所示:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
现在考虑将该应用程序部署到 QA 或生产环境中,假设该应用程序的数据源已在生产应用服务器的 JNDI 目录中注册。我们的 dataSource
bean 现在如下所示:
@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
问题在于如何根据当前环境在这两种变体之间进行切换。随着时间的推移,基础设施用户已经想出了许多方法来完成这项任务,
通常依赖于一组系统环境变量和包含 ${placeholder}
令牌的 XML <import/>
语句,
这些令牌会根据环境变量的值解析为正确的配置文件路径。Bean 定义配置文件是提供这个问题解决方案的核心容器功能。
如果我们概括上述示例中显示的特定环境 bean 定义的用例,我们最终需要在某些上下文中注册特定的 bean 定义, 但在其他上下文中不需要。你可以说在情况 A 中注册特定配置文件的 bean 定义配置文件,并在情况 B 中注册不同的配置文件。我们首先更新配置以反映这种需求。
使用 @Profile
注解
@Profile
注解允许您指示在一个或多个指定的配置文件处于活动状态时,
组件是否符合注册条件。使用我们之前的示例,我们可以将 dataSource
配置重写如下:
@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("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 = "") 禁用了 destroyMethod 默认行为. |
如前所述,使用 @Bean 方法时,通常选择使用编程方式的 JNDI 查找,可以使用 Infra
的 JndiTemplate /JndiLocatorDelegate 辅助工具或之前展示的直接 JNDI InitialContext 使用,
但不能使用 JndiObjectFactoryBean 变体,因为这会强制您将返回类型声明为 FactoryBean 类型。
|
配置文件字符串可以包含简单的配置文件名称(例如 production
)或配置文件表达式。
配置文件表达式允许表达更复杂的配置文件逻辑(例如 production & us-east
)。配置文件表达式支持以下运算符:
-
!
:配置文件的逻辑NOT
-
&
:配置文件的逻辑AND
-
|
:配置文件的逻辑OR
You cannot mix the & and | operators without using parentheses. For example,
production & us-east | eu-central is not a valid expression. It must be expressed as
production & (us-east | eu-central) .
|
You can use @Profile
as a meta-annotation for the purpose
of creating a custom composed annotation. The following example defines a custom
@Production
annotation that you can use as a drop-in replacement for
@Profile("production")
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果不使用括号,不能混合使用 & 和 | 运算符。例如,production & us-east | eu-central
不是一个有效的表达式。必须表达为 production & (us-east | eu-central) 。
|
您可以将 @Profile
用作 元注解,
以创建自定义的组合注解。以下示例定义了一个自定义的 @Production
注解,您可以将其用作 @Profile("production")
的替代:
@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 配置文件中可用。 |
当在 如果您想要使用不同的配置条件定义替代 bean,请使用不同的 Java 方法名称,这些名称通过使用 |
使用 XML 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 对应部分不支持之前描述的 profile 表达式。但是,可以通过使用
在前面的例子中,只有当 |
激活 Profile
现在我们已经更新了配置,但我们仍然需要告诉 Infra 哪个 profile 是激活的。
如果我们现在启动示例应用程序,将会看到一个 NoSuchBeanDefinitionException
异常被抛出,
因为容器找不到名为 dataSource
的 Infra bean。
激活一个 profile 可以通过几种方式来完成,但最直接的方式是通过针对 Environment
API 进行编程,该 API 可通过 ApplicationContext
获取。以下示例展示了如何做到这一点:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
此外,您还可以通过 infra.profiles.active
属性声明式地激活 profiles,该属性可以通过系统环境变量、
JVM 系统属性、web.xml
中的 mockApi 上下文参数,甚至是 JNDI 条目指定
(参见 PropertySource
接口)。
在集成测试中,可以通过在 today-test
模块中使用 @ActiveProfiles
注解来声明活动的 profiles
(参见 环境 profiles 的上下文配置)。
请注意,profiles 不是“要么这样,要么那样”的命题。您可以一次激活多个 profiles。在编程方式中,
您可以向 setActiveProfiles()
方法提供多个 profile 名称,该方法接受 String…
可变参数。以下示例激活了多个 profiles:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
在声明方式下,infra.profiles.active
可以接受逗号分隔的 profile 名称列表,如下例所示:
-Dinfra.profiles.active="profile1,profile2"
默认 Profile
默认 profile 表示如果没有激活任何 profile,则启用的 profile。考虑以下示例:
@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();
}
}
如果没有激活任何 profile(参见 没有 profile 被激活),
则会创建 dataSource
。您可以将其视为为一个或多个 bean 提供默认定义的一种方式。如果启用了任何 profile,则默认 profile 不适用。
默认 profile 的名称是 default
。您可以通过在 Environment
上使用 setDefaultProfiles()
或者在声明方式下使用 infra.profiles.default
属性来更改默认 profile 的名称。
PropertySource
接口
Infra Environment
抽象提供了对可配置属性源层次结构的搜索操作。请考虑以下清单:
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);
在上述片段中,我们看到了一种高层次的方法,询问 Infra 是否对当前环境定义了 my-property 属性。 为了回答这个问题,Environment 对象对一组 PropertySource 对象进行搜索。 PropertySource 是对任何键值对源的简单抽象,而 Infra StandardEnvironment 配置了两个 PropertySource 对象 — 一个代表 JVM 系统属性 (System.getProperties()),另一个代表系统环境变量 (System.getenv())。
这些默认属性源适用于 StandardEnvironment ,用于独立应用程序。
StandardServletEnvironment
会填充额外的默认属性源,包括 mockApi 配置、mockApi 上下文参数,以及如果 JNDI 可用,则会添加一个
JndiPropertySource。
|
具体来说,当您使用 StandardEnvironment
时,调用 env.containsProperty("my-property")
如果在运行时存在 my-property
系统属性或 my-property
环境变量,则会返回 true。
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用
对于通用的
|
最重要的是,整个机制是可配置的。也许您有自定义的属性源想要集成到这个搜索中。
要实现这一点,实现并实例化您自己的 PropertySource
,并将其添加到当前 Environment
的 PropertySources
集合中。以下示例展示了如何实现:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在前面的代码中,MyPropertySource
已经以最高优先级添加到搜索中。如果它包含一个 my-property
属性,
则该属性将被检测并返回,而不考虑任何其他 PropertySource
中的 my-property
属性。
MutablePropertySources
API 公开了许多方法,允许精确地操作属性源
使用 @PropertySource
@PropertySource
注解提供了一个方便和声明性的机制,用于向 Infra Environment
添加 PropertySource
。
假设有一个名为 app.properties 的文件,其中包含键值对 testbean.name=myTestBean
,
以下 @Configuration
类使用 @PropertySource
,使得调用 testBean.getName()
返回 myTestBean
:
@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;
}
}
@PropertySource
资源位置中的任何 ${…}
占位符都将根据已注册到环境的属性源集合进行解析,如下例所示:
@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;
}
}
假设 my.placeholder
已经存在于已注册的某个属性源中(例如系统属性或环境变量),
则占位符将解析为相应的值。如果没有找到,则默认使用 default/path
。如果未指定默认值并且无法解析属性,
则会抛出 IllegalArgumentException
异常。
@PropertySource 可以作为可重复的注解使用。@PropertySource 也可以作为元注解使用,
以创建具有属性覆盖的自定义组合注解。
|
占位符解析
在历史上,元素中占位符的值只能针对 JVM 系统属性或环境变量进行解析。但现在情况已经不同了。
由于 Environment
抽象已经整合到容器中,因此很容易通过它来路由占位符的解析。
这意味着您可以按照任何您喜欢的方式配置解析过程。您可以更改通过系统属性和环境变量搜索的优先顺序,
或者完全删除它们。您还可以根据需要将自己的属性源添加到混合中。
具体来说,以下语句可以在 customer
属性定义的任何地方正常工作,只要它在 Environment
中可用:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>