环境抽象

Environment 接口是一个集成在容器中的抽象,它建模了应用程序环境的两个关键方面:配置文件属性

配置文件是命名过的、逻辑上的 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 方法上使用 @Profile,可能会出现一种特殊情况:在具有相同 Java 方法名称(类似于构造函数重载)的重载 @Bean 方法的情况下,需要在所有重载方法上一致地声明 @Profile 条件。如果条件不一致,则只有重载方法中第一个声明的条件才重要。因此,不能使用 @Profile 来选择具有特定参数签名的重载方法。同一 bean 的所有工厂方法之间的解析遵循 Spring 在创建时的构造函数解析算法。

如果您想使用不同的配置文件条件定义替代 bean,请使用不同的 Java 方法名称,通过 @Bean 名称属性指向相同的 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 对应物不支持前面描述的配置文件表达式。但是,可以使用 ! 运算符否定配置文件。也可以通过嵌套配置文件来应用逻辑“与”,如以下示例所示:

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

在前面的示例中,如果 productionus-east 配置文件都处于活动状态,则会暴露 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 中的条目来指定(请参阅PropertySource 抽象)。在集成测试中,可以使用 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("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。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 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 注解提供了一种方便且声明式的方式,将 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 也可用作元注解,创建具有属性覆盖的自定义组合注解。

语句中的占位符解析

历史上,元素中占位符的值只能针对 JVM 系统属性或环境变量进行解析。现在不再是这样了。由于 Environment 抽象已集成到整个容器中,因此很容易通过它来路由占位符的解析。这意味着您可以以任何您喜欢的方式配置解析过程。您可以更改通过系统属性和环境变量搜索的优先级,或者完全删除它们。您还可以根据需要添加自己的属性源。

具体来说,无论 customer 属性定义在哪里,只要它在 Environment 中可用,以下语句都有效:

<beans>
	<import resource="com/bank/service/${customer}-config.xml"/>
</beans>
© . This site is unofficial and not affiliated with VMware.