天天看點

Spring 5 中文解析核心篇-IoC容器之Environment抽象

1.13 Environment抽象

Environment

接口是在容器中抽象內建,應用環境包括兩個重要的方面:

profiles

properties

。配置檔案是一個命名的bean定義邏輯組,隻有在給定的配置檔案處于激活狀态時才在容器中注冊。可以将bean配置設定給配置檔案,不管它是用XML定義的還是用注解定義的。與

profile

檔案相關的環境對象的角色是确定哪些

profile

檔案(如果有的話)目前是激活的,以及哪些

profile

檔案(如果有的話)在預設情況下應該是激活的。

Properties

幾乎在所有應用中扮演一個重要的角色并且可能來自于多個源:屬性檔案、JVM系統屬性、系統環境變量、JNDI、servlet上下文參數、Properties對象、Map對象等等。

Environment

對象的角色與

properties

關聯去提供給使用者一個便利的服務接口去配置屬性源和解析屬性。

1.13.1 Bean定義Profiles

bean定義屬性檔案在核心容器中提供一個機制,它允許在不同的環境中注冊不同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目錄。我們的

dataSource

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定義配置檔案是一項核心容器功能,可解決此問題。

如果我們概括前面環境特定的bean定義示例中所示的用例,我們最終需要在特定上下文中注冊特定的bean定義,而不是在其他上下文中注冊。可以這樣說,你希望在情形A中注冊bean定義的某個配置檔案,而在情形B中注冊另一個配置檔案。我們開始更新配置以反映這種需求。

使用

@Profile

@Profile

注解允許你去指明哪些元件适合去注冊,當一個或多個指定

profile

處于激活狀态時。使用我們前面的例子,我們可以重寫

dataSource

配置如下:

@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

),下面的操作符在

profile

表達式中是被支援的:

  • !:邏輯非
  • &:邏輯與
  • |:邏輯或
你不能混合

&

操作符而不使用括号。例如,

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類(例如,用于特定bean的替代),類似下面例子展示:

@Configuration
public class AppConfig {

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

    方法僅僅在

    development

    配置中有效
  2. jndiDataSource

    production

對于

@Bean

方法上的

@Profile

,可以應用一個特殊的場景:在重載相同Java方法名的

@Bean

方法(類似于構造函數重載)的情況下,需要一緻地在所有重載方法上聲明

@Profile

條件。如果條件不一緻,則隻有重載方法中第一個聲明的條件重要。是以,

@Profile

不能被使用去選擇具有特定參數簽名重載方法。在建立時,同一bean的所有工廠方法之間的解析都遵循Spring的構造函數解析方法。

如果你想去定義不同配置條件的bean,使用不同的Java方法名稱,通過使用

@Bean

name屬性指向相同名稱的bean,類似前面展示例子。如果參數前面都相同(例如,所有的變體有無參構造函數),這是在一個有效的Java類中表示這種安排的唯一方法(因為隻能有一個具有特定名稱和參數簽名的方法)。

XML bean定義配置檔案

XML對應項是元素的

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 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>           
在前面的例子中,如果

production

us-east

配置都被激活,則

dataSource

bean被暴露。

激活profile

現在我們已經更新了配置檔案,我們仍然需要訓示Spring哪一個配置檔案激活。如果我們已經啟動了我們的應用程式,我們将看到一個

NoSuchBeanDefinitionException

抛出,因為容器不能找到名稱為

dataSource

的bean。

可以通過多種方式來激活配置檔案,但最直接的方法是可通過

ApplicationContext

獲得的

Environment

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

抽象)。在內建測試中,激活配置檔案可以通過使用

@ActiveProfiles

聲明在

spring-test

子產品(檢視

上下文配置通過環境配置檔案

)。

請注意,配置檔案不是

非此即彼

的命題。你可以一次性激活多個配置檔案。程式設計式地,你可以提供多個配置檔案名稱給

setActiveProfiles()

方法,它可以接受

String…

可變參數。下面例子激活多個配置檔案:

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

聲明式地,

spring.profiles.active

可以接收配置名稱逗号分隔清單,類似下面例子展示:

-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不被使用。

你可以通過在

Environment

上使用

