1. 程式人生 > 實用技巧 >一起來讀官方文件-----SpringIOC(11)

一起來讀官方文件-----SpringIOC(11)

1.13。Environment

Environment介面是整合在容器中的抽象存在,它表現為應用程式環境的兩個關鍵方面:profiles和properties。

1.13.1。Bean Definition Profiles

Bean Definition Profiles在核心容器中提供了一種機制,該機制允許在不同environment中註冊不同的Bean。

說白了其實就是判斷 spring.profiles.active 的值
這個值可以有多箇中間用 , 隔開就可以

“environment”一詞對不同的使用者而言可能意味著不同的含義,並且此功能可以在許多用例中提供幫助,包括:

  • 在開發中針對記憶體中的資料來源進行工作,而不是在進行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();
}

現在,假設該應用程式的資料來源已在生產應用程式伺服器的JNDI目錄中註冊,請考慮如何將該應用程式部署到QA或生產環境中。
現在,我們的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語句的組合,這些${placeholder}根據環境變數的值解析為正確的配置檔案路徑。

Bean Definition Profiles是一項核心容器功能,可提供此問題的解決方案。

使用 @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的JNDIMplate/JNDilocatorDeleteGate幫助器,
或者使用前面顯示的直接JNDIInitialContext用法,
而不是JndiObjectFactoryBean變數,因為factoryBean方法返回的是FactoryBean型別,而不是DataSource型別。
原理解釋:
1.@Profile註解中指定了@Conditional註解中的ProfileCondition.class

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

	/**
	 * The set of profiles for which the annotated component should be registered.
	 */
	String[] value();

}

2.首先在載入bean的時候發現有方法判斷是否應該調過當前bean
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
			configClass.skippedBeanMethods.add(methodName);
			return;
}

在shouldSkip中會查詢當前bean的所有的condition
並迴圈執行每個condition的matches
而@Profile的condition的matches如下所示

class ProfileCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				//此處 呼叫了environment的propertySources
				//判斷當前配置中的所有的propertySources是否含有spring.profiles.active屬性
				//有值的話就將它設定到environment的activeProfiles屬性中
				//再判斷當前類的@Profile註解中的值是否被包含在activeProfiles屬性內
				//如果被包含則返回true
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}

}

配置檔案字串可以包含簡單的配置檔名稱(例如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");
    }
}
  • 該standaloneDataSource方法僅在development配置檔案中可用。
  • 該jndiDataSource方法僅在production配置檔案中可用。

對於@Bean方法上的@Profile,可能會應用一個特殊的場景(在同一個配置類中):
-----對於具有相同Java方法名的過載@Bean方法(類似於建構函式過載),需要在所有過載方法上宣告相同的@Profile條件,宣告相同的條件並不是因為可以自動選擇過載方法,是因為這一批過載方法都會因為第一個方法的校驗不合格就全部不通過,如果第一個合格才會往下繼續判斷是否可以用其他的過載方法進行bean的註冊。
-----如果條件不一致,則只有過載方法中第一個宣告的條件才生效。
因此,@Profile不能用於選擇具有特定引數簽名的過載方法。
同一bean的所有工廠方法之間的解析在建立時遵循Spring的建構函式解析演算法。

如果您想定義具有不同配置檔案條件的替代bean,請使用指向相同bean名稱的不同@Bean方法名,方法是使用@Bean的name屬性,如前面的示例所示。

如果引數簽名都相同(例如,所有變數都沒有arg工廠方法),那麼這是在一個有效的Java類中首先表示這種安排的唯一方法(因為只能有一個特定名稱和引數簽名的方法)。

分析:
// 判斷當前@Bean是否需要跳過 
// 這裡的判斷順序就是 Config類中的@Bean程式碼的先後順序跟@Order無關
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
	configClass.skippedBeanMethods.add(methodName);
	return;
}
//當同名方法出現並在之前被跳過之後 這裡會判斷skippedBeanMethods屬性是否包含並直接跳過
//所以不管同一個配置類中後續的同名方法是否帶有註解都將不再處理
if (configClass.skippedBeanMethods.contains(methodName)) {
	return;
}

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對應項不支援前面描述的配置檔案表示式
-----例如:(production & (us-east | eu-central))。
但是,可以通過使用!運算子來取消配置檔案。
也可以通過巢狀配置檔案來應用邏輯“與”,如以下示例所示:

<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="...">

    <!---如果production和 us-east配置檔案都處於活動狀態,則dataSource會被註冊-->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
