1、前言
上一章介紹了Spring IOC容器的設計與實現,同時也講到了高級容器ApplicationContext中有個refresh()方法,執行了這個方法標志着 IOC 容器正式啟動,簡單來說,IOC 容器的初始化是由refresh()方法來啟動的。而在Spring IOC 容器啟動的過程中,會將Bean解析成Spring內部的BeanDefinition結構。不管是通過xml配置文件的<Bean>標簽,還是通過注解配置的@Bean,它最終都會被解析成一個BeanDefinition信息對象,最后我們的Bean工廠就會根據這份Bean的定義信息,對Bean進行實例化、初始化等等操作。
從上可知BeanDefinition這個對象對Spring IoC容器的重要之處,並且IOC的初始化都是圍繞這個BeanDefinition來進行的。所以了解好了它,能讓我們更大視野的來看Spring管理Bean的一個過程,也能透過現象看本質。所以這里再次強調一次BeanDefinition對象的作用:簡單來說,BeanDefinition在Spring中是用來描述Bean對象的,它本身並不是一個Bean實例,而是包含了Bean實例的所有信息,比如類名、屬性值、構造器參數、scope、依賴的bean、是否是單例類、是否是懶加載以及其它信息。其實就是將Bean實例定義的信息存儲到這個BeanDefinition相應的屬性中,后面Bean對象的創建是根據BeanDefinition中描述的信息來創建的,例如拿到這個BeanDefinition后,可以根據里面的類名、構造函數、構造函數參數,使用反射進行對象創建。也就是說 IOC容器可以有多個BeanDefinition,並且一個BeanDefinition對象對應一個<bean>標簽中的信息。當然BeanDefinition的最終目的不只是用來存儲Bean實例的所有信息,而是為了可以方便的進行修改屬性值和其他元信息,比如通過BeanFactoryPostProcessor進行修改一些信息,然后在創建Bean對象的時候就可以結合原始信息和修改后的信息創建對象了。
2、IOC容器的初始化步驟
我們知道,在refresh()之后IOC 容器的啟動會經過一段很復雜的過程,我們暫時不要求全部了解清楚,但是現在大體了解一下 Spring IoC 初始化的過程還是必要的。這對於理解 Spring 的一系列行為是很有幫助的。IOC 容器初始化包括BeanDefinition的Resource定位、載入和注冊三個基本過程,如果我們了解如何編程式的使用 IOC 容器(編程式就是使用DefaultListableBeanFactory來創建容器),就可以清楚的看到Resource定義和載入過程的接口調用,在下面的內容中,我們將會詳細分析這三個過程的實現。
IOC 容器的初始化包括的三個過程介紹如下:
- Resource定位過程:這個Resource定位指的是BeanDefinition的資源定位,就是對開發者的配置文件(Xml)進行資源的定位,並將其封裝成Resource對象。它由ResourceLoader通過統一的Resource接口來完成,這個Resource對各種形式的BeanDefinition的使用都提供了統一接口。比如:在文件系統中的Bean定義信息可以使用FileSystemResource來進行抽象。在類路徑中的Bean定義信息可以使用ClassPathResource來進行抽象等等。這個定位過程類似於容器尋找數據的過程,就像用水捅裝水先要把水找到一樣。
- BeanDefinition的載入:這個載入過程是將Resource 定位到的信息,表示成IoC容器內部的數據結構,而這個容器內部的數據結構就是BeanDefinition。
- BeanDefinition的注冊:這個注冊過程把上面載入過程中解析得到的BeanDeftnition向IoC容器進行注冊。注冊過程是通過調用BeanDefinitionRegistry接口的實現來完成的。在IoC容器內部將BeanDefinition注人到一個HashMap中去,IoC容器就是通過這個HashMap來持有這些BeanDefinition數據的。
注意:Bean的定義和初始化在 Spring IoC 容器是兩大步驟,它是先定義,然后再是初始化和依賴注入。所以當Spring做完了以上 3 步后,Bean 就在 Spring IoC 容器中被定義了,而沒有被初始化,更沒有完成依賴注入,所以此時仍然沒有對應的 Bean 的實例,也就是沒有注入其配置的資源給 Bean,也就是它還不能完全使用。對於初始化和依賴注入,Spring Bean 還有一個配置選項——【lazy-init】,其含義就是:是否默認初始化 Spring Bean。在沒有任何配置的情況下,它的默認值為default,實際值為 false(默認非懶加載),也就是 Spring IoC 容器默認會自動初始化 Bean。如果將其設置為 true(懶加載),那么只有當我們使用 Spring IoC 容器的 getBean 方法獲取它時,它才會進行 Bean 的初始化,完成依賴注入。
3、BeanDefinition的Resource定位
在Spring框架中,如果想要獲取系統中的配置文件,就必須通過Resource接口的實現來完成,Resource是Sping中用於封裝I/O操作的接口。例如我們前面在以編程的方式使用DefaultListableBeanFactory時,首先是定義一個Resource來定位容器使用的BeanDefinition,這里使用的是Resource的實現類ClassPathResource,這時Spring會在類路徑中去尋找以文件形式存在BeanDefinition。
ClassPathResource resource = new ClassPathResource("beans.xml");
但是這里的Resource並不能由 DefaultListableBeanFactory 直接使用,而是需要通過Spring中的 BeanDefinitionReader 來對這些信息進行處理。在這里,我們也可以看到使用 ApplicationContext 相對於直接使用 DefaultListableBeanFactory 的好處,因為在ApplicationContext中,Spring已經為我們提供了一系列加載不同Resource的讀取器實現,而在 DefaultListableBeanFactory 只是一個純粹的IOC容器,需要為它配置配置特定的讀取器才能完成這些功能,當然了 利和弊 是共存的,使用 DefaultListableBeanFactory 這樣更底層的IOC容器,能提高定制IOC容器的的靈活性。
常用的Resource資源類型如下:
- FileSystemResource:以文件的絕對路徑方式進行訪問資源,效果類似於Java中的File;
- ClassPathResourcee:以類路徑的方式訪問資源,效果類似於this.getClass().getResource("/").getPath();
- ServletContextResource:web應用根目錄的方式訪問資源,效果類似於request.getServletContext().getRealPath("");
- UrlResource:訪問網絡資源的實現類。例如file: http: ftp:等前綴的資源對象;
- ByteArrayResource: 訪問字節數組資源的實現類。
回到我們經常使用的ApplicationContext上來,它給我們提供了一系列加載不同Resource的讀取器實現,例如ClassPathXmlApplicationContext、FileSystemXmlApplicationContext以及XmlWebApplicationContext等等,簡單的從這些類的名字上分析,可以清楚的看到他們可以提供哪些不同的Resource讀入功能,比如:ClassPathXmlApplicationContext可以從 classpath載入Resource,FileSystemXmlApplicationContext可以從文件系統中載入Resource,XmlWebApplicationContext可以在Web容器中載入Resource等。
我們通常喜歡拿ClassPathXmlApplicationContext來舉例,所以這里用它來分析ApplicationContext是如何來完成BeanDefinition的Resource定位,首先來看一下ClassPathXmlApplicationContext的整繼承體系:
通過上面的圖片並且查看繼承關系可知,ClassPathXmlApplicationContext繼承了AbstractApplicationContext,所以該實現類具備了讀取Resource定義的BeanDefinition的能力。因為AbstractApplicationContext的基類是DefaultResourceLoader。而且其它的類如FileSystemXmlApplicationContext、XmlWebApplicationContext等等都如出一轍。也是通過DefaultResourceLoader讀取Resource。
下面我們再來看一下ClassPathXmlApplicationContext的順序圖。通過這個順序圖可以清晰的看到IOC容器的初始化階段所調用的各個方法。
那么接下來我們從ClassPathXmlApplicationContext這個類來分析Spring的IoC容器是如何一步一步完成定位的:
①、我們知道IOC容器的啟動是從refresh()方法開始的,所以我們先從refresh()方法開始:ClassPathXmlApplicationContext類中調用的refresh()方法是其繼承的基類 AbstractApplicationContext中的實現,所以先跟蹤AbStractApplicationContext中的refresh()方法:
注意:在refresh()中我們先重點看obtainFreshBeanFactory()這個方法,這是IoC容器初始化的入口。
public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { //刷新上下文環境 prepareRefresh(); //我們先着重看這個方法 這是初始化容器的地方,是在子類中啟動refreshBeanFactory() //並且在這里獲得新的BeanFactory,解析XML、Java類,並加載BeanDefinition ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); //准備bean工廠,以便在此上下文中使用 prepareBeanFactory(beanFactory); try { //設置 beanFactory 的后置處理 postProcessBeanFactory(beanFactory); //調用 BeanFactory 的后處理器,這些處理器是在Bean 定義中向容器注冊的 invokeBeanFactoryPostProcessors(beanFactory); //注冊Bean的后處理器,在Bean創建過程中調用 registerBeanPostProcessors(beanFactory); //對上下文中的消息源進行初始化 initMessageSource(); //初始化上下文中的事件機制 initApplicationEventMulticaster(); //初始化其他特殊的Bean onRefresh(); //檢查監聽Bean並且將這些監聽Bean向容器注冊 registerListeners(); //實例化所有的(non-lazy-init)單件 finishBeanFactoryInitialization(beanFactory); //發布容器事件,結束Refresh過程 finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } destroyBeans(); cancelRefresh(ex); throw ex; } finally { //重置Spring公共的緩存 resetCommonCaches(); } } }
②、然后點擊obtainFreshBeanFactory()這個方法,它還在AbstractApplicationContext中實現,這個obtainFreshBeanFactory()很關鍵,這里面有 IoC的Resource定位和載入。
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { refreshBeanFactory(); return getBeanFactory(); }
進來后發現其調用refreshBeanFactory和getBeanFactory方法,表示重新獲取一個新的BeanFactory實例。
③、繼續跟蹤refreshBeanFactory()方法,點擊進入。
protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
可以看到這里只是定義了抽象方法,既然是抽象的方法,那么肯定有具體的實現,那這個具體初始化IOC容器的實現在哪呢?在AbstractApplicationContext中沒有做具體實現。我們從前面的繼承圖可知,AbstractApplicationContext還有很多子類,所以肯定是交給其子類完成,實現解耦,讓初始化IOC容器變得更加靈活。
所以我們從其子類AbstractRefreshableApplicationContext中找到實現的refreshBeanFactory()方法。
protected final void refreshBeanFactory() throws BeansException { //這里判斷,如果存在了BeanFactory,則銷毀並關閉該BeanFactory if (hasBeanFactory()) { destroyBeans(); closeBeanFactory(); } try { //這里的創建新的BeanFactory,對於DefaultListableBeanFactory前面一章已經介紹了很多了,應該都知道它的作用 DefaultListableBeanFactory beanFactory = createBeanFactory(); beanFactory.setSerializationId(getId()); customizeBeanFactory(beanFactory); //載入Bean ,抽象方法,委托子類AbstractXmlApplicationContext實現 //后面會看到一系列重載的loadBeanDefinitions方法 loadBeanDefinitions(beanFactory); synchronized (this.beanFactoryMonitor) { this.beanFactory = beanFactory; } } catch (IOException ex) { throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); } }
上面的代碼主要分為這么幾個步驟:
- 首先判斷BeanFactory是否存在,如果存在(不為NULL),則銷毀關閉該BeanFactory。也就是清除跟Bean有關的Map或者List等屬性集合,並且將BeanFactory設置為null,序列化Id設置為null。
- 然后創建一個新的DefaultListableBeanFactory(這個類是Spring Bean初始化的核心類),所以我們看下創建DefaultListableBeanFactory的地方:createBeanFactory(),這個方法 是在AbstractRefreshableApplicationContext中實現,所以AbstractApplicationContext 讓我們可以充分自由的實例化自己想初始化的原始IOC容器。
protected DefaultListableBeanFactory createBeanFactory() { //getInternalParentBeanFactory 獲取當前容器已有的父親容器,來作為新容器的父容器,這個方法是在AbstractApplicationContext中實現的。 return new DefaultListableBeanFactory(getInternalParentBeanFactory()); }
- 最后對新建的BeanFactory進行設置,包括bean序列化Id的設置、bean的特殊設置,bean載入操作。然后將beanFactory賦值給本類的beanFactory屬性。注意:customizeBeanFactory(beanFactory)里面只做了兩件事:一個是設置bean是否允許覆蓋,另一個是設置bean是否允許循壞使用。
④、跟蹤loadBeanDefinitions(beanFactory)方法。
protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException;
這個方法的具體實現是由子類AbstractXmlApplicationContext具體實現的。所以我們知道了該怎么去找這個loadBeanDefinitions的具體實現了吧。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { //創建一個xml配置讀寫器用於解析xml文件中定義的bean XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); //設置BeanDefinitionReader 的相關屬性 //1.設置 Environment,即環境,與容器的環境一致 beanDefinitionReader.setEnvironment(this.getEnvironment()); //2.設置 ResourceLoader,即資源加載器,具體加載資源的功能,這個加載器很重要,后面會用到 // 這里傳一個this進去,因為ApplicationContext是實現了ResourceLoader接口 beanDefinitionReader.setResourceLoader(this); //3.設置 EntityResolver,即實體解析器,這里用於解析資源加載器加載的資源內容 beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); //這個方法默認實現是空的,允許用戶自定義實現讀取器的定制化,需要實現接口,可以設置xml解析完成校驗,定制化解析器等 initBeanDefinitionReader(beanDefinitionReader); // 這里開始就是 加載、獲取BeanDefinition資源定位,並且是載入模塊的開始了 loadBeanDefinitions(beanDefinitionReader); }
⑤、繼續跟蹤loadBeanDefinitions(beanDefinitionReader)方法,這個方法在AbstractXMLApplicationContext中有實現,我們看下。
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { //以Resource的方式獲取所有定位到的resource資源位置(用戶定義) //但是現在不會走這條路,因為配置文件還沒有定位到,也就是沒有封裝成Resource對象。 Resource[] configResources = getConfigResources(); if (configResources != null) { reader.loadBeanDefinitions(configResources);//載入resources } //以String的方式獲取所有配置文件的位置(容器自身) String[] configLocations = getConfigLocations(); if (configLocations != null) { reader.loadBeanDefinitions(configLocations);//載入resources } }
這里主要是獲取到用戶定義的resource資源位置以及獲取所以本地配置文件的位置。
⑥、進入第二個reader.loadBeanDefinitions(configLocations)方法。從這里開始就是BeanDefinitionReader模塊的實現了,也就是ApplicationContext上下文將BeanDefinition的定位加載工作交付到了XmlBeanDefinitionReader。這個方法是由XmlBeanDefinitionReader的基類AbstractBeanDefinitionReader來實現的。
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException { Assert.notNull(locations, "Location array must not be null"); int count = 0; //循壞加載配置文件 for (String location : locations) { count += loadBeanDefinitions(location); } return count; }
這里就是循環加載xml配置文件的路徑,然后返回總個數。
⑦、下面我們繼續跟蹤loadBeanDefinitions(loaction)這個方法,它是還在AbstractBeanDefinitionReader的類中實現。
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException { return loadBeanDefinitions(location, null); }
⑧、繼續跟蹤上面代碼中的 loadBeanDefinitions(location, null)。
進入到loadBeanDefinitions(String location, Set<Resource> actualResources)這個方法,依然在AbstractBeanDefinitionReader類中。
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException { //這里取到ResourceLoader對象(其實DefaultResourceLoader對象) ResourceLoader resourceLoader = getResourceLoader(); if (resourceLoader == null) { throw new BeanDefinitionStoreException( "Cannot load bean definitions from location [" + location + "]: no ResourceLoader available"); } //這里對Resource的路徑模式進行解析,比如我們設定的各種Ant格式的路徑定義,得到需要的Resource集合, //這些Resource集合指定我們已經定義好的BeanDefinition信息,可以是多個文件。 if (resourceLoader instanceof ResourcePatternResolver) { try { //把字符串類型的xml文件路徑,形如:classpath*:user/**/*-context.xml,轉換成Resource對象類型, //其實就是用流的方式加載配置文件,然后封裝成Resource對象 Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); //加載Resource資源中的Bean,然后返回加載數量,這個loadBeanDefinitions就是Bean的載入了 int count = loadBeanDefinitions(resources); if (actualResources != null) { Collections.addAll(actualResources, resources); } if (logger.isTraceEnabled()) { logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]"); } return count; } catch (IOException ex) { throw new BeanDefinitionStoreException( "Could not resolve bean definition resource pattern [" + location + "]", ex); } } else { // Can only load single resources by absolute URL. // 調用DefaultResourceLoader的getResource(String)方法來獲取資源定位,然后封裝成Resource對象,這里只能加載一個資源 Resource resource = resourceLoader.getResource(location); //循環加載所有的資源,返回總數,這個loadBeanDefinitions就是Bean的載入了 int count = loadBeanDefinitions(resource); if (actualResources != null) { //對於成功找到的Resource定位,都會添加到這個傳入的actualResources參數中 actualResources.add(resource); } if (logger.isTraceEnabled()) { logger.trace("Loaded " + count + " bean definitions from location [" + location + "]"); } return count; } }
這個方法中主要將xml配置文件加載到內存中並封裝成為Resource對象。但是它是怎么操作的呢?在上述代碼中,loadBeanDefinitions()方法中可能調用ResourcePatternResolver或DefaultResourceLoader中的getResource()方法,這兩個類一個是繼承、一個是實現ResourceLoader。其中ResourcePatternResolver用於解析資源文件的策略接口,其特殊的地方在於,它應該提供帶有*號這種通配符的資源路徑。DefaultResourceLoader用於用來加載資源,並且具體實現了ResourceLoader中的方法。而在第④步的時候,在實例化XmlBeanDefinitionReader的時候已經設置ResourceLoader,並且ResourceLoad為ApplicationContext,然后也設置了ResourcePatternResolver。所以XmlBeanDefinitionReader有了加載資源和解析資源的功能。
⑨、所以我們直接來看getResource()方法,DefaultResourceLoader中的 getResource(String)實現。
public Resource getResource(String location) { Assert.notNull(location, "Location must not be null"); //看有沒有自定義的ProtocolResolver,如果有則先根據自定義的ProtocolResolver解析location得到Resource for (ProtocolResolver protocolResolver : getProtocolResolvers()) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } } //根據路徑是否匹配"/"或"classpath:"來解析得到ClassPathResource if (location.startsWith("/")) { return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); } else { try { //這里處理帶有URL標識的Resource定位 URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { //如果既不是classPath 也不是URL標識的Resource定位(那其實就是自己實現的了).則把getResource的重任交給getResourceByPath來完成, //這個方法是一個protected方法,默認的實現是得到一個ClassPathContextResource,這個方法常常會用子類來實現也就是FileSystemXMLApplicationContext return getResourceByPath(location); } } }
通過上述代碼可以看到,getResource最后又調用了子類實現的getResourceByPath方法或是子類傳遞過來的字符串,從而實現Resource定位。使得整個Resource定位過程就說得通了。總結起來就是,Resource資源通過最外層的實現類傳進來的字符串或者直接調用getResourceByPath方法,來獲取bean資源路徑。
對上面的代碼進行四步來進行介紹:
- 第一步:首先看有沒有自定義的ProtocolResolver,如果有則先根據自定義的ProtocolResolver解析location得到Resource(默認ProtocolResolver是空的,后面我們會說)
for (ProtocolResolver protocolResolver : getProtocolResolvers()) { Resource resource = protocolResolver.resolve(location, this); if (resource != null) { return resource; } }
這里的protocolResolvers是DefaultResourceLoader類中的成員變量,而這個成員變量是ProtocolResolver類型的Set集合。
- 第二步:再根據路徑是否匹配"/"或"classpath:"來解析得到ClassPathResource。
if (location.startsWith("/")) { return getResourceByPath(location); } else if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); }
- 第三步:最后處理帶有URL標識的Resource定位,加載得到一個UrlResource,如果都不是這些類型,則交給getResourceByPath來完成。
else { try { // Try to parse the location as a URL... URL url = new URL(location); return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url)); } catch (MalformedURLException ex) { // No URL -> resolve as resource path. return getResourceByPath(location); } }
- 第四步:上面的getResourceByPath()方法會根據路徑加載Resource對象
protected Resource getResourceByPath(String path) { return new ClassPathContextResource(path, getClassLoader()); }
上面方法返回的是一個ClassPathContextResource對象,通過這個對象Spring就可以進行相關的I/O操作了。
因為對ProtocolResolver這個類不是很熟悉,所以我去了解了一下,ProtocolResolver翻譯過來就是"協議解析器",這個接口類里就只有一個方法,方法如下:
Resource resolve(String location, ResourceLoader resourceLoader);
我們在第一步的時候調用了ProtocolResolver的resolve方法,如果你要使用ProtocolResolver。我們可以自定義一個類實現ProtocolResolver接口,然后實現該resolve方法,就可以解析特定的location得到Resoure。是的,ProtocolResolver是解析location的自定義拓展類,有了它我們才能隨意傳入不同格式的location,然后根據對應的格式去解析並獲得我們的Resource即可。
關於DefaultResourceLoader和ProtocolResolver的區別:
- DefaultResourceLoader類的作用是加載Resource
- ProtocolResolver是解析location獲取Resource的拓展
默認情況下,DefaultResourceLoader類中的protocolResolvers成員變量是一個空的Set,即默認情況下是沒有ProtocolResolver可以去解析的,只能走ClassPath和URL兩種方式獲得Resource。
至此我們的Resource定位已經全部完成了。饒了這么遠就是為了拿到這個Resource對象,拿到這個對象后,就可以通過AbstractBeanDefinitionReader流操作來實現Resource的載入,最后通過AbstractApplicationContext的registerListeners來進行注冊。這就是IoC容器的初始化過程。所以下面我們來介紹一下Resource的載入工程。
4、BeanDefinition的載入
在完成對Resource定位分析之后,就可以通過獲取的Resource對象進行BeanDefinition的載入了。對IOC容器來說,這個載入過程,相當於把定義的bean在IOC容器中轉化成一個Spring內部表示的數據結構的過程,也就是將其轉化為BeanDefinition,IOC容器對Bean的管理和依賴注入功能的實現,是通過對其持有的BeanDefinition進行各種相關操作來完成的,這些BeanDefinition在IOC容器中通過一個HashMap來保持和維護。
我們繼續跟蹤AbstractBeanDefinitionReader中的loadBeanDefinitions方法,之前跟蹤到的是如下圖的loadBeanDefinitions方法。
①、繼續跟到loadBeanDefinitions(resource)方法。
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException { Assert.notNull(resources, "Resource array must not be null"); int count = 0; // 將所有定位到的Resource資源全部加載,交給XmlBeanDefinitionReader實現的方法來處理這些resource for (Resource resource : resources) { count += loadBeanDefinitions(resource); } return count; }
這里循環加載定位到Resource資源,這個方法跟前面循環加載資源路徑類似,但加載的內容不一樣。
②、然后點擊進入loadBeanDefinitions(resource),進入之后我們可以發現,在BeanDefinitionReader接口定義了兩個加載Resource資源的方法:
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException; int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
這兩個方法具體由BeanDefinitionReader接口的子類XmlBeanDefinitionReader 實現,其繼承關系如下圖所示。
XmlBeanDefinitionReader主要用來將Bean的XML配置文件轉換為多個BeanDefinition對象的工具類,所以它會將定位到的Resource資源進行處理。我們先來看上面兩個實現的方法,大致過程是,先將resource包裝為EncodeResource類型,然后繼續進行處理,為生成BeanDefinition對象為后面做准備,我們在XmlBeanDefinitionReader類中找到實現的方法,其主要的兩個方法的源碼如下。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException { //包裝resource為EncodeResource類型 return loadBeanDefinitions(new EncodedResource(resource)); } public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { Assert.notNull(encodedResource, "EncodedResource must not be null"); if (logger.isTraceEnabled()) { logger.trace("Loading XML bean definitions from " + encodedResource); } // 這里使用threadLocal來保證並發的同步 Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get(); //先添加threadLocal,加載完之后finally中再移除threadLocal if (!currentResources.add(encodedResource)) { throw new BeanDefinitionStoreException( "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); } // 通過resource對象得到XML文件內容輸入流,並為I/O的InputSource做准備 try (InputStream inputStream = encodedResource.getResource().getInputStream()) { InputSource inputSource = new InputSource(inputStream); if (encodedResource.getEncoding() != null) { inputSource.setEncoding(encodedResource.getEncoding()); } //這里就是具體讀取Xml文件的方法 return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); } catch (IOException ex) { throw new BeanDefinitionStoreException( "IOException parsing XML document from " + encodedResource.getResource(), ex); } finally { currentResources.remove(encodedResource); if (currentResources.isEmpty()) { this.resourcesCurrentlyBeingLoaded.remove(); } } }
③、接着進入doLoadBeanDefinitions方法,這里就是具體讀取Xml文件的方法,也是從指定xml文件中實際載入BeanDefinition的地方。當然了這肯定是在XmlBeanDefinitionReader中的方法了。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException { try { //這里取得的是XML文件的Document對象,具體的解析過程是由DocumentLoader完成的, //這里使用的DocumentLoader是DefaultDocumentLoader,在定義documentLoader對象時候創建的 Document doc = doLoadDocument(inputSource, resource); //這里啟動的是對BeanDefinition解析的詳細過程,也就是將document文件的bean封裝成BeanDefinition,並注冊到容器 //啟動對BeanDefinition解析的詳細過程,這個解析會用到Spring的Bean配置規則,是我們下面詳細講解的內容 int count = registerBeanDefinitions(doc, resource); if (logger.isDebugEnabled()) { logger.debug("Loaded " + count + " bean definitions from " + resource); } return count; } catch () { 省略...... } }
DefaultDocumentLoader這個類大致了解即可,感興趣可自行百度。
④、下面我們主要關心的是Spring的BeanDefinition是怎么樣按照Spring的Bean語義要求進行解析 並轉化為容器內部數據結構的,這個過程是在registerBeanDefinitions(doc, resource)中完成的,具體的過程是BeanDefinitionDocumentReader來完成的,這個registerBeanDefinitions還對載入的Bean數量進行了統計,這個方法也是在 XmlBeanDefinitionReader 中自己實現的,
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { //這里得到的BeanDefinitionDocumentReader對象來對XML的BeanDefinition信息進行解析 BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); //獲取容器中bean的數量 int countBefore = getRegistry().getBeanDefinitionCount(); //具體的解析過程在這個方法中實現 documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); return getRegistry().getBeanDefinitionCount() - countBefore; }
注意:BeanDefinition的載入分成兩部分,首先通過調用XML的解析器(XmlBeanDefinitionReader)得到document對象,但這些document對象並沒有 按照Spring的Bean規則去進行解析,在完成通用XML解析之后才是按照Spring得 Bean規則進行解析的地方,這個按照Spring的Bean規則進行解析的過程是在documentReade中實現的,這里使用的documentReader是默認設置好的DefaultBeanDefinitionDocumentReader,創建的過程也是在XmlBeanDefinitionReader 中完成的,根據指定的默認方式如下:
private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class; protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() { return BeanUtils.instantiateClass(this.documentReaderClass); }
上面通過通過 XmlBeanDefinitionReader 類中的私有屬性 documentReaderClass 獲得一個 DefaultBeanDefinitionDocumentReader 實例對象,並且具體的解析過程在DefaultBeanDefinitionDocumentReader來實現,所以下面我們繼續跟蹤。
⑤、DefaultBeanDefinitionDocumentReader實現了BeanDefinitionDocumentReader接口,它的registerBeanDefinitions方法定義如下:
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { this.readerContext = readerContext; doRegisterBeanDefinitions(doc.getDocumentElement()); }
這里只是將 XML中的元素取了出來,但是具體的活還是 doRegisterBeanDefinitions(root)來實現的,do開頭的方法才是真正干活的方法。
⑥、所以繼續跟蹤doRegisterBeanDefinitions(root)方法
protected void doRegisterBeanDefinitions(Element root) { // 創建了BeanDefinitionParserDelegate對象 BeanDefinitionParserDelegate parent = this.delegate; this.delegate = createDelegate(getReaderContext(), root, parent); // 如果是Spring原生命名空間,首先解析 profile標簽,這里不重要 if (this.delegate.isDefaultNamespace(root)) { String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); if (StringUtils.hasText(profileSpec)) { String[] specifiedProfiles = StringUtils.tokenizeToStringArray( profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { if (logger.isDebugEnabled()) { logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource()); } return; } } } //解析BeanDefinition之前做的一些事情的接口觸發 preProcessXml(root); //主要看這個方法,標簽具體解析過程 parseBeanDefinitions(root, this.delegate); // 解析BeanDefinition之后可以做的一些事情的觸發 postProcessXml(root); this.delegate = parent; }
在這個方法中,我們重點看“一類三法”,也就是BeanDefinitionParserDelegate類和preProcessXml、parseBeanDefinitions、postProcessXml三個方法。其中BeanDefinitionParserDelegate類非常非常重要(需要了解代理技術,如JDK動態代理、cglib動態代理等)。Spirng BeanDefinition的解析就是在這個代理類下完成的,此類包含了各種對符合Spring Bean語義規則的處理,比如<bean></bean>、<import></import>、<alias><alias/>等的檢測。對於preProcessXml、parseBeanDefinitions、postProcessXml這三個方法,其中preProcessXml和postProcessXml都是空方法,意思是在解析標簽前后我們自己可以擴展需要執行的操作,也是一個模板方法模式,體現了Spring的高擴展性。parseBeanDefinitions方法才是標簽的具體解析過程。所以下面進入parseBeanDefinitions方法看具體是怎么解析標簽的。
⑦、前面提到Document對象不能通過XmlBeanDefinitionReader,真正去解析Document文檔樹的是 BeanDefinitionParserDelegate完成的,這個解析過程是與Spring對BeanDefinition的配置規則緊密相關的,parseBeanDefinitions(root, delegate)方法如下:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { if (delegate.isDefaultNamespace(root)) { NodeList nl = root.getChildNodes(); // 遍歷所有節點,做對應解析工作 // 如遍歷到<import>標簽節點就調用importBeanDefinitionResource(ele)方法對應處理 // 遍歷到<bean>標簽就調用processBeanDefinition(ele,delegate)方法對應處理 for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node instanceof Element) { Element ele = (Element) node; if (delegate.isDefaultNamespace(ele)) { //默認標簽解析 parseDefaultElement(ele, delegate); } else { //自定義標簽解析 delegate.parseCustomElement(ele); } } } } else { delegate.parseCustomElement(root); } }
這里有兩種標簽的解析:Spring原生標簽和自定義標簽,那來怎么區分這兩種標簽呢?如下:
- 默認標簽:<bean:/>
- 自定義標簽:<context:component-scan/>
如果帶有bean的就是Spring默認標簽,否則就是自定義標簽。但無論哪種標簽在使用前都需要在Spring的xml配置文件里聲明Namespace URI,這樣在解析標簽時才能通過Namespace URI找到對應的NamespaceHandler。引入:xmlns:context=http://www.springframework.org/schema/contex http://www.springframework.org/schema/beans
⑧、上面的代碼中先是isDefaultNamespace判斷是不是默認標簽,然后進入parseDefaultElement方法(自定義方法感興趣可以自行百度):
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { // 解析<import>標簽 if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { importBeanDefinitionResource(ele); } // 解析<alias>標簽 else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { processAliasRegistration(ele); } // 解析<bean>標簽,最常用,過程最復雜 else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { processBeanDefinition(ele, delegate); } // 解析<beans>標簽 else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { // recurse doRegisterBeanDefinitions(ele); } }
這里面主要是對import、alias、bean標簽的解析以及beans的字標簽的遞歸解析。
⑨、這里針對常用的<bean>標簽中的方法做簡單介紹,其他標簽的加載方式類似,進入processBeanDefinition方法。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { //BeandefinitionHolder是BeanDefinition的封裝,封裝了BeanDefinition,bean的名字和別名,用它來完成向IOC容器注冊, //得到BeanDefinitionHodler就意味着BeanDefinition是通過BeanDefinitionParseDelegate對xml元素按照bean的規則解析得到的 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { // 這里是向IOC容器解析注冊得到BeanDefinition的地方 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, ex); } // 在BeanDefinition向Ioc容器注冊完成后發送消息 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); } }
⑩、進入parseBeanDefinitionElement(Element ele)方法方法。
注意:parseBeanDefinitionElement(Element ele)方法會調用parseBeanDefinitionElement(ele, null)方法,需要強調一下的是parseBeanDefinitionElement(ele, null)方法中產生了一個抽象類型的BeanDefinition實例,這也是我們首次看到直接定義BeanDefinition的地方,這個方法里面會將<bean>標簽中的內容解析到BeanDefinition中,如果在解析<bean>標簽的過程中出現錯誤則返回null,之后再對BeanDefinition進行包裝,將它與beanName,Alias等封裝到BeanDefinitionHolder 對象中,然后返回BeanDefinitionHolder類對象,該部分源碼如下:
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { return parseBeanDefinitionElement(ele, null); } public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { // 獲取id和name屬性 String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); // 獲取別名屬性,多個別名可用,;隔開 List<String> aliases = new ArrayList<>(); if (StringUtils.hasLength(nameAttr)) { String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); aliases.addAll(Arrays.asList(nameArr)); } String beanName = id; if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { beanName = aliases.remove(0); if (logger.isTraceEnabled()) { logger.trace("No XML 'id' specified - using '" + beanName + "' as bean name and " + aliases + " as aliases"); } } // 檢查beanName是否重復 if (containingBean == null) { checkNameUniqueness(beanName, aliases, ele); } // 具體的解析封裝過程還在這個方法里 AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); if (beanDefinition != null) { if (!StringUtils.hasText(beanName)) { try { if (containingBean != null) { beanName = BeanDefinitionReaderUtils.generateBeanName( beanDefinition, this.readerContext.getRegistry(), true); } else { beanName = this.readerContext.generateBeanName(beanDefinition); // Register an alias for the plain bean class name, if still possible, // if the generator returned the class name plus a suffix. // This is expected for Spring 1.2/2.0 backwards compatibility. String beanClassName = beanDefinition.getBeanClassName(); if (beanClassName != null && beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { aliases.add(beanClassName); } } if (logger.isTraceEnabled()) { logger.trace("Neither XML 'id' nor 'name' specified - " + "using generated bean name [" + beanName + "]"); } } catch (Exception ex) { error(ex.getMessage(), ele); return null; } } String[] aliasesArray = StringUtils.toStringArray(aliases); return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); } return null; }
上面的解析過程可以看做根據xml文件對<bean>的定義生成BeanDefinition對象的過程,這個BeanDefinition對象中封裝的數據大多都是與<bean>相關的,例如:init-method,destory-method,factory-method,beanClass,descriptor。有了這個BeanDefinition中分裝的信息,容器才能對Bean配置進行處理以及實現容器的特性。至此,我們的BeanDefine就已經載入完成了。
⑪、下面再來多加一個點,看一下bean的具體解析。
public AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); // 獲取class名稱和父類名稱 String className = null; if (ele.hasAttribute(CLASS_ATTRIBUTE)) { className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); } // 解析 parent 屬性 String parent = null; if (ele.hasAttribute(PARENT_ATTRIBUTE)) { parent = ele.getAttribute(PARENT_ATTRIBUTE); } try { // 創建GenericBeanDefinition對象 AbstractBeanDefinition bd = createBeanDefinition(className, parent); // 解析bean標簽的屬性,並把解析出來的屬性設置到BeanDefinition對象中 parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); //解析bean中的meta標簽 parseMetaElements(ele, bd); //解析bean中的lookup-method標簽 parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); //解析bean中的replaced-method標簽 parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); //解析bean中的constructor-arg標簽 parseConstructorArgElements(ele, bd); //解析bean中的property標簽 parsePropertyElements(ele, bd); // 解析子元素 qualifier 子元素 parseQualifierElements(ele, bd); bd.setResource(this.readerContext.getResource()); bd.setSource(extractSource(ele)); return bd; } catch (ClassNotFoundException ex) { error("Bean class [" + className + "] not found", ele, ex); } catch (NoClassDefFoundError err) { error("Class that bean class [" + className + "] depends on not found", ele, err); } catch (Throwable ex) { error("Unexpected failure during bean definition parsing", ele, ex); } finally { this.parseState.pop(); } return null; }
上面的代碼是具體生成BeanDefinition的地方,bean標簽的解析步驟仔細理解並不復雜,就是將一個個標簽屬性的值裝入到了BeanDefinition對象中,這里需要注意parseConstructorArgElements和parsePropertyElements方法,分別是對constructor-arg和property標簽的解析,解析完成后分別裝入了BeanDefinition對象的constructorArgumentValues和propertyValues中,而這兩個屬性在c和p標簽的解析中還會用到,而且還涉及一個很重要的設計思想——裝飾器模式。Bean標簽解析完成后將生成的BeanDefinition對象、bean的名稱以及別名一起封裝到了BeanDefinitionHolder對象並返回,然后調用了decorateBeanDefinitionIfRequired進行裝飾,后面具體的調用就不具體介紹了,想了解的可以自行百度。
5、BeanDefinition的注冊
在完成了BeanDefinition的載入和解析后,就要對它進行注冊。我們知道最終Bean配置會被解析成BeanDefinition並與beanName,Alias一同封裝到BeanDefinitionHolder類中,然后返回這個對象,所以我們順着BeanDefinitionHolder類創建的地方,也就是DefaultBeanDefinitionDocumentReader的processBeanDefinition()方法繼續往下看。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { //BeandefinitionHolder是BeanDefinition的封裝,封裝了BeanDefinition,bean的名字和別名,用它來完成向IOC容器注冊, //得到BeanDefinitionHodler就意味着BeanDefinition是通過BeanDefinitionParseDelegate對xml元素按照bean的規則解析得到的 BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); if (bdHolder != null) { bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); try { // 這里是向IOC容器解析注冊得到BeanDefinition的地方 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); } catch (BeanDefinitionStoreException ex) { getReaderContext().error("Failed to register bean definition with name '" + bdHolder.getBeanName() + "'", ele, ex); } // 在BeanDefinition向Ioc容器注冊完成后發送消息 getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); } }
然后跟蹤到BeanDefinitionReaderUtils的registerBeanDefinition()方法,這里會傳入上一步的BeanDefinitionHolder對象,並且將BeanDefinition注冊到IoC容器中。進入BeanDefinitionReaderUtils類的registerBeanDefinition方法如下。
public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // 注冊beanDefinition!! String beanName = definitionHolder.getBeanName(); registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // 如果有別名的話也注冊進去 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
之后會調用BeanDefinitionRegistry接口的registerBeanDefinition( beanName, bdHolder.getBeanDefinition())方法,而對於IoC容器中最重要的一個類DefaultListableBeanFactory實現了該接口的方法。這個方法的主要目的就是將BeanDefinition存放至DefaultListableBeanFactory對象的beanDefinitionMap中,當初始化容器進行bean初始化時,在bean的生命周期分析里必然會在這個beanDefinitionMap中獲取beanDefition實例。我們可以在DefaultListableBeanFactory中看到此Map的定義。
/** Map of bean definition objects, keyed by bean name. */ private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
下面我們在來看一下這個方法是如將BeanDefinition存放至beanDefinitionMap中的,DefaultListableBeanFactory中實現的registerBeanDefinition( beanName, bdHolder.getBeanDefinition() )方法具體如下:
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException { Assert.hasText(beanName, "Bean name must not be empty"); Assert.notNull(beanDefinition, "BeanDefinition must not be null"); if (beanDefinition instanceof AbstractBeanDefinition) { try { ((AbstractBeanDefinition) beanDefinition).validate(); } catch (BeanDefinitionValidationException ex) { throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, "Validation of bean definition failed", ex); } } //此處檢查是不是有相同名字的Bean存在 //如果名字相同又不允許覆蓋,就會拋出異常BeanDefinitionOverrideException BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName); if (existingDefinition != null) { if (!isAllowBeanDefinitionOverriding()) { throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition); } else if (existingDefinition.getRole() < beanDefinition.getRole()) { // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE if (logger.isInfoEnabled()) { logger.info("Overriding user-defined bean definition for bean '" + beanName + "' with a framework-generated bean definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]"); } } else if (!beanDefinition.equals(existingDefinition)) { if (logger.isDebugEnabled()) { logger.debug("Overriding bean definition for bean '" + beanName + "' with a different definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]"); } } else { if (logger.isTraceEnabled()) { logger.trace("Overriding bean definition for bean '" + beanName + "' with an equivalent definition: replacing [" + existingDefinition + "] with [" + beanDefinition + "]"); } } //存儲Bean(Bean名字作為key,BeanDefinition作為value) this.beanDefinitionMap.put(beanName, beanDefinition); } else { if (hasBeanCreationStarted()) { //注冊的過程需要保證數據的一致性 synchronized (this.beanDefinitionMap) { //將獲取到的BeanDefinition放入Map中,容器操作使用bean時通過這個HashMap找到具體的BeanDefinition //存儲Bean(Bean名字作為key,BeanDefinition作為value) this.beanDefinitionMap.put(beanName, beanDefinition); List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); updatedDefinitions.addAll(this.beanDefinitionNames); updatedDefinitions.add(beanName); this.beanDefinitionNames = updatedDefinitions; removeManualSingletonName(beanName); } } else { // Still in startup registration phase this.beanDefinitionMap.put(beanName, beanDefinition); this.beanDefinitionNames.add(beanName); removeManualSingletonName(beanName); } this.frozenBeanDefinitionNames = null; } if (existingDefinition != null || containsSingleton(beanName)) { resetBeanDefinition(beanName); } else if (isConfigurationFrozen()) { clearByTypeCache(); } }
當把所有的BeanDefinition(懶加載除外)都存入IOC容器中的HashMap后,注冊就結束了。但是注意,以上僅僅是BeanDefinition的載入、載入和注冊,Bean之間的依賴關系並不會在初始化的時候完成!后面還需要調用一系列方法才會完成初始化。
這篇文章算是我自己比較深入了解Spring了吧,我也沒怎么看過Spring的源碼,所以參考了很多網上博客才寫出來,之所以還是要寫下這篇博客,是因為想要更加深入的了解一下Spring,當然這只是它的一點皮毛。我也希望在后面的學習中不斷提高自己的技術,同時記錄自己學習過程中的點點滴滴,所以博客中肯定有許多不足之處,望諒解與指出,歡迎大家評論指出。
參考資料: