Spring原理與原始碼分析系列(三)- Spring IoC容器啟動過程分析(下)
前言
關於Spring容器啟動過程的分析,本章節文章分為兩篇文章進行敘述,第一篇主要介紹Spring中Bean的相關概念以及IoC容器型別;第二篇開始詳細介紹IoC容器的啟動過程。
上篇Spring原理與原始碼分析系列(二)- Spring IoC容器啟動過程分析(上)已經介紹了介紹Spring中Bean的相關概念以及IoC容器型別。本篇主要詳述IoC容器的啟動過程。
四、Spring IoC容器實現過程
Spring IoC容器實現過程可分為兩個階段:
• 容器啟動階段
• Bean例項化階段
下面來詳細解釋這兩個過程。
1、容器啟動階段
容器啟動階段,主要是物件管理資訊的收集。
除了直接程式碼的方式,一般是先讀取和載入配置資訊內容,
並將分析後的資訊編組為BeanDefinition,
然後將儲存了bean定義必要資訊的BeanDefinition註冊到BeanDefinitionRegistry中,這樣啟動工作就完成了。
(第一階段:容器啟動階段)
BeanFactoryPostProcessor
在容器啟動階段,BeanFactoryPostProcessor介面允許我們在容器例項化相應物件之前,對註冊到容器的BeanDefinition所儲存的資訊做相應的修改,比如修改其中bean定義的某些屬性,把bean的scope從singleton改為prototype,也可以把property的值給修改掉,為bean定義增加其他資訊等。
BeanFactoryPostProcessor是在Spring容器載入了bean的定義檔案之後,在bean例項化之前執行的。
BeanFactoryPostProcessor介面定義如下:
public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; }
介面方法的入參是ConfigurrableListableBeanFactory,使用該引數,可以獲取到相關bean的定義資訊.
使用的時候,我們可以自己實現BeanFactoryPostProcessor介面,然後修改Bean屬性。
舉個栗子:
(1)Bean:
package com.wgs.spring.beanfactorypostprocessor; /** * @author GenshenWang.nomico * @date 2017/11/21. */ public class Staff { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
併為其注入屬性值:
<bean id="staff" class="com.wgs.spring.beanfactorypostprocessor.Staff"> <property name="age" value="25"></property> <property name="name" value="wgs"></property> </bean>
(2)自己實現一個BeanFactoryPostProcessor,將Bean屬性中的name原始值“wgs”改為“Jack Ma”。
package com.wgs.spring.beanfactorypostprocessor; import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; /** * @author GenshenWang.nomico * @date 2017/11/21. */ public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor{ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { //BeanFactoryPostProcessor發生在讀取Bean的BeanDefinition後,Bean例項化之前,所以獲取的是BeanDefinition BeanDefinition staffBeanDefinition = beanFactory.getBeanDefinition("staff"); //獲取bean屬性 MutablePropertyValues propertyValues = staffBeanDefinition.getPropertyValues(); if(propertyValues.contains("name")){ propertyValues.addPropertyValue("name", "Jack Ma"); } } }
3)測試:
註釋掉spirng.xml中的配置:
<bean id="myBeanFactoryPostProcessor" class="com.wgs.spring.beanfactorypostprocessor.MyBeanFactoryPostProcessor"></bean>,輸出的結果為”wgs”;
在spirng.xml加上<bean id="myBeanFactoryPostProcessor" class="com.wgs.spring.beanfactorypostprocessor.MyBeanFactoryPostProcessor"></bean>,這樣BeanFactoryPostProcessor就能起作用,使name的值“wgs”被修改為“Jack Ma”,所以輸出的結果也為“Jack Ma”。
如果一個容器有多個實現BeanFactoryPostProcessor的介面,這時候就需要實現類實現org.springframework.core.Ordered介面,設定order屬性來保證自定義的BeanFactoryPostProcessor的實現類的執行順序。
BeanFactoryPostProcessor介面有三個常用的實現類:
• org.springframework.beans.factory.config.PropertyPlaceholderConfigurer:允許我們在XML配置檔案中使用佔位符,並將這些佔位符所代表的資源單獨配置到簡單的properties檔案中來載入;
• org.springframework.beans.factory.config.PropertyOverrideConfigurer:可以通過佔位符,來明確表明bean定義中的property與properties檔案中的各項配置項之間的關係。
• org.springframework.beans.factory.config.CustomEditorConfigurer:用來註冊自定義的屬性編輯器
BeanFactoryPostProcessor類圖:
2 Bean例項化階段
在第一階段容器啟動階段中,所有的bean定義的資訊都被註冊到BeanDefinitionRegistry中,該階段容器僅僅擁有所有物件的BeanDefinition來儲存所有必要的例項化資訊。
當某個請求顯示或隱式呼叫getBean()方法的時候,
就會觸發第二階段:Bean例項化階段。
隱式呼叫有兩種情況: (1)BeanFactory: BeanFactory的物件例項化預設採用的是延遲初始化,即只有對某個Bean使用getBean()方法時,才會對該Bean進行例項化以及依賴注入過程,這是一個顯示呼叫過程。 如果物件A被請求而需要第一次例項化的時候,如果A依賴的物件B沒有被例項化,那麼容器內部會隱士呼叫getBean()方法對物件B進行例項化後,再進行顯示呼叫getBean()例項化物件A過程。 (2)ApplicationContext: ApplicationContext在容器啟動之後就會載入所有的bean定義。 不過在這個過程中,是通過呼叫AbstractApplicationContext的refresh()方法,在這個方法中會呼叫註冊到容器當中所有的bean定義的例項化方法getBean(),完成Bean的例項化。
無論是顯示還是隱士getBean(),Bean定義的getBean()方法第一次被呼叫時才會觸發Bean例項化階段,若getBean()內部發現該Bean沒有被例項化,則會通過createBean()方法進行具體的例項化。第二次以及之後呼叫getBean(),都會直接返回容器內快取的Bean的例項(prototype型別的bean除外)。
下圖是Bean的例項化過程:
Bean的生命週期)
Bean例項化階段容器會首先檢查所有請求的物件之前是否已經初始化,如果沒有,則會根據註冊的BeanDefinition所提供的資訊例項化被請求物件,並未其注入依賴。如果該物件實現了某個某些回撥介面,也會根據回撥介面來裝配。當物件裝配完成以後,容器就會返回該bean。
下面是Bean例項化階段中幾個過程。
(1)Bean的例項化與BeanWrapper
Spring提供了兩種方式來例項化Bean:
• 反射
• CGLIB動態位元組碼生成
Spring採用策略模式選擇上述方式來例項化Bean。
org.springframework.beans.factory.support.InstantiationStrategy 是例項化策略的介面,其直接子類SimpleInstantiationStrategy實現了簡單的物件例項化功能。
而CglibSubclassingInstantiationStrategy繼承了SimpleInstantiationStrategy,可通過CGLIB的動態位元組碼生成功能。
容器內部預設採用的是CglibSubclassingInstantiationStrategy。
在Bean例項化完成後,返回的不是Bean例項,而是以BeanWrapper對構造完成的Bean進行包裝,返回BeanWrapper例項。
為什麼使用BeanWrapper對Bean進行包裝呢?
因為BeanWrapper對Bean例項操作很方便,可以免去直接使用Java反射API操作物件例項的繁瑣。
舉個栗子:
使用反射來操作Bean:
@Test public void testReflect() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException { //1 獲取物件例項 Object commentService = Class.forName("com.wgs.spring.beanwrapperdemo.CommentService").newInstance(); Object commentDao = Class.forName("com.wgs.spring.beanwrapperdemo.CommentDao").newInstance(); //2 獲取屬性 Class commentServiceClazz = commentService.getClass(); Field commentDaoField = commentServiceClazz.getField("commentDao"); //3 設定屬性 commentDaoField.set(commentService, commentDao); System.out.println(((CommentService)commentService).getCount()); }
可以看到,不僅程式碼很繁瑣,需要獲取屬性再設值,還需要處理一堆的異常;
而使用BeanWrapper:
@Test public void testBeanWapper() throws ClassNotFoundException, IllegalAccessException, InstantiationException { //1 獲取物件例項 Object commentService = Class.forName("com.wgs.spring.beanwrapperdemo.CommentService").newInstance(); Object commentDao = Class.forName("com.wgs.spring.beanwrapperdemo.CommentDao").newInstance(); BeanWrapper commentServiceWrapper = new BeanWrapperImpl(commentService); commentServiceWrapper.setPropertyValue("commentDao", commentDao); System.out.println(((CommentService)commentService).getCount()); }
獲取到BeanWrapper包裝的Bean後,只需一行程式碼就可以完成設定注入過程,是不是很簡單呢。
(2)Aware介面
當物件例項化完成且相關屬性即依賴值注入完成後,IoC容器會檢查當前物件例項是否實現了XXXAware介面。如果是,則將這些XXXAware介面定義中規定的依賴注入給當前物件例項。
BeanFactory有如下XXXAware介面:
org.springframework.beans.factory.BeanNameAware介面:如果當前物件例項實現了該介面,會將該物件例項的bean對應的beanName設定到當前物件例項; org.springframework.beans.factory.BeanClassLoaderAware介面:如果當前物件例項實現了該介面,會將該物件例項的bean對應的ClassLoader注入到當前物件例項; org.springframework.beans.factory.BeanFactoryAware介面:如果當前物件例項實現了該介面,BeanFactory容器會將自身設定到當前物件例項;這樣當前物件例項就擁有了一個BeanFactory容器的引用。
ApplicationContext有如下XXXAware介面:
org.springframework.context.ResourceLoaderAware介面:ApplicationContext實現了ResourceLoader介面;若當前物件例項實現了ResourceLoaderAware介面,會將ApplicationContext設定到物件例項,這樣當前物件例項就獲取了ApplicationContext容器的引用; org.springframework.context.ApplicationEventPublisherAware介面:ApplicationContext實現了ApplicationEventPublisher介面;若當前物件例項實現了ApplicationEventPublisherAware介面,會將ApplicationContext自身注入到當前物件例項; org.springframework.context.MessageSourceAware介面:ApplicationContext實現了MessageSource介面;若當前物件例項實現了MessageSourceAware介面,會將ApplicationContext自身注入到當前物件例項; org.springframework.context.ApplicationContextAware介面:若當前物件例項實現了ApplicationContextAware介面,會將ApplicationContext自身注入到當前物件例項(換句話說就是獲得ApplicationContext中的所有bean,即載入Spring上下文環境)。
下面舉個栗子,通過ApplicationContextAware Demo來理解下XXXAware介面的作用:
(1)首先註冊一個Bean:
package com.wgs.spring.xxxawaredemo; /** * @author GenshenWang.nomico * @date 2017/11/22. */ public class User { private String name; private String password; public void setPassword(String password) { this.password = password; } public void setName(String name) { this.name = name; } public String getName() { return name; } public String getPassword() { return password; } } <bean id="user" class="com.wgs.spring.xxxawaredemo.User"> <property name="password" value="12345"></property> <property name="name" value="wgs"></property> </bean>
(2)自己實現一個ApplicationContextAware介面的實現類,通過setApplicationContext方法將ApplicationContext上下文注入到當前物件例項,之後就可以在該物件中獲取ApplicationContext容器的bean的資訊:
package com.wgs.spring.xxxawaredemo; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * @author GenshenWang.nomico * @date 2017/11/22. */ public class MyApplicationContextAware implements ApplicationContextAware{ private ApplicationContext context; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; } public void doGetInfo(){ //獲取到ApplicationContext容器當中的Bean的資訊 User userBean = (User) context.getBean("user"); System.out.println("登入使用者姓名:" + userBean.getName()); System.out.println("登入密碼:" + userBean.getPassword()); } } XML中配置: <bean id="myApplicationContextAware" class="com.wgs.spring.xxxawaredemo.MyApplicationContextAware"></bean>
(3)測試,獲取結果
package com.wgs.spring.xxxawaredemo; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author GenshenWang.nomico * @date 2017/11/22. */ RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration({"classpath:spring.xml"}) public class TestMyApplicationContextAware { @Autowired MyApplicationContextAware myApplicationContextAware; @Test public void testApplicationContextAware(){ myApplicationContextAware.doGetInfo(); } }
測試後,即可獲取Bean的相關資訊。
(3)BeanPostProcessor
上節容器啟動階段說過BeanFactoryPostProcessor這個後置處理器,本節將會了解下處理器BeanPostProcessor。兩者有所相似,區別在於發生的階段不同,
BeanFactoryPostProcessor:存在於容器啟動階段,可以修改Bean屬性; BeanPostProcessor:存在於Bean例項化階段,在Bean例項化完成後增加一些自己的邏輯。
BeanPostProcessor會處理容器內符合條件的例項化後的物件例項。該物件聲明瞭兩個方法:
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException; Object postProcessAfterInitialization(Object var1, String var2) throws BeansException; }
postProcessBeforeInitialization()是BeanPostProcessor前置處理執行方法,
postProcessAfterInitialization()是BeanPostProcessor後 置處理執行方法。
BeanPostProcessor的兩個方法中都傳入了原來物件的例項的引用,這樣就可以對傳入的物件進行操作。
下面舉個栗子簡單感受下BeanPostProcessor的用法:
(1)註冊一個Bean:
package com.wgs.spring.beanpostprocessordemo; import org.springframework.beans.factory.InitializingBean; /** * @author GenshenWang.nomico * @date 2017/11/22. */ public class User{ private String name; private String password; public void setPassword(String password) { this.password = password; } public void setName(String name) { this.name = name; } public String getName() { return name; } public String getPassword() { return password; } } <bean id="user" class="com.wgs.spring.beanpostprocessordemo.User"> </bean>
(2)自己寫一個BeanPostProcessor介面的實現類,在Bean例項化前後加入自己的邏輯:
package com.wgs.spring.beanpostprocessordemo; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * @author GenshenWang.nomico * @date 2017/11/22. */ public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println(beanName + "開始例項化了"); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println(beanName + "例項化完成"); return bean; } }
(3)測試:
package com.wgs.spring.beanpostprocessordemo; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author GenshenWang.nomico * @date 2017/11/22. */ RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration({"classpath:spring.xml"}) public class TestMyBeanPostProcessor { @Test public void testMyBeanPostProcessor(){ //BeanFactory測試 ConfigurableListableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring.xml")); beanFactory.addBeanPostProcessor(new MyBeanPostProcessor()); User user = (User) beanFactory.getBean("user"); //ApplicationContext測試 //ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:spring.xml"); } } 注意,如果是ApplicationContext容器,則只需要在XML配置下MyBeanPostProcessor即可: <bean id="myBeanPostProcessor" class="com.wgs.spring.beanpostprocessordemo.MyBeanPostProcessor"/>
測試結果:
user開始例項化了
user例項化完成
(4)InitializingBean 和 init-method
org.springframework.beans.factory.InitializingBean是容器內部廣泛使用的一個物件生命週期標識介面,其作用在於在物件例項化過程中呼叫BeanPostProcessor的前置處理器postProcessBeforeInitialization後,會接著檢測當前物件是否實現了InitializingBean介面。如果實現了該介面,則會呼叫afterPropertiesSet()方法,對物件做進一步處理。
該介面定義如下:
public interface InitializingBean { void afterPropertiesSet() throws Exception; }
當我們在物件中實現該介面,就可以通過afterPropertiesSet方法來完成初始化操作。但該方法對於容器比較具有侵入性,所以Spring提供了另外一種方法:在XML中使用 < bean>的init-method屬性。
舉個栗子來看下InitializingBean 和 init-method的使用方法:
(1)當Login類中的name和password為Null時,通過實現InitializingBean 介面,在afterPropertiesSet方法中為其附上初始值:
package com.wgs.spring.beanpostprocessordemo; import org.springframework.beans.factory.InitializingBean; /** * @author GenshenWang.nomico * @date 2017/11/23. */ public class Login implements InitializingBean { private String name; private String password; public void setName(String name) { this.name = name; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public String getPassword() { return password; } public void doGetInfo(){ System.out.println(name); System.out.println(password); } @Override public void afterPropertiesSet() throws Exception { if(null == name || "".equals(name)){ name = "Admin"; } if(null == password || "".equals(password)){ password = "12345"; } } }
XML中配置,不賦值:
<bean id="login" class="com.wgs.spring.beanpostprocessordemo.Login"></bean>
(2)測試:
package com.wgs.spring.beanpostprocessordemo; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** * @author GenshenWang.nomico * @date 2017/11/23. */ RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration({"classpath:spring.xml"}) public class TestInitializingBean { @Test public void testInitializingBean(){ ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring.xml"); Login login = (Login) ctx.getBean("login"); login.doGetInfo(); } }
輸出結果:
Admin
12345
使用init-method:
Login 類不變,只是將afterPropertiesSet方法改為initMethod()方法:
package com.wgs.spring.beanpostprocessordemo; import org.springframework.beans.factory.InitializingBean; /** * @author GenshenWang.nomico * @date 2017/11/23. */ public class Login { private String name; private String password; public void setName(String name) { this.name = name; } public void setPassword(String password) { this.password = password; } public String getName() { return name; } public String getPassword() { return password; } public void doGetInfo(){ System.out.println(name); System.out.println(password); } public void initMethod() { if(null == name || "".equals(name)){ name = "Admin"; } if(null == password || "".equals(password)){ password = "12345"; } } }
XML中配置:
<bean id="login" class="com.wgs.spring.beanpostprocessordemo.Login" init-method="initMethod"></bean>
總結:
1、Spring為bean提供了兩種初始化bean的方式:
• 實現現InitializingBean介面,重寫afterPropertiesSet方法,
• 在XML配置檔案中通過init-method指定初始化方法;
2、實現InitializingBean介面是直接呼叫afterPropertiesSet方法,比通過反射呼叫init-method指定的方法效率相對來說要高點。但是init-method方式消除了對spring的依賴
3、如果呼叫afterPropertiesSet方法時出錯,則不呼叫init-method指定的方法。
(5)DisposableBean 和 destroy-method
與InitializingBean介面類似,當Bean實現了DisposableBean 介面或者在XML中聲明瞭destroy-method的指定方法,就會為該例項註冊一個用於物件銷燬的回撥方法。在Spring容器關閉的時候,需要我們告知容器來執行物件的銷燬方法。
BeanFactory容器
我們需要在合適的時機,呼叫ConfigurableListableBeanFactory的destroySingletons()方法來銷燬容器中管理的所有singleton型別的物件例項;
BeanFactory container = new XmlBeanFactory(new ClassPathResource("classpath:spring.xml")); ((ConfigurableListableBeanFactory)container).destroySingletons(); //應用程式退出,容器關閉
ApplicationContext容器
類似BeanFactory,但是AbstractApplicationContext為我們提供了registerShutdownhook()方法,該方法底層使用標準的Runtime類的addShutdownHook()方式來呼叫相應bean物件的銷燬邏輯,從而保證在JVM退出之前,這些singleton型別的bean物件例項自定義銷燬邏輯會被執行。
BeanFactory container = new ClassPathXmlApplicationContext("classpath:spring.xml"); ((AbstractApplicationContext)container).registerShutdownHook(); //應用程式退出,容器關閉
至此,Bean的生命就結束了,Spring Ioc容器啟動過程分析也到此結束,接下來我會從原始碼的角度來深入分析這個過程。