Default 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();
    }
}

如果沒有配置檔案被啟用,dataSource被建立。
您可以看到這是為一個或多個bean提供預設定義的一種方法。

如果啟用了任何配置檔案,則預設配置檔案不適用。

您可以通過setDefaultProfiles() 或者使用宣告性地使用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是對任何鍵-值對源的簡單抽象,Spring的StandardEnvironment配置有兩個PropertySource物件

  • 一個代表JVM系統屬性集(System.getProperties())
  • 一個代表系統環境變數集(System.getenv())。
這些預設屬性源是為StandardEnvironment提供的,供獨立應用程式使用。
StandardServletEnvironment使用了附加的預設屬性源,包括servlet配置和servlet上下文引數。
它可以選擇啟用JndiPropertySource。
所執行的搜尋是分層的。
預設情況下,系統屬性優先於環境變數。
因此,如果在呼叫env.getProperty(“my-property”)期間,恰好在兩個位置都設定了my-property屬性,則系統屬性值“勝出”並被返回。
注意,屬性值沒有被合併,而是被前面的條目完全覆蓋。

對於common StandardServletEnvironment,完整的層次結構如下所示,最高優先順序的條目位於頂部:

    ServletConfig引數(如果適用——例如,在DispatcherServlet上下文的情況下)
    ServletContext引數(web.xml上下文引數項)
    JNDI環境變數(java:comp/env/ entries)
    JVM系統屬性(-D命令列引數)
    JVM系統環境(作業系統環境變數)

最重要的是,整個機制是可配置的。
也許您具有要整合到此搜尋中的自定義屬性源。
為此,請實現並例項化自己的例項PropertySource並將其新增到PropertySourcescurrent的集合中Environment。
以下示例顯示瞭如何執行此操作:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
//MyPropertySource被添加了最高優先順序。  
sources.addFirst(new MyPropertySource());

該MutablePropertySources API公開了許多方法,
這些方法允許對屬性源集進行精確操作。

1.13.3。使用@PropertySource

@PropertySource註解提供了一種方便的宣告機制,可以將PropertySource新增到Spring的環境中。

給定一個名為app.properties的檔案,
其中包含鍵值對testbean.name=myTestBean,
下面的@Configuration類使用@PropertySource,
呼叫env.getProperty("testbean.name")會返回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資源位置的${…}佔位符都會根據已經在環境中註冊的屬性源進行解析,如下面的示例所示:

//假定my.placeholder存在於已註冊的屬性源之一(例如,系統屬性或環境變數)中,則佔位符將解析為相應的值。  
//如果不是,則default/path用作預設值。  
//如果未指定預設值並且無法解析屬性, IllegalArgumentException則丟擲。

@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;
    }
}
根據Java 8的約定,@PropertySource註解是可重複的。
但是,所有這樣的@PropertySource註解都需要在同一級別宣告,要麼直接在配置類上宣告,要麼作為同一自定義註解中的元註解宣告。
不推薦混合使用直接註解和元註解,因為直接註解有效地覆蓋了元註解。

1.13.4。宣告中的佔位符解析
過去,元素中的佔位符的值只能根據JVM系統屬性或環境變數解析。
現在情況已經不一樣了。
因為環境抽象整合在整個容器中,所以很容易通過它來解析佔位符。
這意味著您可以以任何您喜歡的方式配置解析過程。
您可以更改搜尋系統屬性和環境變數的優先順序,或者完全刪除它們。
您還可以在適當的情況下新增您自己的屬性源。

具體地說,無論客戶屬性定義在哪裡,只要它在環境中可用,以下語句都適用:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
1.15。ApplicationContext的其他功能

正如在引言中所討論的,
org.springframework.beans.factory包提供了管理和操作bean的基本功能,包括以程式設計的方式。
org.springframework.context 包添加了ApplicationContext介面,
該介面擴充套件了BeanFactory介面,
此外還擴充套件了其他介面,以更面向應用程式框架的風格提供額外的功能。

許多人以一種完全宣告式的方式使用ApplicationContext,甚至不是通過程式設計來建立它,而是依賴於支援類(如ContextLoader)來自動例項化一個ApplicationContext,作為Java EE web應用程式的正常啟動過程的一部分。

為了以更面向框架的風格增強BeanFactory的功能,上下文包還提供了以下功能:

  • 通過MessageSource介面訪問i18n風格的訊息。
  • 通過ResourceLoader介面訪問資源,例如url和檔案。
  • 事件釋出,即通過使用ApplicationEventPublisher介面釋出到實現ApplicationListener介面的bean。
  • 通過HierarchicalBeanFactory介面載入多個(分層的)上下文,讓每個上下文都關注於一個特定的層,比如應用程式的web層。
