Spring官網閱讀(十一)ApplicationContext詳細介紹(上)

在前面的文章中,我們已經完成了官網中關於IOC內容核心的部分。包括容器的概念,Spring創建Bean的模型BeanDefinition的介紹,容器的擴展點(BeanFactoryPostProcessorFactroyBeanBeanPostProcessor)以及最重要的Bean的生命週期等。接下來大概還要花三篇文章完成對官網中第一大節的其它內容的學習,之所以要這麼做,是筆者自己粗讀了一篇源碼後,再讀一遍官網,發現源碼中的很多細節以及難點都在官網中介紹了。所以這裏先跟大家一起把官網中的內容都過一遍,也是爲了更好的進入源碼學習階段。

本文主要涉及到官網中的1.13,1.15,1.16小節中的內容以及第二大節的內容

ApplicationContext

1、ApplicationContext的繼承關係

在這裏插入圖片描述

從上圖中可以發現,ApplicationContext接口繼承了很多接口,這些接口我們可以將其分爲五類:

  • MessageSource,主要用於國際化
  • ApplicationEventPublisher,提供了事件發佈功能
  • EnvironmentCapable,可以獲取容器當前運行的環境
  • ResourceLoader,主要用於加載資源文件
  • BeanFactory,負責配置、創建、管理Bean,IOC功能的實現主要就依賴於該接口子類實現。

關於這些的接口的具體功能的介紹在後文會介紹,當前我們需要知道最重要的一點就是ApplicationContext繼承了BeanFactory接口,也就是說它具有BeanFactory所有的功能。

2、ApplicationContext的功能

Spring中的國際化(MessageSource)

國際化是什麼?

應用程序運行時,可根據客戶端操作系統的國家/地區、語言的不同而顯示不同的界面,比如客戶端OS的語言環境爲大陸的簡體中文,程序就顯示爲簡體中文,客戶端OS的語言環境爲美國——英語,程序就顯示美式英語。OS的語言環境可在控制面板中手動設置。國際化的英文單詞是Internationalization,單詞較長,通常簡稱i18n,I是第一個字母,18表示中間省略了18個字母,N是最後一個字母。

假設我們正在開發一個支持多國語言的Web應用程序,要求系統能夠根據客戶端的系統的語言類型返回對應的界面:英文的操作系統返回英文界面,而中文的操作系統則返回中文界面——這便是典型的i18n國際化問題。對於有國際化要求的應用系統,我們不能簡單地採用硬編碼的方式編寫用戶界面信息、報錯信息等內容,而必須爲這些需要國際化的信息進行特殊處理。簡單來說,就是爲每種語言提供一套相應的資源文件,並以規範化命名的方式保存在特定的目錄中,由系統自動根據客戶端語言選擇適合的資源文件

JAVA中的國際化

國際化信息也稱爲本地化信息,一般需要兩個條件纔可以確定一個特定類型的本地化信息,它們分別是“語言類型”和“國家/地區的類型”。如中文本地化信息既有中國大陸地區的中文,又有中國臺灣、中國香港地區的中文,還有新加坡地區的中文。

【部分國際化代碼】:

ar_sa 阿拉伯語(沙特阿拉伯)
ar_iq 阿拉伯語(伊拉克) 
eu 巴斯克語
bg 保加利亞語 
zh_tw 中文(中國臺灣)
zh_cn 中文(中華人民共和國)
zh_hk 中文(中國香港特別行政區)
zh_sg 中文(新加坡)
hr 克羅地亞語 
en 英語
en_us 英語(美國)
en_gb 英語(英國)
en_au 英語(澳大利亞)
en_ca 英語(加拿大)
本地化對象(Locale)

Java通過java.util.Locale類表示一個本地化對象,它允許通過語言參數和國家/地區參數創建一個確定的本地化對象。

Locale locale=new Locale("zh","cn");//中文,中國
Locale locale2=new Locale("en","us");//英文,美國
Locale locale3=new Locale("zh");//中文--不指定國家
Locale locale4=Locale.CHINA;//中文,中國
Locale locale5=Locale.CHINESE;//中文

在持有一個Locale對象後,我們需要將同一個文字或者數字根據不同的地區/語言格式化成不同的表現形式,所以這裏我們還需要一個格式化的操作,JDK給我們提供以下幾個常見的類用於國際化格式化

NumberFormat:可以處理數字,百分數,貨幣等。下面以貨幣爲例:

public static void main(String[] args) {
    // 1.通過語言跟地區確定一個Locale對象
    // 中國,中文
    Locale chinaLocale = new Locale("zh", "cn");
    // 美國,英文
    Locale usLocale = new Locale("en", "us");
    // 獲取貨幣格式化對象
    NumberFormat chinaCurrencyFormat = NumberFormat.getCurrencyInstance(chinaLocale);
    NumberFormat usLocaleCurrencyFormat = NumberFormat.getCurrencyInstance(usLocale);
    // 中文,中國下的貨幣表現形式
    String chinaCurrency = chinaCurrencyFormat.format(99.9);  // 輸出 ¥99.90
    // 美國,英文下的貨幣表現形式
    String usCurrency = usLocaleCurrencyFormat.format(99.9); // 輸出 $99.90
    System.out.println(chinaCurrency);
    System.out.println(usCurrency);
}
格式化對象

DateFormat:通過DateFormat#getDateInstance(int style,Locale locale)方法按本地化的方式對日期進行格式化操作。該方法第一個入參爲時間樣式,第二個入參爲本地化對象

public static void main(String[] args) {
    // 1.通過語言跟地區確定一個Locale對象
    // 中國,中文
    Locale chinaLocale = new Locale("zh", "cn");
    // 美國,英文
    Locale usLocale = new Locale("en", "us");
    DateFormat chinaDateFormat = DateFormat.getDateInstance(DateFormat.YEAR_FIELD,chinaLocale);
    DateFormat usDateFormat = DateFormat.getDateInstance(DateFormat.YEAR_FIELD,usLocale);
    System.out.println(chinaDateFormat.format(new Date())); // 輸出 2020年1月15日
    System.out.println(usDateFormat.format(new Date()));    // 輸出 January 15, 2020
}

MessageFormat:在NumberFormatDateFormat的基礎上提供了強大的佔位符字符串的格式化功能,它支持時間、貨幣、數字以及對象屬性的格式化操作

  1. 簡單的佔位符替換
public static void main(String[] args) {
    // 1.通過語言跟地區確定一個Locale對象
    // 中國,中文
    Locale chinaLocale = new Locale("zh", "cn");
    String str1 = "{0},你好!你於{1}在農業銀行存入{2}。";
    MessageFormat messageFormat = new MessageFormat(str1,chinaLocale);
    Object[] o = {"小紅", new Date(), 99.99};
    System.out.println(messageFormat.format(o));
    // 輸出:小紅,你好!你於20-1-15 下午4:05在農業銀行存入99.99。
}
  1. 指定格式化類型跟格式化樣式的佔位符替換
public static void main(String[] args) {
    String str1 = "{0},你好!你於{1,date,long}在農業銀行存入{2,number, currency}。";
    MessageFormat messageFormat = new MessageFormat(str1,Locale.CHINA);
    Object[] o = {"小紅", new Date(), 1313};
    System.out.println(messageFormat.format(o));
    // 輸出:小紅,你好!你於2020年1月15日在農業銀行存入¥1,313.00。
}

在上面的例子中,0,1,2代表的是佔位符的索引,從0開始計數。datenumber爲格式化的類型。longcurrency爲格式化樣式。

  • FormatType:格式化類型,取值範圍如下:

​ number:調用NumberFormat進行格式化

​ date:調用DateFormat進行格式化

​ time:調用DateFormat進行格式化

​ choice:調用ChoiceFormat進行格式化

  • FormatStyle::設置使用的格式化樣式

    short
    medium
    long
    full
    integer
    currency
    percent
    SubformatPattern (子格式模式,形如#.##)

對於具體的使用方法就不多贅述了,大家可以自行百度。

資源文件的加載

在實現國際化的過程中,由於我們的用戶界面信息、報錯信息等內容都不能採用硬編碼的方式,所以爲了在不同的區域/語言環境下能進行不同的顯示,我們需要爲不同的環境提供不同的資源文件,同時需要遵循一定的規範。

  • 命名規範:資源名_語言代碼_國/地區代碼.properties

舉一個例子:假設資源名爲content,語言爲英文,國家爲美國,則與其對應的本地化資源文件命名爲content_en_US.properties。

下面以IDEA爲例,創建資源文件並加載讀取

  1. 創建資源文件,在Resource目錄下,創建一個Bundle

在這裏插入圖片描述

  1. 添加需要兼容的區域/語言,我這裏就添加一個英語/美國,給這個Bundle命名爲i18n,名字隨意

在這裏插入圖片描述

  1. 此時會在Resource目錄下生成如下的目錄結構

在這裏插入圖片描述

在兩個配置文件中,我分別添加了兩段配置:

i18n.properties】:

#小明(資源文件對文件內容有嚴格的要求:只能包含ASCII字符,所以必須將非ASCII字符的內容轉換爲Unicode代碼的表示方式)
name=\u5c0f\u660e
#他十九歲了
age=19

i18n_en_US.properties】:

name=Xiaoming
age=19

示例代碼:

public static void main(String[] args) {
    // i18n要跟我們之前創建的Bundle的名稱一致
    // Locale.US指定了我們要拿這個Bundle下的哪個區域/語言對於的資源文件,這裏獲取到的是i18n_en_US.properties這個配置文件
    ResourceBundle usResourceBundle = ResourceBundle.getBundle("i18n", Locale.US);
    System.out.println(usResourceBundle.getString("name")); // 輸出Xiaoming
    System.out.println(usResourceBundle.getString("age"));
    ResourceBundle chinaResourceBundle = ResourceBundle.getBundle("i18n");
    System.out.println(chinaResourceBundle.getString("name"));  // 輸出小明
    System.out.println(chinaResourceBundle.getString("age"));
}

Spring中的MessageSource

在聊完了JAVA中的國際化後,我們迴歸主題,ApplicationContext接口繼承了MessageSource接口,MessageSource接口又提供了國際化的功能,所以ApplicationContext也具有國際化的功能。接下來我們着重看看MessageSource這個接口。

接口定義
public interface MessageSource {

     //code表示國際化資源中的屬性名;args用於傳遞格式化串佔位符所用的運行期參數;
     //當在資源找不到對應屬性名時,返回defaultMessage參數所指定的默認信息;
     //locale表示本地化對象;
    String getMessage(String code, Object[] args, String defaultMessage, Locale locale);

    //與上面的方法類似,只不過在找不到資源中對應的屬性名時,
    //直接拋出NoSuchMessageException異常;
    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;

    //將屬性名、參數數組以及默認信息封裝起來,它的功能和第一個接口方法相同。
    String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

}
UML類圖

在這裏插入圖片描述

我們依次分析下各個類的作用

  1. HierarchicalMessageSource,該接口提供了設置獲取父容器的方法,用於構建MessageSource體系的父子層級結構。其方法定義如下:
// 爲當前MessageSource設置父MessageSource
void setParentMessageSource(@Nullable MessageSource parent);

// 獲取當前MessageSource的父MessageSource
@Nullable
MessageSource getParentMessageSource();
  1. MessageSourceSupport,這個類的作用類似於我們之前介紹的MessageFormat,主要提供了對消息的格式化功能。從這個繼承關係中我們也能看出,Spring在設計時將消息的獲取以及格式化進行了分隔。而在我們實際使用到具體的實現類時,又將功能做了聚合。
  2. DelegatingMessageSource,將所有獲取消息的請求委託給父類查找,如果父類沒有就報錯
  3. AbstractMessageSource,實現了HierarchicalMessageSource,提供了對消息的通用處理方式,方便子類對具體的消息類型實現特定的策略
  4. AbstractResourceBasedMessageSource,提供了對Bundle的處理方式
  5. ResourceBundleMessageSource,基於JDKResourceBundle實現,可以根據名稱加載Bundle
  6. ReloadableResourceBundleMessageSource,提供了定時刷新功能,允許在不重啓系統的情況下,更新資源的信息。
  7. StaticMessageSource,主要用於程序測試
Spring中的簡單使用

我這裏直接取官網中的Demo,先看官網上的一段說明:

在這裏插入圖片描述

從上文中,我們可以得出以下幾點信息:

  1. Spring容器在啓動時會自動查找一個名稱定義的messageSource的Bean(同時需要實現MessageSource接口),如果找到了,那麼所有獲取信息的請求都會由這個類處理。如果在當前容器中沒有找到的話,會在父容器中繼續查找。
  2. 如果沒有找到,那麼Spring會自己new一個DelegatingMessageSource對象,並用這個對象處理消息

基於上面的結論,我們可以做如下配置:

<!--application.xml-->
<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>

同時配置下面三個properties文件:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

測試代碼:

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("application.xml");
    String message1 = resources.getMessage("message", null, "Default", null);
    String message2 = resources.getMessage("argument.required",
                                           new Object [] {"userDao"}, "Required", null);
    System.out.println(message1); // 輸出 Alligators rock!
    System.out.println(message2); // 輸出 The userDao argument is required.
}

