IoC容器13——環境抽象

Environment是集成在容器中的抽象,它爲應用程序的兩個關鍵方面:profiles和properties提供模型。java

只有在給定的profiles處於活動狀態時,profiles纔是要向容器註冊的一個命名邏輯組的bean定義。bean能夠被分配到XML或註解形式的profiles中。與profiles相關的Environment對象的角色是肯定哪些profiles(若是有的話)當前處於活動狀態,哪些profiles若是有的話——默認狀況下應處於活動狀態。web

在幾乎全部的應用程序中,properties都扮演着重要的角色,並可能集合自各類來源:屬性文件、JVM系統屬性、系統環境變量、JNDI、servlet上下文參數、臨時的Properties對象、Maps等等。與propreties相關的Environment的角色時爲用戶提供方便的服務接口,用於配置屬性源並從中解析屬性。算法

1 Bean定義profiles

Bean定義profiles時核心容器中的一個機制,它容許在不一樣環境中的註冊不一樣bean。環境這個詞對於不一樣的用戶意味着不一樣的東西而且這個特性能夠在許多用例中起做用,包括:spring

  • 處理開發中在內存中的數據源 vs 在QA或生產環境中從JNDI查找相同的數據源;
  • 僅在將應用程序部署到性能環境時註冊進空基礎架構;
  • 註冊用戶A vs 用戶B部署的bean的自定義實現。

考慮第一種用例,在一個實際應用中須要一個DataSource。在測試環境中,配置像下面這樣:sql

@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定義profiles是提供這個問題的一個解決方法的核心容器特性。服務器

若是將環境特定的bean定義歸納爲上面的例子,咱們最終須要在某些上下文中註冊某些bean,而不在其它狀況下。能夠說要在情景A中註冊一個特定的bean定義profile,而且在情景B中註冊一個不一樣的profile。首先看看如何更新配置以知足這種需求。架構

@Profile

@Profile註解指示一個組件在一個或多個特定的profile處於活動狀態時有註冊的資格。使用上面的例子,能夠從西安數據源的配置以下:app

@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的InitailContext,如上例,可是不能使用JndiObjectFactoryBean的變體,由於它強制要求將返回類型聲明爲FactroyBean類型。函數

@Profile能夠被用於建立自定義組合註解的元註解。下面的例子聲明一個自定義的@Production註解,用於以插入式的方式替代@Profile("production"):

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

若是一個@Configuration類被標記爲@Profile,全部的@Bean方法和@Import註解關聯的類將會被忽略,除非一個或多個指定的profiles處於活動狀態。若是一個@Component或@Configuration類被@Profile({"p1","p2"})標記,這個類不會被註冊/處理除非profile p1 和/或 p2 被激活。若是一個給定的profile有非操做符!爲前綴,那註解的元素在profile沒有激活時被註冊。例如,給定的@Profile({"p1", "!p2"})的註冊將會發生在profile pi 被激活或者profile p2 不被激活。

@Profile也能夠在方法級別聲明,僅包含配置類的一個特定bean,例如對於特定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");
    }
}

在@Bean方法上使用@Profile,能夠應用於一個特定的場景:在使用相同Java方法名稱的重載@Bean方法(相似於構造函數重載)的狀況下,一個@Profile條件須要在全部重載方法上一致的聲明。若是條件不一致,只有重載方法中第一個聲明的條件起做用。@Profile所以不能用於選擇具備特定參數簽名的重載方法;同一個bean的全部工廠方法之間的解析在建立時遵循Spring的構造函數解析算法。

若是想使用不一樣的profile條件定義bean的選擇,使用@Bean name屬性指向同一個bean名稱,而Java方法名稱不一樣,如上面的例子所展現的。若是參數簽名都同樣(例如全部的工廠方法都沒有參數),這是在有效的Java類中首先表示這種安排的惟一方法(由於只能有一個方法擁有指定的方法名和參數簽名)。

XML bean定義profile

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文件中產生混亂。

激活一個profile

