Spring IOC容器實現機制

鳴澗發表於2019-02-13

容器啟動過程

Spring官方關於容器啟動的科普性圖示:

Spring官方過程圖示

其中The Spring Container節點對容器進行了初始化,產生直接可用的Fully configured system

下圖是容器啟動的各個階段圖示:

容器功能實現的各個階段

其中提到的相關介面和類的關係如下圖:

BeanFactory 、 BeanDefinitionRegistry 以及 DefaultListableBeanFactory 的關係

以上類圖中BeanDefinitionRegistry依賴BeanDefinition,其他都是實現關係。

BeanFactoryPostProcessor容器擴充套件機制(第一階段後)

該機制允許我們在容器例項化相應物件之前,對註冊到容器的 BeanDefinition 所儲存的資訊做相應的修改。也就是在容 器實現的第一階段最後加入一道工序。

Spring容器擴充套件機制

BeanFactoryPostProcessor註冊方式

BeanFactory硬編碼註冊BeanFactoryPostProcessor

// 宣告將被後處理的BeanFactory例項
ConfigurableListableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("..."));
// 宣告要使用的BeanFactoryPostProcessor
PropertyPlaceholderConfigurer propertyPostProcessor = new PropertyPlaceholderConfigurer();
propertyPostProcessor.setLocation(new ClassPathResource("..."));
// 執行後處理操作
propertyPostProcessor.postProcessBeanFactory(beanFactory);
複製程式碼

ApplicationContext配置檔案註冊BeanFactoryPostProcessor

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>conf/jdbc.properties</value>
                <value>conf/mail.properties</value>
            </list>
        </property>
    </bean>
複製程式碼

各常用實現類說明

1. PropertyPlaceholderConfigurer

XML配置檔案與具體引數property分離,在XML中使用佔位符匹配properties檔案中的具體引數,如以下形式:

    //XML資料來源配置
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="url">
            <value>${jdbc.url}</value>
        </property>
        <property name="driverClassName">
            <value>${jdbc.driver}</value>
        </property>
        <property name="username">
            <value>${jdbc.username}</value>
        </property>
        <property name="password">
            <value>${jdbc.password}</value>
        </property>
    </bean>
複製程式碼
//properties檔案對應引數
jdbc.url=jdbc:mysql://server/MAIN?useUnicode=true&characterEncoding=ms932&failOverReadOnly=false&roundRobinLoadBalance=true
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=your username=your password jdbc.password
複製程式碼

PropertyPlaceholderConfigurer還會檢查Java的System類中的Properties,可以通過setSystemPropertiesMode()或者setSystemPropertiesModeName()來控制是否載入或者覆蓋System相應Properties的行為。其提供三種模式:

public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport {
    //不使用System的Properties配置項
    public static final int SYSTEM_PROPERTIES_MODE_NEVER = 0;
    //預設模式。properties中找不到對應引數(配置項),則去System的Properties找。
    public static final int SYSTEM_PROPERTIES_MODE_FALLBACK = 1;
    //優先使用System的Properties配置項
    public static final int SYSTEM_PROPERTIES_MODE_OVERRIDE = 2;
    }
複製程式碼

2. PropertyOverrideConfigurer

可以通過PropertyOverrideConfigurer 對容器中配置的任何你想處理的bean定義的property資訊(不需要使用佔位符)進行覆蓋替換。

例如,對於上文的XML資料來源配置,作如下配置:

註冊PropertyOverrideConfigurer

    <bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
        <property name="location" value="pool-adjustment.properties"/>
    </bean>
複製程式碼

pool-adjustment.properties內容:

#鍵引數值對格式為:beanName.propertyName=value
dataSource.minEvictableIdleTimeMillis=1000
dataSource.maxActive=50
複製程式碼

即可替換dataSource的對應引數。

未使用PropertyOverrideConfigurer進行設定的引數依舊使用bean定義中的引數;多個引數對同一個property值進行設定時,以最後一個為準

3. CustomEditorConfigurer

通過XML定義的bean以及其property都需要由String轉換成對應的各式型別物件,這個工作即是由JavaBeanPropertyEditor來完成(Spring也提供了自身實現的一些PropertyEditor,大多位於org.springframework.beans.propertyeditors)。

部分PropertyEditor(容器預設載入):

部分`PropertyEditor`

  • StringArrayPropertyEditor:將符合CSV格式的字串轉換成String[] 陣列的形式,預設是以逗號(,)分隔的字串。
  • ClassEditor:根據String型別的class名稱,直接將其轉換成相應的Class物件。
  • FileEditor:對應java.io.File型別的PropertyEditor,負責資源定位。
  • LocaleEditor:針對java.util.Locale型別的PropertyEditor
  • PatternEditor:針對Java SE 1.4之後才引入的java.util.regex.PatternPropertyEditor
自定義PropertyEditor

