目录
1.13. Environment的抽象
Environment
是 Spring 容器中对于应用环境两个关键因素的一个抽象。它们分别是:Profiles
和 properties
。
Profile
是一个 bean 定义命名的逻辑分组。容器中可以配置多个 Profile
,但是需要指定 profile
才会把这个 Profile
下的分组 bean 定义注册到容器中。 无论 beans 是定义在 XML 还是 注解里面都可以分配给一个 Profile
。Environment
对象与 Profile
关系是确定哪些 Profiles(if any)
当前是有效的,哪些 Profiles
(如果有的话)应该在默认情况下是有效的。
Properties
在几乎所有应用程序中都扮演着重要的角色,并且可能来自各种各样的来源:properties 文件
、JVM系统属性
、系统环境变量
、JNDI
、Servlet Context 参数
、ad-hoc Properties 对象
、Map
等等。Environment
与 Properties
的关系是为用户提供一个方便的服务接口,用于配置属性源并从它们中解析属性。
1.13.1. Bean 定义Profiles
Bean定义配置文件在核心容器中提供了一种机制,该机制允许在不同环境中注册不同的Bean。 “environment”一词对不同的用户可能具有不同的含义,并且此功能可以帮助解决许多用例,包括:
-
在开发中针对内存中的数据源进行工作,而不是在进行QA或生产时从JNDI查找相同的数据源。
-
仅在将应用程序部署到性能环境中时注册监视基础结构。
-
为客户A和客户B部署注册bean的自定义实现。
考虑需要数据源的实际应用程序中的第一个用例。在测试环境中,配置可能类似于以下内容:
java
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
kotlin
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build()
}
现在,假设该应用程序的数据源已在生产应用程序服务器的JNDI目录中注册,请考虑如何将该应用程序部署到QA或生产环境中。 现在,我们的dataSource bean看起来像下面的列表:
java
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
kotlin
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
问题是如何根据当前环境在使用这两种变体之间进行切换。 随着时间的流逝,Spring用户已经设计出多种方法来完成此任务,通常依赖于系统环境变量和包含$ {placeholder}令牌的XML <import />语句的组合,这些语句根据值解析为正确的配置文件路径 环境变量。 Bean定义配置文件是一个核心容器功能,可提供此问题的解决方案。
如果我们概括前面特定于环境的Bean定义示例中所示的用例,那么最终需要在某些上下文中而不是在其他上下文中注册某些Bean定义。 您可能会说您要在情况A中注册一个特定的bean定义配置文件,在情况B中注册一个不同的配置文件。我们首先更新配置以反映这种需求。
使用@Profile
@Profile解可让您指示一个或多个指定的配置文件处于活动状态时有资格注册的组件。 使用前面的示例,我们可以如下重写dataSource配置:
java
@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();
}
}
kotlin
@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="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
kotlin
@Configuration
@Profile("production")
class JndiDataConfig {
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
}
如前所述,对于@Bean方法,您通常通过使用Spring的JndiTemplate/JndiLocatorDelegate帮助程序或前面显示的直接JNDI InitialContext 用法(而不是JndiObjectFactoryBean 变量)来选择使用编程JNDI查找,这将强制您将返回类型声明为FactoryBean 类型。
profile字符串可以包含一个简单的profile的名字(例如:production)或者一个profile表达式。profile表达式允许表达更复杂的profile逻辑(例如:production&us-east)。在profile表达式中支持下面的操作:
-
!
: profile的逻辑运算"not" -
&
: profile的逻辑运算"and" -
|
: profile的逻辑运算"or"
在没有使用括号是不能混淆使用&和|,例如 production & us-east | eu-central是一个无效的表达式。
你可以使用@Profile作为无注解来创建一个定制的组合注解。下面的示例定义一个自定义注解@Production,你可以使用它来替代
@Profile("production")
:
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
kotlin
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
如果
|
@Profile也可以在方法级别声明为仅包含一个配置类的特殊Bean(例如,特殊Bean的替代变量),如以下示例所示:
java
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development")
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")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
kotlin
@Configuration
class AppConfig {
@Bean("dataSource")
@Profile("development")
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")
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
对于@Bean方法上的@Profile,可能会应用一个特殊的场景:如果重载的@Bean方法具有相同的Java方法名(类似于构造函数重载),则需要在所有重载的方法上一致声明@Profile条件。如果条件不一致,则只有重载方法中第一个声明的条件才重要。因此,@Profile不能用于选择一个重载方法,该方法具有一个特定的参数签名。同一bean的所有工厂方法之间的解析在创建时遵循Spring的构造函数解析算法。
如果要定义具有不同Profile条件的可选bean,请使用不同的Java方法名,这些方法名使用@bean name属性指向同一个bean名,如前一个示例所示。如果参数签名都是相同的(例如,所有变量都没有arg factory方法),这是首先在有效的Java类中表示这种安排的唯一方法(因为只能有一个具有特定名称和参数签名的方法)。 |
XML Bean 定义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文件中的混乱。
The XML counterpart does not support the profile expressions described earlier. It is possible, however, to negate a profile by using the In the preceding example, the |
激活 Profile
现在我们已经更新了配置,我们仍然需要告诉Spring哪个profile是活动的。如果我们现在启动示例应用程序,就会看到抛出一个NoSuchBeanDefinitionException,因为容器找不到名为dataSource的Spring bean。
激活profile可以通过多种方式来完成,但最简单的方法是针对通过ApplicationContext提供的Environment
API进行编程。下面的示例演示了如何执行此操作:
java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
kotlin
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。 通过编程,您可以为setActiveProfiles()方法提供多个profile名称,该方法接受String ... 可变参数。 以下示例激活多个profile:
java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
声明性地,spring.profiles.active可以接受以逗号分隔的profile名列表,如下例所示:
-Dspring.profiles.active="profile1,profile2"
默认Profile
默认profile表示默认启用的profile。请考虑以下示例:
java
@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();
}
}
kotlin
@Configuration
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
如果没有profile处于活动状态,则创建数据源。您可以将其视为为为一个或多个bean提供默认定义的一种方式。如果启用了任何profile,则不应用默认profile。
您可以在 Environment
中使用setDefaultProfiles()或使用spring.profiles.default属性以声明方式更改默认profile 的名称。
1.13.2. PropertySource
抽象
Spring的 Environment
抽象了在可配置的属性源层次结构上提供搜索操作。请考虑以下列表:
java
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);
kotlin
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的标准环境配置有两个PropertySource对象,一个表示JVM系统属性集(System.getProperties()),另一个表示系统环境变量集(System.getenv())。
这些默认属性源用于StandardEnvironment,用于独立应用程序。StandardServletEnvironment填充了其他默认属性源,包括servlet配置和servlet上下文参数。它可以选择性地启用JndiPropertySource。有关详细信息,请参阅javadoc。
具体来说,当您使用StandardEnvironment时,如果在运行时存在my-property系统属性或my-property环境变量,则对env.containsProperty(“ my-property”)的调用将返回true。
执行的搜索是分层的。 默认情况下,系统属性优先于环境变量。 因此,如果在调用env.getProperty("my-property")
时在两个地方都同时设置了my-property属性,则系统属性值“ wins”并返回。 请注意,属性值不会合并,而是会被前面的条目完全覆盖。
对于常见的StandardServletEnvironment,完整的层次结构如下,最高优先级条目位于顶部:
-
ServletConfig 参数(如果适用 - 例如,对于DispatcherServlet上下文)
-
ServletContext 参数(web.xml context-param entries)
-
JNDI 环境变量 (
java:comp/env/
entries) -
JVM 系统属性 (
-D
命令行参数) -
JVM 系统环境(操作系统环境变量)
最重要的是,整个机制是可配置的。也许您有一个自定义的属性源,希望将其集成到此搜索中。为此,实现并实例化您自己的PropertySource,并将其添加到当前环境的PropertySource集合中。下面的示例演示了如何执行此操作:
java
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
kotlin
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在前面的代码中,MyPropertySource在搜索中以最高优先级添加。如果它包含my-property属性,则会检测并返回该属性,以支持任何其他PropertySource中的任何my-property属性。MutablePropertySources API公开了许多允许对属性源集进行精确操作的方法。
1.13.3. 使用@PropertySource
@PropertySource注解为将PropertySource添加到Spring环境中提供了一种方便且声明性的机制。
给定一个名为app.properties的文件,该文件包含键值对testbean.name=myTestBean,下面的@Configuration类使用@PropertySource的方式是调用testbean.getName()返回myTestBean:
java
@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;
}
}
kotlin
@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
@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;
}
}
kotlin
@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。
根据Java 8约定,@PropertySource注解是可重复的。 但是,所有此类@PropertySource注解都需要在同一级别上声明,可以直接在配置类上声明,也可以在同一自定义批注中声明为元注解。 不建议将直接注解和元注解混合使用,因为直接注解会有效地覆盖元注解。
1.13.4. 声明中的占位符解析
从历史上看,元素中占位符的值只能根据JVM系统属性或环境变量来解析。 这已不再是这种情况。 由于Environment
抽象是在整个容器中集成的,因此很容易通过它路由占位符的解析。 这意味着您可以按照自己喜欢的任何方式配置解析过程。 您可以更改搜索系统属性和环境变量的优先级,也可以完全删除它们。 您还可以根据需要将自己的属性源添加到组合中。
具体来说,无论customer
属性在何处定义,只要在Environment中可用,以下语句都有效:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>