setDefaultProfiles()

改變預設

profile

名稱或者,聲明式地,通過使用

spring.profiles.default

屬性。

1.13.2

PropertySource

抽象

Spring的

Environment

抽象提供了可配置屬性源層次結構上的搜尋操作。考慮下面清單:

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

是一個簡單

key-value

對源的抽象,并且Spring的

StandardEnvironment

被配置兩個

PropertySource

對象,一個描述JVM系統(

System.getProperties()

)屬性集合另一個描述系統環境變量集合(

System.getenv()

這些預設的屬性源适用

StandardEnvironment

,為在獨立的應用中使用。

StandardServletEnvironment

是通過附加預設屬性源填充,包括servlet配置和servlet上下文參數。它可以選擇啟用

JndiPropertySource

。檢視javadock詳情。

具體地說,當你使用

StandardEnvironment

時,如果

my-property

系統屬性或

my-property

環境變量在運作時被描述,調用

env.containsProperty("my-property")

方法将傳回true。

執行的搜尋是分層的,預設情況,系統屬性優先級高于環境變量。是以,如果

my-property

屬性在兩個地方被設定,在調用

env.getProperty("my-property")

時,系統屬性值将被傳回。請注意,屬性值不會合并,而是會被前面的值完全覆寫。

對于通用的

StandardServletEnvironment

,完整的層次結構如下,優先級最高的條目位于頂部:

1.ServletConfig

參數(如果使用的-例如,如果是

DispatcherServlet

上下文)

2.ServletContext

參數(web.xml 上下文參數)

3.JNDI 環境變量(

java:comp/env/

)

4.JVM系統參數(-D 指令行參數)

5.JVM系統變量(作業系統環境變量)

最重要地,整個機制是可配置的。也許你有一個自定義的屬性源,你想整合到此搜尋中。這樣做,實作和執行個體化你的

PropertySource

并且添加到目前的

Environment

PropertySources

集合中。下面例子展示怎樣去做:

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

在前面的代碼中,

MyPropertySource

被添加到最高索引優先級中。如果它包含

my-property

屬性,則會檢測到并傳回該屬性,進而支援任何其他

PropertySource

中的

my-property

MutablePropertySources

API暴露了一些方法,這些方法允許去精确操作屬性源集合。

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

在一個已經被注冊的屬性源中被描述(例如,系統屬性或環境變量),則占位符被解析為對應值。如果沒有,則使用一個預設值

default/path

。如果沒有指定預設值并且屬性不能被解析,一個

IllegalArgumentException

被抛出。

根據Java8約定,

@PropertySource

注解是可以重複的。然而,所有的

@PropertySource

注解需要在相同等級被聲明,要麼直接地在配置類上,要麼作為中繼資料注解在相同自定義注解中。不建議将直接注釋和元注釋混合使用,因為直接注釋會有效地覆寫元注釋。

參考代碼:

com.liyong.ioccontainer.starter.EnvironmentIocContainer

1.13.4 語句中的占位符解析

在以前,元素中占位符的值隻能根據JVM系統屬性或環境變量來解析。現在情況已經不同了。因為

Environment

抽象已經內建到容器,很容易通過它來路由占位符的解析。這意味着你可以按照自己喜歡的任何方式配置解析過程。你可以更改搜尋系統屬性和環境變量的優先級,也可以完全删除它們。你還可以适當地将你自己的屬性源添加到組合中。

具體地說,不論在何處定義customer屬性,隻要在環境中可用,以下語句就可以工作:

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

作者

個人從事金融行業,就職過易極付、思建科技、某網約車平台等重慶一流技術團隊,目前就職于某銀行負責統一支付系統建設。自身對金融行業有強烈的愛好。同時也實踐大資料、資料存儲、自動化內建和部署、分布式微服務、響應式程式設計、人工智能等領域。同時也熱衷于技術分享創立公衆号和部落格站點對知識體系進行分享。

部落格位址:

http://youngitman.tech

CSDN:

https://blog.csdn.net/liyong1028826685

微信公衆号:

Spring 5 中文解析核心篇-IoC容器之Environment抽象

技術交流群:

Spring 5 中文解析核心篇-IoC容器之Environment抽象