兩種方式:

  • 直接實現java.beans.PropertyEditor
  • 繼承java.beans.PropertyEditorSupport,只需要實現setAsText(String)方法。

如下為定製日期格式的PropertyEditorSupport實現:

public class DatePropertyEditor extends PropertyEditorSupport {
    private String datePattern;

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern(getDatePattern());
        Date dateValue = dateTimeFormatter.parseDateTime(text).toDate();
        setValue(dateValue);
    }

    public String getDatePattern() {
        return datePattern;
    }

    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }
    
    public DatePropertyEditor(String datePattern){
        this.datePattern = datePattern;
    }
}
複製程式碼
通過CustomEditorConfigurer註冊自定義的PropertyEditor
  • 容器為BeanFactory時,硬編碼註冊:
XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("……"));
CustomEditorConfigurer ceConfigurer = new CustomEditorConfigurer();

Map customerEditors = new HashMap();
customerEditors.put(java.util.Date.class, new DatePropertyEditor("yyyy/MM/dd"));
ceConfigurer.setCustomEditors(customerEditors);

ceConfigurer.postProcessBeanFactory(beanFactory);
複製程式碼
  • 容器為ApplicationContext時,作為bean註冊:
    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        //Spring2.0前使用customEditors
        <property name="customEditors">
            <map>
                <entry key="java.util.Date">
                    <ref bean="datePropertyEditor"/>
                </entry>
            </map>
        </property>
    </bean>
    
    <bean id="datePropertyEditor" class="...DatePropertyEditor">
        <property name="datePattern">
            <value>yyyy/MM/dd</value>
        </property>
    </bean>
複製程式碼

Spring2.0後提倡使用propertyEditorRegistrars屬性來指定自定義的PropertyEditor

需額外實現PropertyEditorRegistrar

public class DatePropertyEditorRegistrar implements PropertyEditorRegistrar {
    private PropertyEditor propertyEditor;

    public void registerCustomEditors(PropertyEditorRegistry peRegistry) {
        peRegistry.registerCustomEditor(java.util.Date.class, getPropertyEditor());
    }

    public PropertyEditor getPropertyEditor() {
        return propertyEditor;
    }

    public void setPropertyEditor(PropertyEditor propertyEditor) {
        this.propertyEditor = propertyEditor;
    }
}
複製程式碼

此時的bean註冊:

    <bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="propertyEditorRegistrars">
            //多個PropertyEditorRegistrar可以在list裡一一指定
            <list>
                <ref bean="datePropertyEditorRegistrar"/>
            </list>
        </property>
    </bean>
    //Spring2.0後提倡使用DatePropertyEditor
    <bean id="datePropertyEditorRegistrar" class="...DatePropertyEditorRegistrar">
        <property name="propertyEditor">
            <ref bean="datePropertyEditor"/>
        </property>
    </bean>
    <bean id="datePropertyEditor" class="...DatePropertyEditor">
        <property name="datePattern">
            <value>yyyy/MM/dd</value>
        </property>
    </bean>
複製程式碼

BeanFactoryPostProcessor工作機制類圖:

BeanFactoryPostProcessor工作機制類圖
其中,左側部分為BeanFactoryPostProcessor及其子類,右側為`PropertyEditor及其子類,下方為自定義CostomEditor序號產生器制實現。

bean生命週期(第二階段)

當請求方通過BeanFactorygetBean()方法來請求某個物件例項的時候,才有可能觸發Bean例項化階段的活動。

  • 客戶端物件顯式呼叫
  • 容器內部隱式呼叫
    • 對於 BeanFactory 來說,物件例項化預設採用延遲初始化。當初始化A物件時,會隱式初始化A的依賴物件B。
    • ApplicationContext 啟動之後會例項化所有的bean定義。當初始化A物件時,會隱式初始化A的依賴物件B。

bean例項化過程:

`bean`例項化過程

org.springframework.beans.factory.support.AbstractBeanFactory檢視getBean()org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory檢視createBean()

getBean()大致邏輯:(待閱原始碼)

Bean的例項化與 BeanWrapper

可以通過反射或者CGLIB動態位元組碼生成來初始化相應的bean例項或者動態生成其子類。

spring預設使用CglibSubclassingInstantiationStrategy生產被BeanWrapperImpl包裝的目標類的bean

InstantiationStrategy類圖:

InstantiationStrategy類圖

BeanWrapper和他的爸爸們:

BeanWrapper和他的爸爸們

各色的 Aware 介面

當物件例項化完成並且相關屬性以及依賴設定完成之後,Spring容器會檢查當前物件例項是否實現了一系列的以 Aware 命名結尾的介面定義。如果是,則將這些 Aware 介面定義中規定的依賴注入給當前物件例項。

BeanFactory對應的Aware

BeanFactory對應的Aware
ApplicationContext對應的Aware
ApplicationContext對應的Aware

BeanPostProcessor

BeanPostProcessor存在於物件例項化階段。

介面定義如下:

package org.springframework.beans.factory.config;
public interface BeanPostProcessor {

    //例項化前執行
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	//例項化後執行
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}
複製程式碼

例如ApplicationContextAwareProcessor,則是對ApplicationContext對應的Aware進行檢測執行對應操作的BeanPostProcessor實現類,其postProcessBeforeInitialization方法如下:

	public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
		AccessControlContext acc = null;

        //檢測此bean是否實現以下Aware中的一個或多個
		if (System.getSecurityManager() != null &&
				(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
						bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
						bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
			//獲取當前applicationContext的AccessControlContext
			acc = this.applicationContext.getBeanFactory().getAccessControlContext();
		}

		if (acc != null) {
			AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
				invokeAwareInterfaces(bean);
				return null;
			}, acc);
		}
		else {
			invokeAwareInterfaces(bean);
		}

		return bean;
	}
複製程式碼

自定義 BeanPostProcessor

  1. 標註需要進行處理的實現類(可定義並實現標記介面(Aware))
  2. 實現相應的BeanPostProcessor對符合條件的Bean例項進行處理
  3. 將自定義的BeanPostProcessor註冊到容器,註冊方式如下:
  • 對於 BeanFactory 型別的容器,採用硬編碼
ConfigurableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource(...));
beanFactory.addBeanPostProcessor(new CustomPostProcessor());
複製程式碼
  • 對於 ApplicationContext 容器,直接XML註冊
<beans>
    <bean id="customPostProcessor" class="package.name.CustomPostProcessor">
    <!--如果需要,注入必要的依賴-->
    </bean>
    ...
</beans>
複製程式碼

InitializingBean 和 init-method

org.springframework.beans.factory.InitializingBean是容器內部廣泛使用的一個物件生命週期標識介面,用於在BeanPostProcessor的前置處理執行後進一步編輯實現該介面的bean,如下:

public interface InitializingBean {

	/**
	 * Invoked by the containing {@code BeanFactory} after it has set all bean properties
	 * and satisfied {@link BeanFactoryAware}, {@code ApplicationContextAware} etc.
	 * <p>This method allows the bean instance to perform validation of its overall
	 * configuration and final initialization when all bean properties have been set.
	 * @throws Exception in the event of misconfiguration (such as failure to set an
	 * essential property) or if initialization fails for any other reason
	 */
	void afterPropertiesSet() throws Exception;

}
複製程式碼

實際開發中使用 <bean>init-method 屬性來代替上述方式。一般用於整合第三方庫。

DisposableBean 與 destroy-method

InitializingBeaninit-method對應,用於執行singleton型別的物件銷燬操作。

為該例項註冊一個用於物件銷燬的回撥(Callback),以便在這些singleton型別的物件例項銷燬之前,執行銷燬邏輯。

例如Spring註冊的資料庫連線池:

    <!--銷燬方法為BasicDataSource自定義的close方法-->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
          destroy-method="close">
        <property name="url">
            <value>${jdbc.url}</value>
        </property>
        <property name="driverClassName">
            <value>${jdbc.driver}</value>
        </property>
        <property name="username">
            <value>${jdbc.username}</value>
        </property>
        <property name="password">
            <value>${jdbc.password}</value>
        </property>
        ...
    </bean>
複製程式碼

BeanFactory:
應在程式退出或者其他業務場景呼叫ConfigurableBeanFactorydestroySingletons()方法(處理所有實現DisposableBean介面和註冊了destroy-method方法的類)銷燬容器管理的所有singleton型別的物件例項。

/**
 * BeanFactory銷燬單例例項方法呼叫。
 */
public class ApplicationLauncher {
    public static void main(String[] args) {
        BasicConfigurator.configure();
        BeanFactory container = new XmlBeanFactory(new ClassPathResource("..."));
        BusinessObject bean = (BusinessObject) container.getBean("...");
        bean.doSth();
        ((ConfigurableListableBeanFactory) container).destroySingletons();
        // 應用程式退出,容器關閉
    }
}
複製程式碼

ApplicationContext
AbstractApplicationContext為我們提供了registerShutdownHook()方法,該方法底層使用標準的Runtime類的addShutdownHook()方式來呼叫相應bean物件的銷燬邏輯。

/**
 * 使用 registerShutdownHook() 方法註冊並觸發物件銷燬邏輯回撥行為
 */
public class ApplicationLauncher {
    public static void main(String[] args) {
        BasicConfigurator.configure();
        BeanFactory container = new ClassPathXmlApplicationContext("...");
        ((AbstractApplicationContext) container).registerShutdownHook();
        BusinessObject bean = (BusinessObject) container.getBean("...");
        bean.doSth();
        // 應用程式退出,容器關閉
    }
}
複製程式碼

相關文章