环境的抽象

Environment是一个集成到容器之中的特殊抽象,它针对应用的环境建立了两个关键的概念:profileproperties.

profile是命名好的,其中包含了多个Bean的定义的一个逻辑集合,只有当指定的profile被激活的时候,其中的Bean才会激活。无论是通过XML定义的还是通过注解解析的Bean都可以配置到profile之中。而Environment对象的角色就是跟profile相关联,然后决定来激活哪一个profile,还有哪一个profile为默认的profile。

properties在几乎所有的应用当中都有着重要的作用,当然也可能存在多个数据源:property文件,JVM系统property,系统环境变量,JNDI,servlet上下文参数,ad-hoc属性对象,Map等。Environment对象和property相关联,然后来给开发者一个方便的服务接口来配置这些数据源,并正确解析。

Bean定义的profile

在容器之中,Bean定义profile是一种允许不同环境注册不同bean的机制。环境的概念就意味着不同的Bean对应不同的开发者,而且这个特性在以下场景使用十分便利:

  • 解决一些内存中的数据源的问题,可以在不同环境访问不同的数据源,开发环境,QA测试环境,生产环境等。
  • 仅仅在开发环境来使用一些监视服务
  • 在不同的环境,使用不同的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(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题就是如何基于当前的环境来使用不同的配置。在以前,Spring的开发者开发了很多的方法来解决这个问题,通常都依赖于系统环境变量和XML中的<import/>标签以及占位符${placeholder}等来根据不同的环境解析当前的配置文件。现在Bean的profile属于容器的特性,也是该问题的解决方案之一。

如果我们泛化了我们一些特殊环境下引用的bean定义,我们可以将其中指定的Bean注入到特定的context之中,而不是所有的context之中了。很多开发者就希望能够在一种环境下使用Bean定义A,另一种情况下使用Bean定义B。

@Profile注解

@Profile注解允许开发者来表示一个组件是否适合在当前环境来进行注册,只有当前的Profile是激活的时候,对应的Bean才会被注册到上下文中。使用前面的例子,代码可以进行如下调整:

@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");
    }
}

@Profile注解可以当做元注解来使用。比如,下面所定义的@Production注解就可以来替代@Profile("production"):

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

@Profile注解也可以在方法级别使用,可以声明在包含@Bean注解的方法之上:

@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注解的相关的类都会被传递为该Profile。除非这个Profile激活了,否则其中的Bean定义都不会激活。如果配置为@Component或者@Configuration的类标记了@Profile({"p1", "p2"}),那么这个类当且仅当Profile是p1或者p2的时候才会激活。如果某个Profile的前缀是!这个非操作符,那么@Profile注解的类会只有当前的Profile没有激活的时候才能生效。举例来说,如果配置为@Profile({"p1", "!p2"}),那么注册的行为会在Profile为p1或者是Profile为非p2的时候才会激活。

XML中Bean定义的profile

在XML中相对应配置是<beans/>中的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 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="dev">
        <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文件的配置提供更多便利。

激活profile

现在,我们已经更新了配置信息来使用环境抽象,但是我们还需要告诉Spring来激活具体哪一个Profile。如果我们直接启动应用的话,现在就回抛出NoSuchBeanDefinitionException异常,因为容器会找不到Spring的BeandataSource

有多种方法来激活一个Profile,最直接的方式就是通过编程的方式来直接调用EnvironmentAPI,ApplicationContext中包含这个接口:

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

额外的,Profile还可以通过spring.profiles.active中的属性来指定,可以通过系统环境变量,JVM系统变量,servlet上下文中的参数,甚至是JNDI的一个参数等来写入。在集成测试中,激活Profile可以通过spring-test中的@ActiveProfiles来实现。

需要注意的是,Profile的定义并不是一种互斥的关系,我们完全可以在同一时间激活多个Profile的。编程上来说,为setActiveProfile()方法提供多个Profile的名字即可:

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

也可以通过spring.profiles.active来指定逗号分隔的多个Profile的名字:

-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则不会激活。

默认的Profile的名字可以通过Environment中的setDefaultProfiles()方法或者是通过spring.profiles.default属性来更改。

属性源抽象

Spring的Environment的抽象提供了一些层次化的搜索选项,来配置的源信息。具体的内容,参考如下代码:

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

在上面的代码片段之中,我们看到一个high-level的查找Spring是否定义foo属性的一种方式。为了知道Spring中是否包含这个属性,Environment对象会针对PropertySource的集合进行查找。PropertySource是针对一些key-value的属性对的简单抽象,而Spring的StandardEnvironment是由两个PropertySource对象所组成的,一个代表的是JVM的系统属性(可以通过System.getProperties()来获取),而另一种则是系统的环境变量(通过System.getenv()来获取。)

这些默认的属性源都是StandardEnvironment的代表,在任何应用之中都可以使用。StandardServletEnvironment则是包含Servlet配置的环境信息,其中会包含很多Servlet的配置和Servlet上下文参数。StandardPortletEnvironment类似于StandardServletEnvironment,能够配置portlet上下文参数。可以参考其Javadoc了解更多信息。

具体的说,当使用StandardEnvironment的时候,调用env.containsProperty("foo")将返回一个foo的系统属性,或者是foo的运行时环境变量。

查询配置属性是按层次来查询的。默认情况下,系统属性优于系统环境变量,所以如果foo属性在两个环境中都有配置的话,那么在调用env.getProperty("foo")期间,系统属性值会优先返回。需要注意的是,属性的值是不会合并的,而是完全覆盖掉。 在一个普通的StandardServletEnvironment之中,查找的顺序如下,优先查找* ServletConfig参数(比如DispatcherServlet上下文),然后是* ServletContext参数(web.xml中的上下文参数),再然后是* JNDI环境变量,JVM系统变量("-D"命令行参数)以及JVM环境变量(操作系统环境变量)。

最重要的是,整个的查找机制是可以配置的。也许开发者自己有些定义的配置源信息想集成到配置检索的系统中去。没问题,只要实现开发者自己的PropertySource并且将其加入到当前EnvironmentPropertySources之中即可:

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

在上面的代码之中,MyPropertySource被添加到检索配置的第一优先级之中。如果存在一个foo属性,它将由于其他的PropertySource之中的foo属性优先返回。MutablePropertySourcesAPI提供一些方法来允许精确控制配置源。

@PropertySource注解

@PropertySource注解提供了一种方便的机制来将PropertySource增加到Spring的Environment之中。 给定一个文件app.properties包含了key-value对testbean.name=myTestBean,下面的代码中,使用了@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之中形如${...}的占位符,都可以被解析成Environment中的属性资源,比如:

@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是我们已经注册到Environment之中的资源,举例来说,JVM系统属性或者是环境变量的话,占位符会解析成对象的值。如果没有的话,default/path会来作为默认值。如果没有指定默认值,而且占位符也解析不出来的话,就会抛出IllegalArgumentException

占位符解析

从历史上来说,占位符的值是只能针对JVM系统属性或者环境变量来解析的。但是现在不是了,因为环境抽象已经继承到了容器之中,现在很容易通过容器将占位符解析集成。这意味着开发者可以任意的配置占位符:

  • 开发者可以自由调整系统变量还有环境变量的优先级
  • 开发者可以额外增加自己的属性源信息

具体的说,下面的XML配置不会在意customer属性在哪里定义,只有这个值在Environment之中有效即可:

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

书籍推荐