1.15.1。國際化使用MessageSource

ApplicationContext介面擴充套件了一個名為MessageSource的介面,因此提供了國際化(“i18n”)功能。
Spring還提供了HierarchicalMessageSource介面,該介面可以分層解析訊息。

這些介面一起提供了Spring實現訊息解析的基礎。

在這些介面上定義的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):
    用於從MessageSource檢索訊息的基本方法。
    如果未找到指定語言環境的訊息,則使用預設訊息。
    通過使用標準庫提供的MessageFormat功能,傳入的任何引數都將成為替換值。
  • String getMessage(String code, Object[] args, Locale loc):
    本質上與前面的方法相同,但有一個區別:不能指定預設訊息。
    如果找不到訊息,則丟擲NoSuchMessageException。
  • String getMessage(MessageSourceResolvable, Locale Locale):前面方法中使用的所有屬性也包裝在一個名為MessageSourceResolvable類中,可與此方法一起使用。

載入ApplicationContext時,它會自動搜尋上下文中定義的MessageSource bean。
bean的名稱必須是messageSource。

  • 如果找到這樣一個bean,對前面方法的所有呼叫都將委託給訊息源。
  • 如果沒有找到訊息源,ApplicationContext將嘗試查詢包含同名bean的父訊息源。如果是,則使用該bean作為訊息源。
  • 如果ApplicationContext找不到任何訊息源,則例項化一個空的DelegatingMessageSource,以便能夠接受對上面定義的方法的呼叫。

Spring提供了兩個訊息源實現:
ResourceBundleMessageSource和StaticMessageSource。

兩者都實現了HierarchicalMessageSource以執行巢狀訊息傳遞。
很少使用StaticMessageSource,但它提供了將訊息新增到源的程式設計方法。

下面的示例展示ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

這個例子假設你有所謂的三個資源包format.properties,exceptions.properties,windows.properties 在類路徑中定義。
解析訊息的任何請求均通過JDK標準的通過ResourceBundle物件解析訊息的方式來處理。
就本示例而言,假定上述兩個資源束檔案的內容如下:

#在format.properties中
message=Alligators rock!



#在exceptions.properties中
argument.required=The {0} argument is required.

下一個示例顯示了執行該MessageSource功能的程式。
請記住,所有ApplicationContext實現也是MessageSource實現,因此可以強制轉換為MessageSource介面。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

以上程式的結果輸出如下:

Alligators rock!

總之,MessageSource是在一個名為beans.xml的檔案中定義的,它存在於classpath中。
MessageSource bean定義通過其basenames屬性引用大量資源包。
在列表中傳遞給basenames屬性的三個檔案作為類路徑的根檔案存在,它們被稱為format.properties,exceptions.properties,and windows.properties。

下一個示例顯示了傳遞給訊息查詢的引數。

這些引數被轉換為字串物件,並插入到查詢訊息中的佔位符中。

<beans>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}

execute()方法呼叫的結果輸出如下:

The userDao argument is required.

關於國際化(“i18n”),Spring的各種MessageSource實現遵循與標準JDK ResourceBundle相同的語言環境解析和回退規則。
簡而言之,繼續前面定義的示例messageSource,如果您希望根據英國(en-GB)地區解析訊息,您將建立名為format_en_GB.properties, exceptions_en_GB.properties, and windows_en_GB.properties。

通常,語言環境解析由應用程式的周圍環境管理。
在下面的示例中,手動指定解析(英國)訊息所對應的語言環境:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.


public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

執行上述程式的結果輸出如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

您還可以使用MessageSourceAware介面來獲取對已定義的任何訊息源的引用。
當建立和配置bean時,在ApplicationContext中定義的任何bean實現MessageSourceAware介面的都被注入應用上下文的MessageSourceAware介面。

作為ResourceBundleMessageSource的替代方案,Spring提供了一個ReloadableResourceBundleMessageSource類。
這個變體支援相同的bundle檔案格式,但是比基於JDK的標準ResourceBundleMessageSource實現更加靈活。
特別是,它允許從任何Spring資源位置讀取檔案(不僅僅是從類路徑),並支援bundle屬性檔案的熱重新載入(同時有效地快取它們)。
有關詳細資訊,請參見ReloadableResourceBundleMessageSource javadoc。
1.15.2。標準和自定義事件

