Environment 接口

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

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

如果您想要使用不同的配置条件定义替代 bean,请使用不同的 Java 方法名称,这些名称通过使用 @Bean 的名称属性指向相同的 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。 同时,也可以通过嵌套 profiles 来应用逻辑的 “and”,如下例所示:

<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 两个 profile 都处于激活状态时,dataSource bean 才会被暴露出来。

激活 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。

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

对于通用的 StandardMockEnvironment,完整的层次结构如下,优先级最高的条目位于顶部:

  • ServletConfig 参数(如果适用 — 例如,在 MockDispatcher 上下文中)

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

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

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

  • JVM 系统环境(操作系统环境变量)

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

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>