Bean 定义配置文件在核心容器中提供了一种机制,允许在不同的环境中注册不同的 bean。「环境」 这个词对不同的用户来说意味着不同的东西,这个功能可以帮助许多用例,包括:

  • 在开发中针对内存中的数据源工作,而在 QA 或生产中从 JNDI 查找相同的数据源。
  • 仅在将应用程序部署到性能环境中时才注册监控基础设施。
  • 为客户 A 与客户 B 的部署注册定制的 bean 实现。

考虑一下实际应用中的第一个用例,它需要一个 DataSource。在一个测试环境中,配置可能类似于以下:

  1. @Bean
  2. public DataSource dataSource() {
  3. return new EmbeddedDatabaseBuilder()
  4. .setType(EmbeddedDatabaseType.HSQL)
  5. .addScript("my-schema.sql")
  6. .addScript("my-test-data.sql")
  7. .build();
  8. }

现在考虑如何将该应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录中。我们的 dataSource bean 现在看起来像下面的清单:

  1. @Bean(destroyMethod="")
  2. public DataSource dataSource() throws Exception {
  3. Context ctx = new InitialContext();
  4. return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
  5. }

问题是如何根据当前环境在使用这两种变化之间切换。随着时间的推移,Spring 用户已经设计出了许多方法来完成这一工作,通常依靠系统环境变量和 XML <import/>语句的组合,这些语句包含${placeholder}标记,根据环境变量的值解析到正确的配置文件路径。Bean 定义配置文件是一个核心的容器功能,为这个问题提供了一个解决方案。

如果我们把前面的例子中显示的环境特定的 Bean 定义的用例进行概括,我们最终需要在某些情况下注册某些 Bean 定义,但在其他情况下不需要。你可以说,你想在情况 A 中注册某种类型的 bean 定义,而在情况 B 中注册另一种类型的 bean 定义。

@Profile

@Profile 注解可以让你表明,当一个或多个指定的配置文件处于活动状态时,一个组件就有资格注册。使用我们前面的例子,我们可以重写数据源配置如下。

  1. @Configuration
  2. @Profile("development")
  3. public class StandaloneDataConfig {
  4. @Bean
  5. public DataSource dataSource() {
  6. return new EmbeddedDatabaseBuilder()
  7. .setType(EmbeddedDatabaseType.HSQL)
  8. .addScript("classpath:com/bank/config/sql/schema.sql")
  9. .addScript("classpath:com/bank/config/sql/test-data.sql")
  10. .build();
  11. }
  12. }
  1. @Configuration
  2. @Profile("production")
  3. public class JndiDataConfig {
  4. @Bean(destroyMethod="")
  5. public DataSource dataSource() throws Exception {
  6. Context ctx = new InitialContext();
  7. return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
  8. }
  9. }

:::info 如前所述,对于 @Bean 方法,你通常会选择使用程序化的 JNDI 查找,通过使用 Spring 的 JndiTemplate/JndiLocatorDelegate 助手或前面显示的直接使用 JNDI InitialContext ,但不使用 JndiObjectFactoryBean 变体,这将迫使你将返回类型声明为 FactoryBean 类型。 :::

配置文件字符串可以包含一个简单的配置文件名称(例如,production)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,production & us-east)。配置文件表达式中支持以下运算符:

  • !:逻辑 「不」
  • &: 逻辑「和」
  • |:逻辑「或则」

:::info 你不能在不使用括号的情况下混合使用 & 和 | 运算符。例如,production & us-east | eu-central 不是一个有效的表达。它必须表示为 production & (us-east | eu-central)。 :::

你可以使用 @Profile 作为元注解来创建一个自定义的组成注解。下面的例子定义了一个自定义的@Production 注解,你可以把它作为 @Profile("production")的直接替换。

  1. @Target(ElementType.TYPE)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Profile("production")
  4. public @interface Production {
  5. }

:::tips 如果一个 @Configuration 类被标记为 @Profile,所有与该类相关的 @Bean 方法和 @Import 注解都会被绕过,除非一个或多个指定的配置文件处于激活状态。如果一个 @Component 或 @Configuration 类被标记为 @Profile({"p1", "p2"}),该类不会被注册或处理,除非配置文件 「p1」 或 「p2 」已经被激活。如果一个给定的配置文件前缀为 NOT 操作符(!),那么只有在该配置文件没有激活的情况下,才会注册被注解的元素。例如,给定 @Profile({"p1", "!p2"}),如果 profile 「p1」 是激活的,或者 profile 「p2」不是激活的,注册将发生。 :::

@Profile 也可以在 方法层面上声明,以便只包括一个配置类的一个特定 Bean(例如,对于一个特定 Bean的备选变体),正如下面的例子所示:

  1. @Configuration
  2. public class AppConfig {
  3. @Bean("dataSource")
  4. @Profile("development") // 仅在 development 环境下有效
  5. public DataSource standaloneDataSource() {
  6. return new EmbeddedDatabaseBuilder()
  7. .setType(EmbeddedDatabaseType.HSQL)
  8. .addScript("classpath:com/bank/config/sql/schema.sql")
  9. .addScript("classpath:com/bank/config/sql/test-data.sql")
  10. .build();
  11. }
  12. @Bean("dataSource")
  13. @Profile("production") // 仅在 production 环境下有效
  14. public DataSource jndiDataSource() throws Exception {
  15. Context ctx = new InitialContext();
  16. return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
  17. }
  18. }