ApplicationContext中的事件處理是通過ApplicationEvent類和ApplicationListener介面提供的。
如果實現ApplicationListener介面的bean被部署到上下文中,那麼每當一個ApplicationEvent被髮布到ApplicationContext時,該bean就會得到通知。
本質上,這就是標準的觀察者設計模式。

從Spring 4.2開始,事件基礎設施已經得到了顯著改進,並提供了一個基於註解的模型,
以及釋出任意事件的能力(也就是說,不需要從ApplicationEvent擴充套件的物件)。
當釋出這樣的物件時,我們為您將其包裝在事件中。

表7.內建事件

事件 說明
ContextRefreshedEvent 在初始化或重新整理ApplicationContext時釋出
例如,ConfigurableApplicationContext.refresh()方法

這裡,初始化意味著載入了所有bean
檢測並激活了後處理器bean
預先例項化了singleton
並且ApplicationContext物件可以使用了。

只要上下文尚未關閉
並且所選的ApplicationContext實際上支援這種“熱”重新整理
就可以多次觸發重新整理
例如,XmlWebApplicationContext支援熱重新整理,
但GenericApplicationContext不支援。
ContextStartedEvent 在ConfigurableApplicationContext.start()方法
啟動ApplicationContext時釋出。

這裡,啟動意味著所有生命週期bean都收到一個顯式的啟動訊號。
通常,這個訊號用於在顯式停止後重新啟動bean
,但是它也可以用於啟動尚未配置為自動啟動的元件
例如,尚未在初始化時啟動的元件。
ContextStoppedEvent 在ConfigurableApplicationContext.stop()方法
停止ApplicationContext時釋出。

“停止”意味著所有生命週期bean都收到一個顯式的停止訊號。
停止的上下文可以通過start()呼叫重新啟動。
ContextClosedEvent 通過使用ConfigurableApplicationContext.close()方法
或通過JVM shutdown hook關閉ApplicationContext時釋出。

,“關閉”意味著所有的單例bean將被銷燬。
一旦上下文關閉,
它就會到達生命的終點,無法重新整理或重新啟動。
RequestHandledEvent 一個特定於web的事件,
告訴所有bean一個HTTP請求已經得到服務。
此事件在請求完成後釋出。
此事件僅適用於使用Spring的DispatcherServlet的web應用程式。
ServletRequestHandledEvent 該類的子類RequestHandledEvent
添加了Servlet-specific的上下文資訊。

您還可以建立和釋出自己的自定義事件。
下面的示例顯示了一個簡單的類,該類擴充套件了Spring的ApplicationEvent基類:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

要釋出自定義ApplicationEvent,請在ApplicationEventPublisher上呼叫publishEvent()方法 。

通常,這是通過建立一個實現ApplicationEventPublisherAware並註冊為Spring bean的類來完成的 。

以下示例顯示了此類:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置時,Spring容器檢測到該ApplicationEventPublisherAware實現EmailService 並自動呼叫 setApplicationEventPublisher()。

實際上,傳入的引數是Spring容器本身。
您正在通過其ApplicationEventPublisher介面與應用程式上下文進行互動。

要接收自定義ApplicationEvent,您可以建立一個實現 ApplicationListener並註冊為Spring bean的類。
以下示例顯示了此類:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener通常是用自定義事件的型別引數化的(在前面的示例中是BlockedListEvent)。
這意味著onApplicationEvent()方法可以保持型別安全,避免向下強制轉換。
您可以註冊任意數量的事件監聽器,但是請注意,預設情況下,事件監聽器同步接收事件
這意味著publishEvent()方法會阻塞,直到所有監聽器都完成了事件的處理。
這種同步和單執行緒方法的一個優點是,當偵聽器接收到事件時,如果事務上下文可用,它將在釋出程式的事務上下文內操作。

下面的例子顯示了用於註冊和配置上面每個類的bean定義:

<!--當呼叫emailService bean的sendEmail()方法時,  
    如果有任何需要阻止的電子郵件訊息,
    則釋出型別為BlockedListEvent的自定義事件。 -->
<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

<!--blockedListNotifier         
    bean註冊為一個ApplicationListener並接收BlockedListEvent, 
    此時它可以通知適當的方。-->
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="[email protected]"/>
</bean>
Spring的事件機制是為相同上下文中的Spring bean之間的簡單通訊而設計的。