同時Spring對資源的加載也遵循我們在JAVA國際化中提到的規範,我們可以將上面例子中的exceptions.properties,更名爲exceptions_en_GB.properties

// 可以看出這種方式跟我們使用JAVA直接加載國際化資源沒有太大差別
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);
}

Spring中的環境(Environment)>

這小結內容對應官網中的1.13小節

在前面的ApplicationContext的繼承關係中我們知道ApplicationContext這個接口繼承了一個EnvironmentCapable接口,而這個接口的定義非常簡單,如下

public interface EnvironmentCapable {
	Environment getEnvironment();
}

可以看到它只是簡單的提供了一個獲取Environment對象的方法,那麼這個Environment對象是做什麼的呢?

1、什麼是環境(Environment)?

它其實代表了當前Spring容器的運行環境,比如JDK環境,系統環境;每個環境都有自己的配置數據,如System.getProperties()可以拿到JDK環境數據、System.getenv()可以拿到系統變量,ServletContext.getInitParameter()可以拿到Servlet環境配置數據。Spring抽象了一個Environment來表示Spring應用程序環境配置,它整合了各種各樣的外部環境,並且提供統一訪問的方法。

2、接口定義
public interface Environment extends PropertyResolver {
    
    // 獲取當前激活的Profile的名稱
	String[] getActiveProfiles();
	
    // 獲取默認的Profile的名稱
	String[] getDefaultProfiles();

	@Deprecated
	boolean acceptsProfiles(String... profiles);
	
    // 判斷指定的profiles是否被激活
	boolean acceptsProfiles(Profiles profiles);

}

public interface PropertyResolver {
	// 當前的環境中是否包含這個屬性
	boolean containsProperty(String key);
	
    //獲取屬性值 如果找不到返回null   
	@Nullable
	String getProperty(String key);
	
    // 獲取屬性值,如果找不到返回默認值        
	String getProperty(String key, String defaultValue);
	
    // 獲取指定類型的屬性值,找不到返回null  
	@Nullable
	<T> T getProperty(String key, Class<T> targetType);
	
    // 獲取指定類型的屬性值,找不到返回默認值  
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);
	
    // 獲取屬性值,找不到拋出異常IllegalStateException  
	String getRequiredProperty(String key) throws IllegalStateException;
	
    // 獲取指定類型的屬性值,找不到拋出異常IllegalStateException         
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
	
    // 替換文本中的佔位符(${key})到屬性值,找不到不解析  
	String resolvePlaceholders(String text);
    
    // 替換文本中的佔位符(${key})到屬性值,找不到拋出異常IllegalArgumentException 
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}

我們可以看到,Environment接口繼承了PropertyResolver,而Environment接口自身主要提供了對Profile的操作,PropertyResolver接口中主要提供了對當前運行環境中屬性的操作,如果我們去查看它的一些方法的實現可以發現,對屬性的操作大都依賴於PropertySource

所以在對Environment接口學習前,我們先要了解Profile以及PropertySource

3、Profile

我們先看官網上對Profile的介紹:

在這裏插入圖片描述

從上面這段話中我們可以知道

  1. Profile是一組邏輯上的Bean的定義
  2. 只有這個Profile被激活時,這組Bean纔會被註冊到容器中
  3. 我們既可以通過註解的方式來將Bean加入到指定的Profile中,也可以通過XML的形式
  4. Environment主要決定哪個Profile要被激活,在沒有激活的Profile時要使用哪個作爲默認的Profile
註解方式(@Profile)

1、簡單使用

@Component
@Profile("prd")
public class DmzService {
	public DmzService() {
		System.out.println("DmzService in prd");
	}
}

@Component
@Profile("dev")
public class IndexService {
	public IndexService(){
		System.out.println("IndexService in dev");
	}
}

public static void main(String[] args) {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    ac.register(ProfileConfig.class);
    //ac.getEnvironment().setActiveProfiles("prd");
    //ac.refresh();  	   // 輸出 DmzService in prd
    ac.getEnvironment().setActiveProfiles("dev");
    ac.refresh(); 		   // 輸出 IndexService in dev
}

在上面的例子中,我們給兩個組件(DmzServiceIndexService)配置了不同的profile,可以看到當我們利用Environment激活不同的Profile時,可以分別只創建不同的兩個類。

在實際生產環境中,我們往往會將"prd""dev"這種代表環境的標籤放到系統環境變量中,這樣依賴於不同系統的同一環境變量,我們就可以將應用程序運行在不同的profile下。

2、結合邏輯運算符使用

有時間我們某一組件可能同時要運行在多個profile下,這個時候邏輯運算符就派上用場了,我們可以通過如下的三種運行符,對profile進行邏輯運算

  • !: 非,只有這個profile不被激活才能生效
  • &: 兩個profile同時激活才能生效
  • |: 只要其中一個profile激活就能生效

比如在上面的例子中,我們可以新增兩個類,如下:

@Component
@Profile("dev & qa")
public class LuBanService {
	public LuBanService(){
		System.out.println("LuBanService in dev & qa");
	}
}

@Component("!prd")
public class ProfileService {
	public ProfileService(){
		System.out.println("ProfileService in !prd");
	}
}

public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(ProfileConfig.class);
//		ac.getEnvironment().setActiveProfiles("prd");
//		ac.refresh();  // 輸出 DmzService in prd
//		ac.getEnvironment().setActiveProfiles("dev");
//		ac.refresh(); // 輸出 IndexService in dev
ac.getEnvironment().setActiveProfiles("dev","qa");
ac.refresh();// 輸出IndexService in dev
//LuBanService in dev & qa
//ProfileService in !prd
}

爲了編碼的語義,有時候我們會將不同的profile封裝成不同的註解,如下:

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

有時候可能我們要將多個Bean同時置於某個profile下,這個時候每個Bean添加一個@Profile註解顯得過去麻煩,這個時候如果我們是採用@Bean方式申明的Bean,可以直接在配置類上添加@Profile註解,如下(這裏我直接取官網中的例子了,就不做驗證了):

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

3、注意一種特殊的場景

如果我們對使用了@Bean註解的方式進行了重載,那麼要求所有重載的方法都在同一個@Profile下,否則@Profile的語義會被覆蓋。什麼意思呢?大家看下面這個Demo

public class A {
	public A() {
		System.out.println("independent A");
	}
	public A(B b) {
		System.out.println("dependent A with B");
	}
}

public class B {

}

@Configuration
public class ProfileConfig {
	@Bean
	@Profile("dev")
	public A a() {
		return new A();
	}

	@Bean
	@Profile("prd")
	public A a(B b) {
		return new A(b);
	}

	@Bean
	@Profile("prd | dev")
	public B b() {
		return new B();
	}
}

public static void main(String[] args) {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    ac.register(ProfileConfig.class);
    ac.getEnvironment().setActiveProfiles("dev");
    ac.refresh(); // 輸出:dependent A with B
}

我們明明激活的是dev這個profile,爲什麼創建的用的帶參的構造函數呢?這是因爲Spring在創建Bean時,方法的優先級高於Profile,前提是方法的參數在Spring容器內(在上面的例子中,如果我們將B的profile限定爲dev,那麼創建的A就會是通過空參構造創建的)。這裏暫且不多說,大家知道有這種場景存在即可。在後面分析源碼時我們會介紹,這裏涉及到Spring對創建Bean的方法的推斷(既包括構造函數也包括factroyMethod)。

XML方式
<!--在beans標籤中指定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>

XML方式不多贅述,大家有需要可以自行研究

4、PropertySource

通過我們的Environment對象,除了能操作profile對象之外,通過之前的繼承結構我們知道,他還能進行一些關於屬性的操作。而這些操作是建立在Spring本身對運行環境中的一些屬性文件的抽象而來的。抽象而成的結果就是PropertySource

接口定義
public abstract class PropertySource<T> {
    protected final  String name;//屬性源名稱
    protected final T source;//屬性源(比如來自Map,那就是一個Map對象)
    public String getName();  //獲取屬性源的名字  
    public T getSource();        //獲取屬性源  
    public boolean containsProperty(String name);  //是否包含某個屬性  
    public abstract Object getProperty(String name);   //得到屬性名對應的屬性值   
    // 返回一個ComparisonPropertySource對象
    public static PropertySource<?> named(String name) {
		return new ComparisonPropertySource(name);
	}
} 

除了上面定義的這些方法外,PropertySource中還定義了幾個靜態內部類,我們在下面的UML類圖分析時進行介紹

UML類圖

在這裏插入圖片描述

從上圖中可以看到,基於PropertySource子類主要可以分爲兩類,一類是StubPropertySource,另一類是EnumerablePropertySource。而StubPropertySource這一類都是申明於PropertySource中的靜態內部類。這兩個類主要是爲了完成一些特殊的功能而設計的。

  1. StubPropertySource:這個類主要起到類似一個佔位符的作用,例如,一個基於ServletContextPropertySource必須等待,直到ServletContext對象對這個PropertySource所在的上下文可用。在這種情況下,需要用到StubPropertySource來預設這個PropertySource的位置跟順序,之後在上下文刷新時期,再用一個ServletContextPropertySourc來進行替換
  2. ComparisonPropertySource:這個類設計的目的就是爲了進行比較,除了hashCode()equals()toString()方法能被調用外,其餘方法被調用時均會拋出異常

PropertySource的另外一些子類,都是繼承自EnumerablePropertySource的,我們先來看EnumerablePropertySource這個類對其父類PropertySource進行了哪些補充,其定義如下:

public abstract class EnumerablePropertySource<T> extends PropertySource<T> {

	public EnumerablePropertySource(String name, T source) {
		super(name, source);
	}

	protected EnumerablePropertySource(String name) {
		super(name);
	}
	
    // 複寫了這個方法
	@Override
	public boolean containsProperty(String name) {
		return ObjectUtils.containsElement(getPropertyNames(), name);
	}
	