:::info 对于 @Bean 方法的 @Profile,一个特殊的情况可能适用。在同一 Java 方法名的重载 @Bean 方法的情况下(类似于构造函数重载),@Profile 条件需要在所有重载的方法上一致声明。如果条件不一致,那么在重载的方法中,只有第一个声明的条件才是重要的。因此,@Profile 不能被用来选择具有特定参数签名的重载方法而不是另一个。同一个 Bean 的所有工厂方法之间的解析遵循 Spring 在创建时的构造器解析算法。

如果你想用不同的配置文件条件来定义替代的 Bean,请使用不同的 Java 方法名,通过使用 @Bean 名称属性指向同一个 Bean 名称,如前面的例子所示。如果参数签名都是一样的(例如,所有的变体都有无参数的工厂方法),这是首先在一个有效的 Java 类中表示这种安排的唯一方法(因为一个特定名称和参数签名的方法只能有一个)。 :::

XML Bean 定义 Profiles

XML 的对应部分是 <beans>元素的profile属性。我们前面的配置样本可以用两个 XML 文件重写,如下:

  1. <beans profile="development"
  2. xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  5. xsi:schemaLocation="...">
  6. <jdbc:embedded-database id="dataSource">
  7. <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
  8. <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
  9. </jdbc:embedded-database>
  10. </beans>
  1. <beans profile="production"
  2. xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:jee="http://www.springframework.org/schema/jee"
  5. xsi:schemaLocation="...">
  6. <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
  7. </beans>

也可以避免这种分割,在同一个文件中嵌套 <beans/>元素,如下例所示:

  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  4. xmlns:jee="http://www.springframework.org/schema/jee"
  5. xsi:schemaLocation="...">
  6. <!-- other bean definitions -->
  7. <beans profile="development">
  8. <jdbc:embedded-database id="dataSource">
  9. <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
  10. <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
  11. </jdbc:embedded-database>
  12. </beans>
  13. <beans profile="production">
  14. <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
  15. </beans>
  16. </beans>

spring-bean.xsd 已经被限制了,只允许在文件中最后出现这样的元素。这应该有助于提供灵活性,而不会造成 XML 文件的混乱。

对应的 XML 不支持前面描述的配置文件表达式。然而,可以通过使用 !操作符来否定一个配置文件。也可以通过嵌套配置文件来应用逻辑上的 &,正如下面的例子所示:

  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  4. xmlns:jee="http://www.springframework.org/schema/jee"
  5. xsi:schemaLocation="...">
  6. <!-- other bean definitions -->
  7. <beans profile="production">
  8. <beans profile="us-east">
  9. <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
  10. </beans>
  11. </beans>
  12. </beans>

在前面的例子中,如果 production 和 us-east 配置文件都处于活动状态,那么 dataSource Bean 就会被暴露。

激活一个配置文件(Activating a Profile)

现在我们已经更新了我们的配置,我们仍然需要指示 Spring 哪个配置文件是活动的。如果我们现在启动我们的示例应用程序,我们会看到一个 NoSuchBeanDefinitionException 被抛出,因为容器找不到名为 dataSource 的 Spring Bean。

激活一个配置文件可以通过几种方式进行,但最直接的方式是针对环境 API 以编程方式进行,它可以通过ApplicationContext 获得。下面的例子显示了如何做到这一点:

  1. AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
  2. ctx.getEnvironment().setActiveProfiles("development"); // 设置激活的配置文件
  3. ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
  4. ctx.refresh();

此外,你还可以通过 spring.profiles.active属性声明性地激活配置文件,它可以通过系统环境变量、JVM 系统属性、web.xml 中的 servlet 上下文参数,甚至作为 JNDI 中的一个条目来指定(参见 PropertySource 抽象)。在集成测试中,可以通过使用 spring-test 模块中的 @ActiveProfiles 注解来声明活动配置文件(见环境配置文件的上下文配置)。

请注意,配置文件不是一个 「非此即彼」的命题。你可以同时激活多个配置文件。在程序上,你可以向setActiveProfiles()方法提供多个配置文件名称,该方法接受 String...varargs。下面的例子激活了多个配置文件:

  1. ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

在声明上,spring.profiles.active可以接受一个用逗号分隔的配置文件名称列表,正如下面的例子所示:

  1. -Dspring.profiles.active="profile1,profile2"

默认的 Profile

默认配置文件代表默认启用的配置文件。考虑一下下面的例子:

  1. @Configuration
  2. @Profile("default")
  3. public class DefaultDataConfig {
  4. @Bean
  5. public DataSource dataSource() {
  6. return new EmbeddedDatabaseBuilder()
  7. .setType(EmbeddedDatabaseType.HSQL)
  8. .addScript("classpath:com/bank/config/sql/schema.sql")
  9. .build();
  10. }
  11. }

如果没有激活配置文件,就会创建数据源。你可以把它看作是为一个或多个 Bean 提供默认定义的一种方式。如果任何配置文件被启用,默认的配置文件就不适用。

你可以通过在环境中使用 setDefaultProfiles()来改变默认配置文件的名称,或者通过声明性地使用 spring.profiles.default 属性。