然而,對於更復雜的企業整合需求,
單獨維護的Spring integration專案提供了構建輕量級、面向模式、事件驅動架構的完整支援,
這些架構構建在眾所周知的Spring程式設計模型之上。
基於註解的事件偵聽器

從Spring 4.2開始,您可以使用@EventListener註解在託管Bean的任何公共方法上註冊事件偵聽器。

該BlockedListNotifier可改寫如下:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法簽名再次宣告它偵聽的事件型別,但是這次使用了靈活的名稱,並且沒有實現特定的偵聽器介面。
只要實際事件型別在其實現層次結構中解析泛型引數,就可以通過泛型縮小事件型別。

如果您的方法應該偵聽多個事件,或者您希望在不使用任何引數的情況下定義它,那麼還可以在註解本身上指定事件型別。

下面的例子展示瞭如何做到這一點:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

還可以通過使用定義SpEL表示式的註解的條件屬性來新增額外的執行時過濾,該註解應該與針對特定事件實際呼叫方法相匹配。

下面的例子展示了我們的通知程式如何被重寫,只有在content 屬性等於my-event時才被呼叫:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

每個SpEL表示式針對專用上下文進行評估。
下表列出了可用於上下文的專案,以便您可以將它們用於條件事件處理:

表8. Event SpEL可用的元資料

名稱 例子
Event #root.event or event
引數陣列 #root.args or args;
args[0]訪問第一個引數等。
引數名稱 #blEvent或#a0
(您還可以使用#p0或#p<#arg>引數表示法作為別名)
請注意,root.event允許您訪問基礎事件,即使您的方法簽名實際上引用了已釋出的任意物件。

如果你需要釋出一個事件作為處理另一個事件的結果,你可以改變方法簽名來返回應該釋出的事件,如下面的例子所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
asynchronous listeners.不支援此功能 。

此新方法為每處理一個BlockedListEvent事件都會發佈一個新事件ListUpdateEvent。
如果您需要釋出多個事件,則可以返回一個Collection事件。

asynchronous listeners 非同步偵聽器

如果需要一個特定的偵聽器非同步處理事件,可以重用常規的@Async支援。
下面的例子展示瞭如何做到這一點:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

使用非同步事件時,請注意以下限制:

  • 如果非同步事件偵聽器丟擲Exception,則不會傳播到呼叫者。
  • 非同步事件偵聽器方法無法通過返回值來發布後續事件。如果您需要釋出另一個事件作為處理的結果,請插入一個 ApplicationEventPublisher 以手動釋出事件。
Ordering Listeners

如果需要先呼叫一個偵聽器,則可以將@Order註解新增到方法宣告中,
如以下示例所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}
一般事件

還可以使用泛型來進一步定義事件的結構。
考慮使用EntityCreatedEvent,其中T是所建立的實際實體的型別。
例如,您可以建立以下偵聽器定義來只為一個人接收EntityCreatedEvent:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由於型別擦除,只有在觸發的事件解析了事件偵聽器所基於的通用引數
(即class PersonCreatedEvent extends EntityCreatedEvent { …​ })時,此方法才起作用 。

在某些情況下,如果所有事件都遵循相同的結構(前面示例中的事件也應該如此),那麼這可能會變得非常乏味。
在這種情況下,您可以實現ResolvableTypeProvider來指導執行時環境所提供的框架。
下面的事件展示瞭如何做到這一點:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}
這不僅適用於ApplicationEvent作為事件傳送的任何物件,而且適用於該物件。
1.15.3。方便地訪問低階資源

為了優化使用和理解應用程式上下文,您應該熟悉Spring的Resource類,如參考資料中所述。

應用程式上下文是一個ResourceLoader,可用於載入資源物件。

Resource本質上是JDK java.net.URL類的功能豐富版本。
事實上,Resource 的實現在適當的時候包裝了一個java.net.URL的例項。

Resource可以以透明的方式從幾乎任何位置獲取底層資源,包括類路徑、檔案系統位置、可用標準URL描述的任何位置,以及其他一些變體。

如果Resource位置字串是沒有任何特殊字首的簡單路徑,那麼這些資源的來源是特定的,並且適合於實際的應用程式上下文型別。

您可以配置部署到應用程式上下文中的bean,以實現特殊的回撥介面ResourceLoaderAware,在初始化時自動回撥,而應用程式上下文字身作為ResourceLoader傳入。
您還可以公開Resource型別的屬性,以便用於訪問靜態資源。Resource 像其他屬性一樣可以被注入。

您可以將這些資源屬性指定為簡單的字串路徑,並在部署bean時依賴於從這些文字字串到實際資源物件的自動轉換。

