3.13 环境抽象

优质
小牛编辑
127浏览
2023-12-01

3.13 环境抽象 {#toc_11}

在应用环境中,集成在容器的抽象环境模型有两个方面:profiles和properties。只有给出的profile被激活,一组逻辑命名的bean定义才会在容器中注册。无论是在XML中或者通过注解,bean都会被分配给一个profile。环境变量对象角色和profiles的关系来决定哪个profiles(如果有)处于当前激活状态,哪个profiles默认被激活。几乎在所有的应用中,Properties都扮演了一个重要的对象,这可能有各种来源:属性文件, JVM 系统属性文件,系统环境变量,JNDI,servlet上下文参数,属性查询对象,Maps等等。环境变量对象的角色和properties的关系用于配置属性并从中解析属性提供给用户一个便捷的服务接口。

3.13.1 Bean的profiles定义 {#toc_12}

Bean定义profiles是在核心容器中允许不同的bean在不同环境注册的机制。环境对于不同的用户意味着不同的东西,这个特性可以帮助许多用例,包括:

  • 在开发中不使用内存中的数据源 VS 在质量测试或生产环境中从JNDI查找相同的数据源。
  • 当把应用部署在可执行的环境中时注册监控基础架构
  • 对于客户A注册的自定义实现VS.客户B部署

让我首先考虑在一个需要数据源的应用中使用这个例子。在测试环境中,配置可能如下:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

让我现在考虑一下,如何把这个应用部署在测试环境或者生产环境中,假设应用所需的数据源将会被注册在生产应用环境中的JNDI目录。现在我们的数据源bean看起来像这样:

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

问题就是如何根据当前的环境在这两种变量之间进行切换。随着时间的推移,Spring的用户已经设计了很多种方法来实现此功能,通常依赖于系统环境变量和包含${placeholder}的XML 语句,根据环境变量的值可以解决正确的文件路径配置。Bean定义profiles是容器为了解决这个问题而提供的一个核心功能。

如果我们概括一下上面bean定义环境变量的示例,我们最终需要在特定的上下文中注册特定的bean,而不是其他的。你可以说你想要在情形A中注册一个特定的Bean定义的profile,在情形B中是另外一个。我们首先看下如何更新我们的配置以反映这种需求。

@Profile

当一个或者多个特定的profiles被激活,@Profile注解允许你指定一个有资格的组件来注册。使用我们上面的例子,我们可以按照下面的重写dataSource配置:

@Configuration
@Profile("dev")
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="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

如前所述,使用@Bean方法,通常会选择使用程序话的JNDI查找:要么使用Spring的 JndiTemplate/JndiLocatorDelegate帮助要么直接使用上面展示的JNDI InitialContext,而不是强制声明返回类型为FactoryBean的JndiObjectFactoryBean变体。

@Profile可以被用作为创建一个自定义组合注解的元注解。下面的例子定义了一个@Production注解,它可以被用作替换@Profile(“production”)的注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

在仅仅包含一个特殊bean的配置类中,@Profile也可以被声明在方法级别:

@Configuration
public class AppConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        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
    @Profile("production")
    public DataSource productionDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

如果一个@Configuration类被标记为@Profile,那么所有的@Bean方法和@Import注解相关的类都会被忽略,除非一个或多个特别的profiles被激活。如果一个@Component或@Configuration类被标记为@Profile({“p1”, “p2”}),那么这个类将不会被注册/处理,除非被标记为’p1’和/或’p2’的profiles已经被激活。如果给出的profile的前缀带有取反的操作符(!),那么注解的元素将会被注册,除非这个profile没有被激活。例如,给出@Profile({“p1”, “!p2”}),如果profile ‘p1’是激活状态或者profile ‘p2’不是激活状态的时候才会注册。

3.13.2 XML bean 定义 profiles {#toc_13}

XML对应元素的profile属性。我们上面的示例配置可以重写为下面的两个XML配置:

<beans profile="dev"
    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>
        <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 jndi-name="java:comp/env/jdbc/datasource"/>
</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="dev">
        <jdbc:embedded-database>
            <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 jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

spring-bean.xsd 约束允许这样的元素仅作为文件中的最后一个元素。这有助于XML的灵活性,且不会产生混乱。

激活profile

现在我们已经更新了我们的配置,我们仍然需要说明哪个profile是激活的。如果我们现在开始我们示例应用程序,我们将会看到一个NoSuchBeanDefinitionException被抛出,因为容器找不到一个名为dataSource的Spring bean。
激活一个profile可以通过多种方式完成,但是大多数情况下,最直接的办法就是通过存在ApplicationContext当中的环境变量的API进行编程:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

除此之外,profiles也可以通过声明spring.profiles.active属性来激活,这个可以通过在系统环境变量,JVM系统属性,web.xml中的servlet上下文环境参数,甚至JNDI的入口(请参考 3.13.3, “PropertySource abstraction”)。在集成测试中,激活profiles可以通过在Spring-test模块中的@ActiveProfiles注解来声明(参见“使用profiles来配置上下文环境”章节)。

注意,profiles不是“二者选一”的命题;它可以一次激活多个profiles。以编程的方式来看,简单的传递多个profile名字给接受String 可变变量参数的setActiveProfiles()方法:

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

在声明式中,spring.profiles.active可以接受以逗号分隔的profile 名称列表:

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

默认的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是激活状态,上面的dataSource将会被创建;这种方式可以被看做是对一个或者多个bean提供了一种默认的定义方式。如果启用任何的profile,那么默认的profile都不会被应用。
在上下文环境可以使用setDefaultProfiles()或者spring.profiles.default属性来修改默认的profile名字。

3.13.3 属性源抽象 {#toc_14}

Spring 环境抽象提供了可配置的属性源层次结构的搜索操作。为了充分的解释,请考虑下面的例子:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);

在上面的代码段中,我们看到了一个高级别的方法来要求Spring是否为当前环境定义foo属性。为了回答这个问题,环境对象对一组PropertySource对象执行搜索。一个PropertySource是对任何key-value资源的简单抽象,并且Spring 的标准环境是由两个PropertySource配置的,一个表示一系列的JVM 系统属性(System.getProperties()),一个表示一系列的系统环境变量(System.getenv())。

这些默认的属性资源存在于StandardEnvironment,可以在应用中独立使用。StandardServletEnvironment包含其他默认的属性资源,包括servlet配置和servlet上下文参数。它可以选择性的启用JndiPropertySource。详细信息请查看javadocs。

具体的说,当使用StandardEnvironment时,如果在运行时系统属性或者环境变量中包括foo,那么调用env.containsProperty(“foo”)方法将会返回true。

搜索是按照层级执行的。默认情况,系统属性优先于环境变量,所以这两个地方同时存在属性foo的时候,调用env.getProperty(“foo”)将会返回系统属性中的foo值。注意,属性值不会被合并而是被之前的值覆盖。对于一个普通的StandardServletEnvironment,它完整的层次结构如下,最顶端的优先级最高:

  • ServletConfig参数(如果适用,例如DispatcherServlet上下文环境)
  • ServletContext参数(web.xml中的context-param)
  • JNDI环境变量(“java:comp/env/”)
  • JVM系统属性(“-D”命令行参数)
  • JVM系统环境变量(操作系统环境变量)

更重要的是,整个机制都是可配置的。也许你有个自定义的属性来源,你想把它集成到这个搜到里面。这也没问题,只需简单的实现和实例化自己的PropertySource,并把它添加到当前环境的PropertySources集合中:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在上面的代码中,MyPropertySource被添加到搜索中的最高优先级。如果它包含了一个foo属性,在任何其他的PropertySource中的foo属性之前它会被检测到并返回。MutablePropertySources API暴露了很多允许精确操作该属性源集合的方法。

3.13.4 @PropertySource {#toc_15}

@PropertySource注解对添加一个PropertySource到Spring的环境变量中提供了一个便捷的和声明式的机制。
给出一个名为”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异常。

3.13.5 声明中的占位符解决方案 {#toc_15}

以前,元素中占位符的值只能被JVM系统熟悉或者环境变量解析。现在已经解决了这种情况。因为抽象的环境已经通过容器被集成了,很容易通过它来分配占位符。这意味着你可以使用任何你喜欢的方式配置:可以通过系统属性和环境变量来改变搜索优先级,或者完全删除它们;可以以适当的方式添加你自己混合属性资源。

具体来说,下面的声明无论customer属性被定义在哪里,只要它存在环境变量中就有作用: