1.13 环境抽象

Environment接口是集成在容器中的抽象,它为应用程序环境的两个关键方面建模:profiles 和properties。

profile是一个命名的逻辑bean定义组,只有在给定的配置文件处于活动状态时,才会在容器中注册。bean可以分配给一个配置文件,无论是用XML定义的还是用注解定义的。与配置文件相关的环境对象的角色是确定哪些配置文件(如果有)当前处于活动状态,以及默认情况下哪些配置文件(如果有)应处于活动状态。

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

1.13.1 Bean定义Profiles

bean定义Profiles在核心容器中提供了一种机制,允许在不同环境中注册不同的bean。“环境”这个词对不同的用户来说可能意味着不同的东西,并且这个特性可以帮助处理许多用例,包括:

  • 在开发中处理内存中的数据源,而不是在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目录中。我们的数据源bean现在看起来如下所示:

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

问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring用户设计了许多实现这一点的方法,通常依赖于系统环境变量和包含$placeholder标记的XML<import/>语句的组合,这些标记根据环境变量的值解析为正确的配置文件路径。bean定义概要文件是一个核心容器特性,它为这个问题提供了解决方案。

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

使用@Profile

@Profile注解允许你指示一个组件在一个或多个指定的配置文件处于活动状态时可以注册。使用前面的示例,我们可以如下重写数据源配置:

@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="")
    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, 而不是JndiObjectFactoryBean, **这会让你的返回值变成FactoryBean。

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

  • ! 逻辑非

  • & 逻辑与

  • | 逻辑或

你不能不加括号来混合使用 & 和 | ,如production & us-east | eu-central 是错误的,应该production & (us-east | eu-central)。

可以将@Profile用作元注解,以创建自定义组合注解。以下示例定义了一个自定义的@Production注解,你可以将其用作@Profile(“production”)的插入替换:

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

如果@Configuration类被标记为@Profile,那么所有与该类关联的@Bean方法和@Import注解都将被忽略,除非一个或多个指定的概要文件处于活动状态。如果@Component或@Configuration类标记为@Profile(“p1”、“p2”),则除非激活了配置文件“p1”或“p2”,否则不会注册或处理该类。如果给定配置文件的前缀是not运算符(!),仅当配置文件未处于活动状态时,才会注册带注解的元素。例如,给定@Profile(“p1”,“!p2“”),如果配置文件“p1”处于活动状态或配置文件“p2”未处于活动状态,则会发生注册。

@Profile也可以用在方法级别用来包含一个特殊的bean或者配置类。如下所示:

@Configuration
public class AppConfig {

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

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

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

Profiles在XML中使用

可以在XML中使用profile属性,如下所示:

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

同样可以将两个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="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对应项不支持前面描述的配置文件表达式。但是,可以使用否定配置文件!操作元。也可以通过嵌套配置文件来应用逻辑“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>

上面的例子中,dataSource被用在production和us-east profiles。

激活Profile

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

激活一个概要文件可以用几种方法完成,但最简单的方法是通过应用程序上下文提供的环境API以编程方式完成。以下示例显示了如何执行此操作:

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

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

请注意,配置文件不是“要么或”命题。你可以一次激活多个配置文件。通过编程,你可以为setActiveProfiles()方法提供多个配置文件名,该方法接受String…​ varargs。以下示例激活多个配置文件:

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

在声明中,spring.profiles.active可以接受由逗号分隔的配置文件名列表,如下例所示:

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

默认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被激活,那么dataSource就会被创建,你可以看成创建bean的默认方式。如果其他的Profile被激活了,那么默认的Profile就不会被使用。

你可以在环境中使用SetDefaultProfiles()更改默认profile的名称,或者声明性地使用spring.profiles.default属性更改默认概要文件的名称。

1.13.2 PropertySource简介

Spring的Environment抽象在property sources的可配置层次结构上提供搜索操作。考虑以下列表:

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);

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

这些默认属性源使用StandardEnvironment,可在独立应用程序中使用。StandardServletEnvironment使用其他默认属性源(包括servlet config和servlet上下文参数)填充。它可以选择启用JndiPropertySource。有关详细信息,请参阅JavaDoc。

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

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

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

  • ServletConfig参数(如果适用-例如,对于DispatcherServlet上下文)

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

  • JNDI环境变量(Java:COMP/Env/条目)

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

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

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

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

在前面的代码中,MyPropertySource已在搜索中以最高优先级添加。如果它包含“我的属性”属性,则检测并返回该属性,而不是任何其他属性源中的“我的属性”属性。MutablePropertySources API公开了许多允许对属性源集进行精确操作的方法。

1.13.3 使用@PropertySource

@PropertySource注解提供了方便和声明式的机制为Spring的添加PropertySource。

假如一个app.properties包括key-value值,例如: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。

根据Java 8约定,@PropertySource注解是可重复的。但是,所有这样的@PropertySource注解都需要在同一级别声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。不建议混合使用直接注解和元注解,因为直接注解有效地覆盖了元注解。

1.13.4 语句中的占位符解析

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

具体来说,只要customer property在Environment中可用,无论customer property在何处定义,以下陈述都有效:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

最后更新于