IOC介紹
相信大多數人在學習Spring時 IOC 和 Bean 算得上是最常聽到的兩個名詞,IOC在學習Spring當中出現頻率如此之高必然有其原因。如果我們做一個比喻的話,把Bean說成Spring中主角的話,那麼IOC便是這個主角進行演出的舞台,沒有IOC作為Bean的承載,那麼Bean便不會在編程中大放異彩。作為Spring核心組件的重要一員,瞭解其內部實現對我們編程和窺探Spring內幕是相當有協助的,下面一步步從源碼的角度來剖析IOC究竟是怎樣實現的。 介面設計
首先我們先通過一張介面設計圖瞭解下,IOC容器整體的設計架構
源碼剖析 BeanFacory
通過上面的介面設計圖我們可以看到,在Spring當中最基本的IOC容器介面是BeanFactory,這個介面當中定義作為一個IOC容器最基本的一些操作和功能,下來看看它的源碼:
package org.springframework.beans.factory;import org.springframework.beans.BeansException;import org.springframework.core.ResolvableType;/** * BeanFactory作為最原始同時也最重要的Ioc容器,它主要的功能是為依賴注入 (DI) 提供支援,BeanFactory 和相關的介面.這裡定義的只是一系列的介面方法,通過這一系列的BeanFactory介面,可以使用不同的Bean的檢索方法很方便地從Ioc容器中得到需要的Bean,從而忽略具體的Ioc容器的實現,從這個角度上看,這些檢索方法代表的是最為基本的容器入口。 * * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams * @since 13 April 2001 */public interface BeanFactory { /** * 轉定義符"&" 用來引用執行個體,或把它和工廠產生的Bean區分開,就是說,如果一個FactoryBean的名字為a,那麼,&a會得到那個Factory * * FactoryBean和BeanFactory 是在Spring中使用最為頻繁的類,它們在拼字上很相似。一個是Factory,也就是Ioc容器或對象工廠;一個 * 是Bean。在Spring中,所有的Bean都是由BeanFactory(也就是Ioc容器)來進行管理的。但對FactoryBean而言,這個Bean不是簡單的Be * an,而是一個能產生或者修飾對象產生的工廠Bean,它的實現與設計模式中的原廠模式和修飾器模式類似。 */ String FACTORY_BEAN_PREFIX = "&"; /** * 五個不同形式的getBean方法,擷取執行個體 * @param name 檢索所用的Bean名 * @return Object(<T> T) 執行個體對象 * @throws BeansException 如果Bean不能取得 */ Object getBean(String name) throws BeansException; <T> T getBean(String name, Class<T> requiredType) throws BeansException; <T> T getBean(Class<T> requiredType) throws BeansException; Object getBean(String name, Object... args) throws BeansException; <T> T getBean(Class<T> requiredType, Object... args) throws BeansException; /** * 讓使用者判斷容器是否含有指定名字的Bean. * @param name 搜尋所用的Bean名 * @return boolean 是否包含其中 */ boolean containsBean(String name); /** * 查詢指定名字的Bean是否是Singleton類型的Bean. * 對於Singleton屬性,可以在BeanDefinition指定. * @param name 搜尋所用的Bean名 * @return boolean 是否包是Singleton * @throws NoSuchBeanDefinitionException 沒有找到Bean */ boolean isSingleton(String name) throws NoSuchBeanDefinitionException; /** * 查詢指定名字的Bean是否是Prototype類型的。 * 與Singleton屬性一樣,可以在BeanDefinition指定. * @param name 搜尋所用的Bean名 * @return boolean 是否包是Prototype * @throws NoSuchBeanDefinitionException 沒有找到Bean */ boolean isPrototype(String name) throws NoSuchBeanDefinitionException; /** * 查詢指定了名字的Bean的Class類型是否是特定的Class類型. * @param name 搜尋所用的Bean名 * @param typeToMatch 匹配類型 * @return boolean 是否是特定類型 * @throws NoSuchBeanDefinitionException 沒有找到Bean */ boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException; boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException; /** * 查詢指定名字的Bean的Class類型. * @param name 搜尋所用的Bean名 * @return 指定的Bean或者null(沒有找到合適的Bean) * @throws NoSuchBeanDefinitionException 沒有找到Bean */ Class<?> getType(String name) throws NoSuchBeanDefinitionException; /** * 查詢指定了名字的Bean的所有別名,這些都是在BeanDefinition中定義的 * @param name 搜尋所用的Bean名 * @return 指定名字的Bean的所有別名 或者一個空的數組 */ String[] getAliases(String name);}
通過上面的介面說明我們看到在BeanFactory裡面只對IOC容器最基本的行為做了定義,而不關心Bean是怎樣定義和載入的。如果我們想要知道一個工廠具體生產對象的過程,就需要去看這個介面的實作類別,在Spring當中實現這個介面的有很多子類,下面我們來看看其一個常用的實作類別XmlBeanFacotry的代碼吧。 XmlBeanFactory
package org.springframework.beans.factory.xml;import org.springframework.beans.BeansException;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.support.DefaultListableBeanFactory;import org.springframework.core.io.Resource;/** * XmlBeanFactory是BeanFactory的最簡單實作類別 * * XmlBeanFactory的功能是建立在DefaultListableBeanFactory這個基本容器的基礎上的,並在這個基本容器的基礎上實行了其他諸如 * XML讀取的附加功能。XmlBeanFactory使用了DefaultListableBeanFactory作為基礎類,DefaultListableBeanFactory是一個很重 * 要的Ioc實現,會在下一章進行重點論述。 * * @author Rod Johnson * @author Juergen Hoeller * @author Chris Beams * @since 15 April 2001 */public class XmlBeanFactory extends DefaultListableBeanFactory { private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this); /** * 根據給定來源,建立一個XmlBeanFactory * @param resource Spring中對與外部資源的抽象,最常見的是對檔案的抽象,特別是XML檔案。而且Resource裡面通常 * 是儲存了Spring使用者的Bean定義,比如applicationContext.xml在被載入時,就會被抽象為Resource來處理。 * @throws BeansException 載入或者解析中發生錯誤 */ public XmlBeanFactory(Resource resource) throws BeansException { this(resource, null); } /** * 根據給定來源和BeanFactory,建立一個XmlBeanFactory * @param resource Spring中對與外部資源的抽象,最常見的是對檔案的抽象,特別是XML檔案。而且Resource裡面通常 * 是儲存了Spring使用者的Bean定義,比如applicationContext.xml在被載入時,就會被抽象為Resource來處理。 * @param parentBeanFactory 父類的BeanFactory * @throws BeansException 載入或者解析中發生錯誤 */ public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { super(parentBeanFactory); this.reader.loadBeanDefinitions(resource); }}
我們可以看到要構造XmlBeanFactory對象的話需要一個Resource對象,這個Resource是什麼呢。
簡單的講Resource介面是為了提供更強的訪問底層資源能力的抽象,它是spring訪問資源最基本的介面。
通過XmlBeanFactory我們也可以大致可以猜到,對於該類的要傳的Resource對象便是一個xml的檔案流。那麼,這下我們知道這個BeanFactory的簡單實作類別的用法,也知道了構造這個對象的的參數,先動手實踐建立一個最原始的Ioc容器吧。 利用XmlBeanFactory實現最原始Ioc容器 準備工作
1、實體類User.java
public class User{ private String name; private int age; getter(); setter();}
2、XML資源檔beans.xml
<bean id="user1" name="user1" class="com.yanxiao.ioc.User"> <property name="name" value="yanxiao"></property> <property name="age" value="20"></property> </bean>
最原始IOC容器的使用
package com.yanxiao.ioc;import org.springframework.beans.factory.BeanFactory;import org.springframework.beans.factory.xml.XmlBeanFactory;import org.springframework.core.io.ClassPathResource;/** * Created by yanxiao on 2016/7/28. */@SuppressWarnings("deprecation")public class MySimpleBeanFactory { public static void main(String[] args) { ClassPathResource resource = new ClassPathResource("META-INF/beans.xml"); BeanFactory beanFactory = new XmlBeanFactory(resource); User user = beanFactory.getBean("user1", User.class); System.out.println(user.getName()+" : "+user.getAge()); }}跟蹤上面的代碼流程
使用單步調試瞭解上面代碼的主要內部調用流程,這裡沒有跟蹤getBean()方法的過程,該步驟後設計Bean的解析過程,後面我們再說這個
| 調用順序 |
類名 |
方法名 |
| 1 |
XmlBeanFactory |
構造方法(Resource) |
| 2 |
DefaultListableBeanFactory |
構造方法(BeanFactory) |
| 3 |
AbstractAutowireCapableBeanFactory |
構造方法(BeanFactory) |
| 4 |
AbstractAutowireCapableBeanFactory |
setParentFactory(BeanFactory) |
| 5 |
XmlBeanDefinitionReader |
loadBeanDefinitions(Resource) |
| 6 |
AbstractBeanDefinitionReader |
getResourceLoader() |
通過上面的流程,我們可以簡單的將使用IOC容器的步驟概括如下:
1.建立Ioc設定檔的抽象資源,這個抽象資源套件含了BeanDefinition的定義資訊 2.建立一個BeanFactory,這裡使用了DefaultListableBeanFactory 3.建立一個載入BeanDefinition的讀取器,這裡使用XmlBeanDefinitionReader來載入XML檔案形式的BeanDefinition 4.然後將上面定位好的Resource,通過一個回調配置給BeanFactory 5.從定位好的資源位置讀入配置資訊,具體的解析過程由XmlBeanDefinitionReader完成 6.完成整個載入和註冊Bean定義之後,需要的Ioc容器就初步建立起來了
我們對這個內部流程有了瞭解了以後,下面可以在實踐當中進行應用,我們之前寫的MySimpleBeanFactory代碼便可以更改為下面的形式:
public static void main(String[] args) { ClassPathResource resource = new ClassPathResource("META-INF/beans.xml"); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); reader.loadBeanDefinitions(resource); User user = factory.getBean("user", User.class); System.out.println(user.getName()+" : "+user.getAge()); }
兩者的作用是等效的。
我們現在對IOC大體上有了初步瞭解後,下面對一些關鍵點進行細節上的一些分析。首先,從我們第一步建立的ClassPathResource對象說起,重新回過頭來認識Spring當中的Resource。 Resource介面體系
在上面的文章我們也簡單提到過 Resource 介面的作用,其實Spring使用自己的抽象結構對整個架構中所用到資源進行統一的封裝,封裝這個介面的原因其實大家也大致可以想到,我們在編程當中所遇到的資源檔類型多種多樣有File、InputStream、UrlResource、ContextResource等等,面對如此多的資源類型架構內部要是不設計出一套成熟合理的資源封裝體系的話,無疑對架構的實現和開發人員的編程來說造成很多的複雜性。物件導向編程當中的封裝和繼承特性,在Spring當中可謂應用的淋漓盡致,有時間自己可以花點功夫體會Spring架構這一套設計體系對我們的以後編寫可擴充和可用代碼有很大的好處。
繼續展開上面Resource介面的介紹,該介面作為資源的一個原始封裝,它繼承自InputStreamResource介面,InputStreamResource只有InputStream getInputStream()這樣一個方法,通過這次繼承賦予了通過Resource對象獲得InputStram流的功能。下面看看介面的源碼: InputStreamResource介面
package org.springframework.core.io;import java.io.IOException;import java.io.InputStream;/** * @author Juergen Hoeller * @since 20.01.2004 */public interface InputStreamSource { /** * 返回InputStream的類,比如File、Classpath下的資源和Byte Array等 * @return InputStream 返回一個新的InputStream的對象 * @throws IOException */ InputStream getInputStream() throws IOException;}Resource介面
package org.springframework.core.io;import java.io.File;import java.io.IOException;import java.net.URI;import java.net.URL;/** * Resource介面抽象了所有Spring內部使用到的底層資源:File、URL、Classpath等。 * 同時,對於來源不同的資源檔,Resource也有不同實現:檔案(FileSystemResource)、Classpath資源(ClassPathResource)、 * URL資源(UrlResource)、InputStream資源(InputStreamResource)、Byte數組(ByteArrayResource)等等。 * * @author Juergen Hoeller * @since 28.12.2003 */public interface Resource extends InputStreamSource { /** * 判斷資源是否存在 * @return boolean 是否存在 */ boolean exists(); /** * 判斷資源是否可讀 * @return boolean 是否可讀 */ boolean isReadable(); /** * 是否處於開啟狀態 * @return boolean 是否開啟 */ boolean isOpen(); /** * 得到URL類型資源,用於資源轉換 * @return URL 得到URL類型 * @throws IOException 如果資源不能開啟則拋出異常 */ URL getURL() throws IOException; /** * 得到URI類型資源,用於資源轉換 * @return URI 得到URI類型 * @throws IOException 如果資源不能開啟則拋出異常 */ URI getURI() throws IOException; /** * 得到File類型資源,用於資源轉換 * @return File 得到File類型 * @throws IOException 如果資源不能開啟則拋出異常 */ File getFile() throws IOException; /** * 擷取資源長度 * @return long 資源長度 * @throws IOException 如果資源不能開啟則拋出異常 */ long contentLength() throws IOException; /** * 擷取lastModified屬性 * @return long 擷取lastModified * @throws IOException 如果資源不能開啟則拋出異常 */ long lastModified() throws IOException; /** * 建立一個相對的資源方法 * @param relativePath 相對路徑 * @return Resource 返回一個新的資源 * @throws IOException 如果資源不能開啟則拋出異常 */ Resource createRelative(String relativePath) throws IOException; /** * 擷取檔案名稱 * @return String 檔案名稱或者null */ String getFilename(); /** * 得到錯誤處理資訊,主要用於錯誤處理的資訊列印 * @return String 錯誤資源資訊 */ String getDescription();}ClassPathResource
再看看我們上面代碼當中所用到的Resource介面的實作類別ClassPathResource的部分需要重點關注的源碼
package org.springframework.core.io;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;import java.net.URL;import org.springframework.util.Assert;import org.springframework.util.ClassUtils;import org.springframework.util.ObjectUtils;import org.springframework.util.StringUtils;/** * @author Juergen Hoeller * @author Sam Brannen * @since 28.12.2003 * @see ClassLoader#getResourceAsStream(String) * @see Class#getResourceAsStream(String) */public class ClassPathResource extends AbstractFileResolvingResource { private final String path; private ClassLoader classLoader; private Class<?> clazz; /** 通過資源的路徑建立一個ClassPathResource對象 */ public ClassPathResource(String path) { this(path, (ClassLoader) null); } /** 上面構造方法的具體實現 */ public ClassPathResource(String path, ClassLoader classLoader) { Assert.notNull(path, "Path must not be null"); String pathToUse = StringUtils.cleanPath(path); //去掉路徑最前面的 / if (pathToUse.startsWith("/")) { pathToUse = pathToUse.substring(1); } this.path = pathToUse; this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); } /** * Create a new {@code ClassPathResource} for {@code Class} usage. * The path can be relative to the given class, or absolute within * the classpath via a leading slash. * @param path relative or absolute path within the class path * @param clazz the class to load resources with * @see java.lang.Class#getResourceAsStream */ public ClassPathResource(String path, Class<?> clazz) { Assert.notNull(path, "Path must not be null"); this.path = StringUtils.cleanPath(path); this.clazz = clazz; } /** 獲得InputStram的具體實現 * This implementation opens an InputStream for the given class path resource. * @see java.lang.ClassLoader#getResourceAsStream(String) * @see java.lang.Class#getResourceAsStream(String) */ @Override public InputStream getInputStream() throws IOException { InputStream is; if (this.clazz != null) { is = this.clazz.getResourceAsStream(this.path); } else if (this.classLoader != null) { is = this.classLoader.getResourceAsStream(this.path); } else { is = ClassLoader.getSystemResourceAsStream(this.path); } if (is == null) { throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist"); } return is; } /** 獲得檔案名稱 * This implementation returns the name of the file that this class path * resource refers to. * @see org.springframework.util.StringUtils#getFilename(String) */ @Override public String getFilename() { return StringUtils.getFilename(this.path); }}DefaultListableBeanFactory類
我們已經把資源層面上的介面和實現瞭解了,下面分析我們之前代碼當中的DefaultListableBeanFactory factory = new DefaultListableBeanFactory();這句。在這裡我們有引入了一個新類DefaultListableBeanFactory,從IOC類圖中可以看到這個是BeanFactory的一個預設實作類別,其執行個體對象可以作為一個獨立使用的IOC容器,可以認為該對象是容納Bean對象的一個容器,我們定義好的Bean對象經過Spring的載入和載入等處理後,最終交由改工廠進行管理,我們需要得到Bean對象時便可以向它擷取。
我們調用其無參構造方法建立了factory對象,看看底層都做了哪些事情。
@SuppressWarnings("serial")public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { /** * Create a new DefaultListableBeanFactory. */ public DefaultListableBeanFactory() { super();//調用了父類的無參構造方法 }}
接下來跟蹤父類AbstractAutowireCapableBeanFactory中源碼 AbstractAutowireCapableBeanFactory類
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { /** * Create a new AbstractAutowireCapableBeanFactory. */ public AbstractAutowireCapableBeanFactory() { super();//父類AbstractBeanFactory的無參構造方法,代碼在下面↓↓↓ ignoreDependencyInterface(BeanNameAware.class); ignoreDependencyInterface(BeanFactoryAware.class); ignoreDependencyInterface(BeanClassLoaderAware.class); }}AbstractBeanFactory類
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory { /** * Create a new AbstractBeanFactory. */ public AbstractBeanFactory() { //然而這裡並沒有做什麼 }}BeanDefinition的載入、解析和註冊
上面我們建立好了 DefaultListableBeanFactory 對象,已經有了容納Bean的容器了,但我們通過剛才分析源碼可以看到現在我們還沒有對該factory進行其他實用性的操作,裡面還沒有任何東西。那麼,我們定義在設定檔定義的Bean是如何進入Spring容器當中的,就得繼續分析下面的代碼了。我們先分析XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);的實現。
從XmlBeanDefinitionReader類名中我們又見到一個新名詞BeanDefinition,我們先瞭解下Spring當中BeanDefinition的作用。
BeanDefinition類似於Resource介面的功能,起到的作用就是對所有的Bean進行一層抽象的統一,把形式各樣的對象統一封裝成一個便於Spring內部進行協調管理和調度的資料結構,BeanDefinition屏蔽了不同對象對於Spring架構的差異。
接著我們看XmlBeanDefinitionReader類中關鍵區段的源碼 XmlBeanDefinitionReader類
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { //對Resource參數首先做一層封裝處理,主要是為了獲得getReader()這個方法的實現,源碼在下面↓↓↓↓ return loadBeanDefinitions(new EncodedResource(resource)); }public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isInfoEnabled()) { logger.info("Loading XML bean definitions from " + encodedResource.getResource()); } Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); if (currentResources == null) { currentResources = new HashSet<EncodedResource>(4); this.resourcesCurrentlyBeingLoaded.set(currentResources);