既然已經更新了配置,仍然須要指示Spring哪一個profile要被激活。若是如今開始示例應用,會拋出NoSuchBeanDefinitionException,由於容器不能找到名爲dataSource的bean。

激活一個profile能夠有幾種方式,可是最直接的方式是以編程的方式使用Environment API,Environment能夠經過ApplicationContext獲取:

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

此外,profile能夠以聲明的方式經過spring.profiles.active屬性被激活,這個屬性能夠由系統環境變量、JVN系統屬性、web.xml中的servlet上下文參數甚至是JNDI中的一個鍵值對定義。在集成測試中,激活profile能夠經過spring-test模塊中的@ActiveProfiles註解實現。

注意profile不是一個「任意-或」的命題,能夠一次激活多個profile。編程的方式,提供多個profile名字給setActiveProfiles()函數就能夠了,這個函數接受可變數量的String類型:

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屬性改變。

2 PropertySource 抽象

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

在上面的代碼片斷中,能夠看到一個高級的方法來詢問Spring foo屬性是否在當前環境中定義。爲了回答這個問題,Environment對象對一組PropertySource對象進行搜索。PropertySource是對任何來源的鍵值對的簡單抽象,同時Spring的StandardEnvironment被兩個PropertySource對象配置,一個表明JVM系統屬性的集合(System.getProperties()),另外一個表明系統環境變量的集合(System.getenv())。

這些默認的屬性源存在於StandardEnvironment,用於獨立應用程序。StandardServletEnvironment還包含其它默認的屬性源,包括servlet配置和servlet上下文參數。StandardPortletEnvironment一樣包含portlet配置和portlet上下文參數做爲配置源。這二者均可以選擇啓用JndiPropertySource。細節可參考javadoc。

具體的,當使用StandardEnvironment,調用env.containsProperty("foo")會返回true若是一個foo系統屬性或foo環境變量在運行時存在。

搜索的行爲是有層次的。默認的,系統屬性優先於環境變量,因此若是在調用env.getProperty("foo")時剛好在兩個地方設置了foo屬性,系統屬性值將會「勝出」而且優先於環境變量被返回。請注意屬性值只會被優先的覆蓋而不會被合併。

對於普通的StandardServletEnvironment,完整的層次結構以下,最上面的有最高的優先權:

  • ServletConfig 參數(若是適用,例如在DispatcherServlet上下文的狀況下);
  • ServletContext 參數(web.xml的context-param鍵值對);
  • JNDI環境變量("java:comp/env/"鍵值對);
  • JVM 系統屬性("-D" 命令行參數);
  • JVM 系統環境(操做系統環境變量)。

最重要的是,整個機制是可配置的。也許有一個自定義的配置源須要集成到搜索機制中。沒問題——只要實現並實例化自定義的PropertySource而且將其添加到當前Environment的PropertySources集合中:

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

在上面的代碼中,MyPropertySource被添加爲最高優先級。若是它包含一個foo屬性,它會被發現並先於其它PropertySource中foo屬性被返回。MutablePropertySources API暴露了一系列方法,容許精確操做屬性源的集合。

3 @PropertySource

@PropertySource註解爲添加一個PropertySource到Spring的Environment提供了方便的聲明機制。

給定一個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"存在於已經註冊的配置源集合中的一個,例如系統配置或環境變量,佔位符將會被解析爲相應的值。若是沒有,那麼「默認/路徑」將會被使用。若是沒有聲明默認值而且屬性不能被解析,將拋出IllegalArgumentException。

4 聲明中的佔位符解析

從前XML元素中的佔位符的值只能被JVM系統屬性或環境變量解析。如今已不是這種狀況。由於環境抽象被集成到容器中,經過它能夠輕鬆的路由佔位符解析。這意味着能夠任意的配置解析處理:改變系統屬性和環境變量的搜索優先級,或者直接移除它們;添加自定義的屬性源。

具體的,下面的聲明不關注customer屬性在哪裏定義,只要在Environment中便可:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
相關文章
相關標籤/搜索