提供給ApplicationContext建構函式的位置路徑或路徑實際上是資源字串,並且以簡單的形式,根據特定的上下文實現進行適當的處理。
例如,ClassPathXmlApplicationContext將簡單的位置路徑視為類路徑位置。
您還可以使用帶有特殊字首的位置路徑(資源字串)來強制從類路徑或URL載入定義,而不管實際上下文型別是什麼。

1.15.4。應用程式啟動跟蹤

ApplicationContext管理Spring應用程式的生命週期,並圍繞元件提供豐富的程式設計模型。
因此,複雜的應用程式可能具有同樣複雜的元件圖和啟動階段。

使用特定的度量來跟蹤應用程式的啟動步驟可以幫助理解啟動階段的時間花費在哪裡,它也可以作為一種更好地理解整個上下文生命週期的方法。

AbstractApplicationContext(及其子類)由ApplicationStartup檢測,它收集關於不同啟動階段的StartupStep資料:

  • 應用程式上下文生命週期(基本包掃描,配置類管理)
  • bean生命週期(例項化、智慧初始化、後處理)
  • 應用程式事件處理

下面是AnnotationConfigApplicationContext中的插裝示例:

// 建立並啟動記錄
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 向當前步驟新增標記資訊
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 執行我們正在測量的實際階段
this.scanner.scan(basePackages);
// 結束
scanPackages.end();
1.15.5。Web應用程式的便捷ApplicationContext例項化

例如,可以使用ContextLoader以宣告方式建立ApplicationContext例項。當然,也可以通過使用ApplicationContext實現之一以程式設計方式建立ApplicationContext例項。

您可以使用ContextLoaderListener來註冊一個ApplicationContext,如以下示例所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

監聽器檢查contextConfigLocation引數。
如果引數不存在,偵聽器將使用/WEB-INF/applicationContext.xml作為預設值。

當引數確實存在時,偵聽器使用預定義的分隔符(逗號、分號和空白)分隔字串,並將這些值用作搜尋應用程式上下文的位置。

也支援Ant風格的路徑模式。
例如:
/WEB-INF/*Context.xml(對於在WEB-INF目錄中名稱以Context結尾的所有檔案)

/WEB-INF/**/*Context.xml(對於WEB-INF任何子目錄中的所有此類檔案)

1.16.1。BeanFactory or ApplicationContext?

本節解釋BeanFactory和ApplicationContext容器級別之間的差異,以及引導的含義。

您應該使用ApplicationContext,除非您有很好的理由不這樣做,使用GenericApplicationContext和它的子類AnnotationConfigApplicationContext作為自定義引導的通用實現。
這些是用於所有常見目的的Spring核心容器的主要入口點:載入配置檔案、觸發類路徑掃描、以程式設計方式註冊bean定義和帶註解的類,以及(從5.0開始)註冊功能性bean定義。

因為ApplicationContext包含了BeanFactory的所有功能,所以一般建議它優於普通的BeanFactory,除非需要對bean處理進行完全控制的場景除外。

對於許多擴充套件的容器特性,如註解處理和AOP代理,BeanPostProcessor擴充套件點是必不可少的。如果只使用普通的DefaultListableBeanFactory,預設情況下不會檢測到這種後處理器並激活它。

下表列出了BeanFactory和ApplicationContext介面和實現提供的特性。
表9.功能矩陣

特徵 BeanFactory ApplicationContext
Bean例項化/佈線 Yes Yes
整合的生命週期管理 No Yes
自動BeanPostProcessor登記 No Yes
方便的訊息源訪問(用於內部化) No Yes
內建ApplicationEvent釋出機制 No Yes

要使用顯式註冊Bean後處理器DefaultListableBeanFactory,您需要以程式設計方式呼叫addBeanPostProcessor,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();

factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要將一個BeanFactoryPostProcessor應用到一個普通的DefaultListableBeanFactory中,你需要呼叫它的postProcessBeanFactory方法,如下面的例子所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

如上所見手動登記是十分不方便的,尤其是依靠BeanFactoryPostProcessor和BeanPostProcessor擴充套件功能的時候。

一個AnnotationConfigApplicationContext註冊了所有公共註解後處理器,並可能通過配置註解(如@EnableTransactionManagement)在後臺引入額外的處理器。

在Spring的基於註解的配置模型的抽象層上,bean後處理器的概念僅僅成為容器內部的細節。

本文由部落格一文多發平臺 OpenWrite 釋出!