环境抽象

Environment接口是集成在容器中的一个抽象,模拟应用环境的两个关键方面:profilesproperties

Profile是一个命名的逻辑bean定义组,仅在给定profile处于活动状态时才向容器注册。无论是在XML中定义还是使用注解,都可以将bean分配给一个profile。Environment对象与profiles的关系在于确定当前活动的profiles(如果有的话),以及默认情况下应该活动的profiles(如果有的话)。

Properties在几乎所有应用程序中都扮演着重要角色,可能来自各种来源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、临时Properties对象、Map对象等等。Environment对象与properties的关系在于为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。

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用户已经想出了许多方法来解决这个问题,通常依赖于一组系统环境变量和包含${placeholder}标记的XML <import/>语句,这些标记根据环境变量的值解析为正确的配置文件路径。 Bean定义配置文件是一个核心容器功能,提供了解决这个问题的解决方案。

如果我们概括前面示例中显示的特定环境bean定义的用例,我们最终需要在某些上下文中注册某些bean定义,而在其他上下文中不注册。您可以说您希望在情况A中注册一组特定的bean定义配置文件,在情况B中注册另一组不同的配置文件。我们首先更新我们的配置以反映这种需求。

Using @Profile

The @Profile annotation lets you indicate that a component is eligible for registration when one or more specified profiles are active. Using our preceding example, we can rewrite the dataSource configuration as follows:

  • 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 = "") disables default destroy method inference.
@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 = "") disables default destroy method inference.
As mentioned earlier, with @Bean methods, you typically choose to use programmatic JNDI lookups, by using either Spring’s JndiTemplate/JndiLocatorDelegate helpers or the straight JNDI InitialContext usage shown earlier but not the JndiObjectFactoryBean variant, which would force you to declare the return type as the FactoryBean type.

The profile string may contain a simple profile name (for example, production) or a profile expression. A profile expression allows for more complicated profile logic to be expressed (for example, production & us-east). The following operators are supported in profile expressions:

  • !: A logical NOT of the profile

  • &: A logical AND of the profiles

  • |: A logical OR of the profiles

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"):

  • Java

  • Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
If a @Configuration class is marked with @Profile, all of the @Bean methods and @Import annotations associated with that class are bypassed unless one or more of the specified profiles are active. If a @Component or @Configuration class is marked with @Profile({"p1", "p2"}), that class is not registered or processed unless profiles 'p1' or 'p2' have been activated. If a given profile is prefixed with the NOT operator (!), the annotated element is registered only if the profile is not active. For example, given @Profile({"p1", "!p2"}), registration will occur if profile 'p1' is active or if profile 'p2' is not active.

@Profile can also be declared at the method level to include only one particular bean of a configuration class (for example, for alternative variants of a particular bean), as the following example shows:

  • 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 The standaloneDataSource method is available only in the development profile.
2 The jndiDataSource method is available only in the 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 The standaloneDataSource method is available only in the development profile.
2 The jndiDataSource method is available only in the production profile.

@Bean方法上使用@Profile时,可能会出现特殊情况:在相同Java方法名称的重载@Bean方法的情况下(类似于构造函数重载),需要在所有重载方法上一致地声明@Profile条件。如果条件不一致,则只有在重载方法中第一个声明的条件才会起作用。因此,@Profile不能用于选择具有特定参数签名的重载方法而不是另一个方法。在创建时,同一bean的所有工厂方法之间的解析遵循Spring的构造函数解析算法。

如果要使用不同的配置条件定义替代bean,请使用指向相同bean名称的不同Java方法名称,并使用@Bean的name属性,如前面的示例所示。如果参数签名都相同(例如,所有变体都具有无参数工厂方法),这是在有效的Java类中表示这种安排的唯一方法(因为特定名称和参数签名的方法只能有一个)。

XML Bean Definition Profiles

XML Bean Definition Profiles(XML Bean定义配置文件)的对应部分是<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="...">

	<!-- 其他bean定义 -->

	<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。也可以通过嵌套profiles来应用逻辑“与”,如下例所示:

<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="...">

	<!-- 其他bean定义 -->

	<beans profile="production">
		<beans profile="us-east">
			<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
		</beans>
	</beans>
</beans>

在上面的示例中,如果productionus-east两个profiles都是活动的,则dataSource bean会被暴露。

激活配置文件

现在我们已经更新了我们的配置,我们仍然需要告诉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中的条目来指定。在集成测试中,可以使用spring-test模块中的@ActiveProfiles注解来声明活动配置文件(参见带环境配置文件的上下文配置)。

请注意,配置文件不是“要么...要么”的命题。您可以同时激活多个配置文件。在编程方式下,可以向setActiveProfiles()方法提供多个配置文件名称,该方法接受String…​可变参数。以下示例激活多个配置文件:

  • 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("当前环境是否包含'my-property'属性? " + containsMyProperty);
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("当前环境是否包含'my-property'属性? $containsMyProperty")

在上面的代码片段中,我们看到了一种高级的方式询问Spring当前环境是否定义了my-property属性。为了回答这个问题,Environment对象在一组PropertySource对象上执行搜索。一个PropertySource是对任何键值对源的简单抽象,Spring的StandardEnvironment配置了两个PropertySource对象,一个代表JVM系统属性的集合(System.getProperties()),另一个代表系统环境变量的集合(System.getenv())。

这些默认属性源适用于StandardEnvironment,用于独立应用程序。StandardServletEnvironment填充了额外的默认属性源,包括servlet配置、servlet上下文参数,以及如果JNDI可用,则包括一个JndiPropertySource

具体来说,当您使用StandardEnvironment时,调用env.containsProperty("my-property")如果在运行时存在my-property系统属性或my-property环境变量,则返回true。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用env.getProperty("my-property")my-property属性同时设置在两个地方,系统属性值“胜出”并返回。请注意,属性值不是合并的,而是被前面的条目完全覆盖。

对于常见的StandardServletEnvironment,完整的层次结构如下,优先级最高的条目在顶部:

  1. ServletConfig参数(如果适用 - 例如,在DispatcherServlet上下文中)

  2. ServletContext参数(web.xml context-param条目)

  3. JNDI环境变量(java:comp/env/条目)

  4. JVM系统属性(-D命令行参数)

  5. JVM系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个自定义的属性源要集成到这个搜索中。为此,实现并实例化自己的PropertySource,并将其添加到当前EnvironmentPropertySources集合中。以下示例显示了如何实现:

  • 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>