    // 新增了這個方法
	public abstract String[] getPropertyNames();

}

這個類跟我們PropertySource的區別在於:

  1. 複寫了containsProperty這個方法
  2. 新增了一個getPropertyNames方法

並且我們可以看到,再containsProperty這個方法中調用了getPropertyNames,這麼做的理由是什麼呢?爲什麼它不直接使用父類的containsProperty方法而要自己複寫一個呢?我們對比下父類的實現:

public boolean containsProperty(String name) {
    return (getProperty(name) != null);
}

結合這個類上的一段javadoc,如下:

A {@link PropertySource} implementation capable of interrogating its
underlying source object to enumerate all possible property name/value
pairs. Exposes the {@link #getPropertyNames()} method to allow callers
to introspect available properties without having to access the underlying
source object. This also facilitates a more efficient implementation of
{@link #containsProperty(String)}, in that it can call {@link #getPropertyNames()}
and iterate through the returned array rather than attempting a call to
{@link #getProperty(String)} which may be more expensive. Implementations may
consider caching the result of {@link #getPropertyNames()} to fully exploit this
performance opportunity.

Spring設計這個類的主要目的是爲了,讓調用者可以不訪問其中的Source對象但是能判斷這個PropertySource中是否包含了指定的key,所以它多提供了一個getPropertyNames,同時這段javadoc還指出,子類的實現應該考慮去緩存getPropertyNames這個方法的返回值去儘可能的壓榨性能。

接下來,我們分別看一看它的各個實現類

  • MapPropertySource

MapPropertySource的source來自於一個Map,這個類結構很簡單,這裏不說。
用法如下:

public static void main(String[] args) {
    Map<String,Object> map=new HashMap<>();
    map.put("name","wang");
    map.put("age",23);
    MapPropertySource source_1=new MapPropertySource("person",map);
    System.out.println(source_1.getProperty("name"));//wang
    System.out.println(source_1.getProperty("age"));//23
    System.out.println(source_1.getName());//person
    System.out.println(source_1.containsProperty("class"));//false
}
  • ResourcePropertySource

source是一個Properties對象,繼承自MapPropertySource。與MapPropertySource用法相同

  • ServletConfigPropertySource

source爲ServletConfig對象

  • ServletContextPropertySource

source爲ServletContext對象

  • SystemEnvironmentPropertySource

繼承自MapPropertySource,它的source也是一個map,但來源於系統環境。

  • CompositePropertySource

內部可以保存多個PropertySource

private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();

取值時依次遍歷這些PropertySource

PropertySources

我們在閱讀PropertySource源碼上,會發現在其上有一段這樣的javaDoc解釋,其中提到了

{@code PropertySource} objects are not typically used in isolation, but rather through a {@link PropertySources} object, which aggregates property sources and in conjunction with a {@link PropertyResolver} implementation that can perform precedence-based searches across the set of {@code PropertySources}.

也就是說,PropertySource通常都不會單獨的使用,而是通過PropertySources對象。

  • 接口定義
public interface PropertySources extends Iterable<PropertySource<?>> {

	default Stream<PropertySource<?>> stream() {
		return StreamSupport.stream(spliterator(), false);
	}
    
	boolean contains(String name);

	@Nullable
	PropertySource<?> get(String name);

}

這個接口由於繼承了Iterable接口,所以它的子類也具備了迭代能力。

  • 唯一子類
public class MutablePropertySources implements PropertySources {
private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();
......
}

這個類最大的特點就是,持有了一個保存PropertySourceCopyOnWriteArrayList集合。並且它其餘提供的方法,都是在往集合中增刪PropertySource

5、PropertyResolver

在之前的Environment的接口定義中我們知道,Environment接口繼承了PropertyResolver接口,接下來我們再來關注下這個接口的定義

接口定義
public interface PropertyResolver {
	// 當前的環境中是否包含這個屬性
	boolean containsProperty(String key);
	
    //獲取屬性值 如果找不到返回null   
	@Nullable
	String getProperty(String key);
	
    // 獲取屬性值,如果找不到返回默認值        
	String getProperty(String key, String defaultValue);
	
    // 獲取指定類型的屬性值,找不到返回null  
	@Nullable
	<T> T getProperty(String key, Class<T> targetType);
	
    // 獲取指定類型的屬性值,找不到返回默認值  
	<T> T getProperty(String key, Class<T> targetType, T defaultValue);
	
    // 獲取屬性值,找不到拋出異常IllegalStateException  
	String getRequiredProperty(String key) throws IllegalStateException;
	
    // 獲取指定類型的屬性值,找不到拋出異常IllegalStateException         
	<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;
	
    // 替換文本中的佔位符(${key})到屬性值,找不到不解析  
	String resolvePlaceholders(String text);
    
    // 替換文本中的佔位符(${key})到屬性值,找不到拋出異常IllegalArgumentException 
	String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;

}
UML類圖

它的實現類主要有兩種:

  1. 各種Resolver:主要是PropertySourcesPropertyResolver
  2. 各種Environment

在這裏插入圖片描述

  • PropertySourcesPropertyResolver使用示例
MutablePropertySources sources = new MutablePropertySources();
sources.addLast(new MapPropertySource("map", new HashMap<String, Object>() {
    {
        put("name", "wang");
        put("age", 12);
    }
}));//向MutablePropertySources添加一個MapPropertySource

PropertyResolver resolver = new PropertySourcesPropertyResolver(sources);
System.out.println(resolver.containsProperty("name"));//輸出 true
System.out.println(resolver.getProperty("age"));//輸出 12
System.out.println(resolver.resolvePlaceholders("My name is ${name} .I am ${age}."));
  • 關於Environment實現主要分爲兩種
  1. StandardEnvironment,標準環境,普通Java應用時使用,會自動註冊System.getProperties()System.getenv()到環境
  2. StandardServletEnvironment:標準Servlet環境,其繼承了StandardEnvironment,Web應用時使用,除了StandardEnvironment外,會自動註冊ServletConfig(DispatcherServlet)ServletContext及有選擇性的註冊JNDI實例到環境

總結

​ 在這篇文章中,我們學習了ApplicationContext的部分知識,首先我們知道ApplicationContext繼承了5類接口,正由於繼承了這五類接口,所以它具有了以下這些功能:

  • MessageSource,主要用於國際化
  • ApplicationEventPublisher,提供了事件發佈功能
  • EnvironmentCapable,可以獲取容器當前運行的環境
  • ResourceLoader,主要用於加載資源文件
  • BeanFactory,負責配置、創建、管理Bean,IOC功能的實現主要就依賴於該接口子類實現。

​ 在上文,我們分析學習了國際化,以及Spring中環境的抽象(Environment)。對於國際化而言,首先我們要知道國際化到底是什麼?簡而言之,國際化就是爲每種語言提供一套相應的資源文件,並以規範化命名的方式保存在特定的目錄中,由系統自動根據客戶端語言選擇適合的資源文件。其次,我們也一起了解了java中的國際化,最後學習了Spring對java國際化的一些封裝,也就是MessageSource接口

​ 對於Spring中環境的抽象(Environment這塊內容比較多,主要要知道Environment完成了兩個功能

  • 爲程序運行提供不同的剖面,即Profile
  • 操作程序運行中的屬性資源

整個Environment體系可以用下圖表示

在這裏插入圖片描述

對上圖的解釋:

  1. Environment可以激活不同的Profile而爲程序選擇不同的剖面,一個Profile其實就是一組Spring中的Bean
  2. Environment繼承了PropertyResolver,從而可以操作程序運行時中的屬性資源。而PropertyResolver的實現依賴於PropertySource,同時PropertySource一般不會獨立使用,而是被封裝進一個PropertySources對象中。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章