tiny-spring分析

lingshenghang發表於2018-10-19

轉自:www.zybuluo.com/dugu9sword/…

程式碼地址 :github.com/lingo0/tiny…

這個是我fork的程式碼,我在其中加入了一些分析,同時我準備在這個基礎上實現利用註解來注入。

下面是原文。

前言

在閱讀 Spring 的原始碼(依賴注入部分和麵向切面程式設計部分)時遇到不少困惑,龐大的類檔案結構、紛繁複雜的方法呼叫、波詭雲譎的多型實現,讓自己深陷其中、一頭霧水。 後來注意到 code4crafttiny-spring 專案,實現了一個微型的 Spring,提供對 IoC 和 AOP 的最基本支援,麻雀雖小,五臟俱全,對 Spring 的認知清晰了不少。這個微型框架的結構包括檔名、方法名都是參照 Spring 來實現的,對於初讀 Spring 的學習者,作為研究 Spring 的輔助工具應該能夠受益匪淺。 在研究 tiny-spring 的時候,收穫頗多,把對這個微型框架的一些分析寫了下來,行文可能有點紊亂。

本文結構

  1. 第一部分 IoC 容器的實現 對應了 tiny-springstep-1step-5 部分,這 5 個 step 實現了基本的 IoC 容器,支援singleton型別的bean,包括初始化、屬性注入、以及依賴 Bean 注入,可從 XML 中讀取配置,XML 讀取方式沒有具體深入。
  2. 第二部分 AOP 容器的實現 對應了 tiny-springstep-6step-9 部分。step-10 中對 cglib 的支援沒有分析。這 4 個 step 可以使用 AspectJ 的語法進行 AOP 編寫,支援介面代理。考慮到 AspectJ 語法僅用於實現 execution("***") 部分的解析,不是主要內容,也可以使用 Java 的正規表示式粗略地完成,因此沒有關注這些細節。

參考書目 《Spring 實戰》《Spring 技術內幕》

IoC 容器的實現

檔案結構

Resource

Resource 介面為核心發散出的幾個類,都是用於解決 IoC 容器中的內容從哪裡來的問題,也就是 配置檔案從哪裡讀取、配置檔案如何讀取 的問題。

類名 說明
Resource 介面,標識一個外部資源。通過 getInputStream() 方法 獲取資源的輸入流
UrlResource 實現 Resource 介面的資源類,通過 URL 獲取資源。
ResourceLoader 資源載入類。通過 getResource(String) 方法獲取一個 Resouce 物件,是 獲取 Resouce 的主要途徑

注: 這裡在設計上有一定的問題,ResourceLoader 直接返回了一個 UrlResource,更好的方法是宣告一個 ResourceLoader 介面,再實現一個 UrlResourceLoader 類用於載入 UrlResource

BeanDefinition

BeanDefinition 類為核心發散出的幾個類,都是用於解決 Bean 的具體定義問題,包括 Bean 的名字是什麼、它的型別是什麼,它的屬性賦予了哪些值或者引用,也就是 如何在 IoC 容器中定義一個 Bean,使得 IoC 容器可以根據這個定義來生成例項 的問題。

類名 說明
BeanDefinition 該類儲存了 Bean 定義。包括 Bean名字 String beanClassName型別Class beanClass屬性 PropertyValues propertyValues。根據其 型別 可以生成一個類例項,然後可以把 屬性 注入進去。propertyValues 裡面包含了一個個 PropertyValue 條目,每個條目都是鍵值對 String - Object,分別對應要生成例項的屬性的名字與型別。在 Spring 的 XML 中的 property 中,鍵是 key ,值是 value 或者 ref。對於 value 只要直接注入屬性就行了,但是 ref 要先進行解析。Object 如果是 BeanReference 型別,則說明其是一個引用,其中儲存了引用的名字,需要用先進行解析,轉化為對應的實際 Object
BeanDefinitionReader 解析 BeanDefinition 的介面。通過 loadBeanDefinitions(String) 來從一個地址載入類定義。
AbstractBeanDefinitionReader 實現 BeanDefinitionReader 介面的抽象類(未具體實現 loadBeanDefinitions,而是規範了 BeanDefinitionReader 的基本結構)。內建一個 HashMap rigistry,用於儲存 String - beanDefinition 的鍵值對。內建一個 ResourceLoader resourceLoader,用於儲存類載入器。用意在於,使用時,只需要向其 loadBeanDefinitions() 傳入一個資源地址,就可以自動呼叫其類載入器,並把解析到的 BeanDefinition 儲存到 registry 中去。
XmlBeanDefinitionReader 具體實現了 loadBeanDefinitions() 方法,從 XML 檔案中讀取類定義。

BeanFactory

BeanFactory 介面為核心發散出的幾個類,都是用於解決 IoC 容器在 已經獲取 Bean 的定義的情況下,如何裝配、獲取 Bean 例項 的問題。

類名 說明
BeanFactory 介面,標識一個 IoC 容器。通過 getBean(String) 方法來 獲取一個物件
AbstractBeanFactory BeanFactory 的一種抽象類實現,規範了 IoC 容器的基本結構,但是把生成 Bean 的具體實現方式留給子類實現。IoC 容器的結構:AbstractBeanFactory 維護一個 beanDefinitionMap 雜湊表用於儲存類的定義資訊(BeanDefinition)。獲取 Bean 時,如果 Bean 已經存在於容器中,則返回之,否則則呼叫 doCreateBean 方法裝配一個 Bean。(所謂存在於容器中,是指容器可以通過 beanDefinitionMap 獲取 BeanDefinition 進而通過其 getBean() 方法獲取 Bean。)
AutowireCapableBeanFactory 可以實現自動裝配的 BeanFactory。在這個工廠中,實現了 doCreateBean 方法,該方法分三步:1,通過 BeanDefinition 中儲存的類資訊例項化一個物件;2,把物件儲存在 BeanDefinition 中,以備下次獲取;3,為其裝配屬性。裝配屬性時,通過 BeanDefinition 中維護的 PropertyValues 集合類,把 String - Value 鍵值對注入到 Bean 的屬性中去。如果 Value 的型別是 BeanReference則說明其是一個引用(對應於 XML 中的 ref),通過 getBean 對其進行獲取,然後注入到屬性中。

ApplicationContext

ApplicationContext 介面為核心發散出的幾個類,主要是對前面 ResouceBeanFactoryBeanDefinition進行了功能的封裝,解決 根據地址獲取 IoC 容器並使用 的問題。

類名 說明
ApplicationContext 標記介面,繼承了 BeanFactory。通常,要實現一個 IoC 容器時,需要先通過 ResourceLoader 獲取一個 Resource,其中包括了容器的配置、Bean 的定義資訊。接著,使用 BeanDefinitionReader 讀取該 Resource 中的 BeanDefinition 資訊。最後,把 BeanDefinition 儲存在 BeanFactory 中,容器配置完畢可以使用。注意到 BeanFactory 只實現了 Bean裝配、獲取,並未說明 Bean來源 也就是 BeanDefinition 是如何 載入 的。該介面把 BeanFactoryBeanDefinitionReader 結合在了一起。
AbstractApplicationContext ApplicationContext 的抽象實現,內部包含一個 BeanFactory 類。主要方法有 getBean()refresh() 方法。getBean() 直接呼叫了內建 BeanFactorygetBean() 方法,refresh() 則用於實現 BeanFactory 的重新整理,也就是告訴 BeanFactory 該使用哪個資源(Resource)載入類定義(BeanDefinition)資訊,該方法留給子類實現,用以實現 從不同來源的不同型別的資源載入類定義 的效果。
ClassPathXmlApplicationContext 從類路徑載入資源的具體實現類。內部通過 XmlBeanDefinitionReader 解析 UrlResourceLoader 讀取到的 Resource,獲取 BeanDefinition 資訊,然後將其儲存到內建的 BeanFactory 中。

*注 1:*在 Spring 的實現中,對 ApplicatinoContext 的分層更為細緻。AbstractApplicationContext 中為了實現 不同來源不同型別 的資源載入類定義,把這兩步分層實現。以“從類路徑讀取 XML 定義”為例,首先使用 AbstractXmlApplicationContext 來實現 不同型別 的資源解析,接著,通過 ClassPathXmlApplicationContext 來實現 不同來源 的資源解析。 *注 2:*在 tiny-spring 的實現中,先用 BeanDefinitionReader 讀取 BeanDefiniton 後,儲存在內建的 registry(鍵值對為 String - BeanDefinition 的雜湊表,通過 getRigistry() 獲取)中,然後由 ApplicationContextBeanDefinitionReaderregistry 的鍵值對一個個賦值給 BeanFactory 中儲存的 beanDefinitionMap。而在 Spring 的實現中,BeanDefinitionReader 直接操作 BeanDefinition ,它的 getRegistry() 獲取的不是內建的 registry,而是 BeanFactory 的例項。如何實現呢?以 DefaultListableBeanFactory 為例,它實現了一個 BeanDefinitonRigistry 介面,該介面把 BeanDefinition註冊獲取 等方法都暴露了出來,這樣,BeanDefinitionReader 可以直接通過這些方法把 BeanDefiniton 直接載入到 BeanFactory 中去。

設計模式

注:此處的設計模式分析不限於 tiny-spring,也包括 Spring 本身的內容

模板方法模式

該模式大量使用,例如在 BeanFactory 中,把 getBean() 交給子類實現,不同的子類 **BeanFactory 對其可以採取不同的實現。

代理模式

在 tiny-spring 中(Spring 中也有類似但不完全相同的實現方式),ApplicationContext 繼承了 BeanFactory 介面,具備了 getBean() 功能,但是又內建了一個 BeanFactory 例項,getBean() 直接呼叫 BeanFactorygetBean() 。但是ApplicationContext 加強了 BeanFactory,它把類定義的載入也包含進去了。

AOP 的實現

重新分析 IoC 容器

注:以下所說的 BeanFactory 和 ApplicationContext 不是指的那幾個最基本的介面類(例如 BeanFactory 介面,它除了 getBean 空方法之外,什麼都沒有,無法用來分析。),而是指這一類物件總體的表現,比如 ClasspathXmlApplicationContext、FileSystemXmlApplicationContext 都算是 ApplicationContext。

BeanFactory 的構造與執行

BeanFactory 的核心方法是 getBean(String) 方法,用於從工廠中取出所需要的 BeanAbstractBeanFactory 規定了基本的構造和執行流程。

getBean 的流程:包括例項化和初始化,也就是生成 Bean,再執行一些初始化操作。

  1. doCreateBean :例項化 Bean。 a. createInstance :生成一個新的例項。 b. applyProperties :注入屬性,包括依賴注入的過程。在依賴注入的過程中,如果 Bean 實現了 BeanFactoryAware 介面,則將容器的引用傳入到 Bean 中去,這樣,Bean 將獲取對容器操作的許可權,也就允許了 編寫擴充套件 IoC 容器的功能的 Bean
  2. initializeBean(bean) : 初始化 Bean。 a. 從 BeanPostProcessor 列表中,依次取出 BeanPostProcessor 執行 bean = postProcessBeforeInitialization(bean,beanName) 。(為什麼呼叫 BeanPostProceesor 中提供方法時,不是直接 post...(bean,beanName) 而是 bean = post...(bean,beanName) 呢?見分析1 。另外,BeanPostProcessor 列表的獲取有問題,見分析2。) b. 初始化方法(tiny-spring 未實現對初始化方法的支援)。 c. 從 BeanPostProcessor 列表中, 依次取出 BeanPostProcessor 執行其 bean = postProcessAfterInitialization(bean,beanName)

ApplicationContext 的構造和執行

ApplicationContext 的核心方法是 refresh() 方法,用於從資原始檔載入類定義、擴充套件容器的功能。

refresh 的流程:

  1. loadBeanDefinitions(BeanFactory) :載入類定義,並注入到內建的 BeanFactory 中,這裡的可擴充套件性在於,未對載入方法進行要求,也就是可以從不同來源的不同型別的資源進行載入
  2. registerBeanPostProcessors(BeanFactory) :獲取所有的 BeanPostProcessor,並註冊到 BeanFactory 維護的 BeanPostProcessor 列表去。
  3. onRefresh : a. preInstantiateSingletons :以單例的方式,初始化所有 Bean。tiny-spring 只支援 singleton 模式。

IoC 實現的一些思考與分析

分析 1:AOP 可以在何處被嵌入到 IoC 容器中去?

Bean 的初始化過程中,會呼叫 BeanPostProcessor 對其進行一些處理。在它的 postProcess...Initialization方法中返回了一個 Bean,這個返回的 Bean 可能已經不是原來傳入的 Bean 了,這為實現 AOP 的代理提供了可能!以 JDK 提供的動態代理為例,假設方法要求傳入的物件實現了 IObj 介面,實際傳入的物件是 Obj,那麼在方法中,通過動態代理,可以 生成一個實現了 IObj 介面並把 Obj 作為內建物件的代理類 Proxy 返回,此時 Bean 已經被偷偷換成了它的代理類。

分析 2:BeanFactory 和 ApplicationContext 設計上的耦合

BeanFactory 中的 BeanPostProcessor 的列表是哪裡生成的呢?是在 ApplicationContext 中的 refresh 方法的第二步,這裡設計上應該有些問題,按理說 ApplicationContext 是基於 BeanFactory 的,BeanFactory 的屬性的獲取,怎麼能依賴於 ApplicationContext 的呼叫呢?

分析 3:tiny-spring 總體流程的分析

總體來說,tiny-spring 的 ApplicaitonContext 使用流程是這樣的:

  1. ApplicationContext 完成了類定義的讀取和載入,並註冊到 BeanFactory 中去。
  2. ApplicationContextBeanFactory 中尋找 BeanPostProcessor,註冊到 BeanFactory 維護的 BeanPostProcessor 列表中去。
  3. ApplicationContext 以單例的模式,通過主動呼叫 getBean 例項化、注入屬性、然後初始化 BeanFactory 中所有的 Bean。由於所有的 BeanPostProcessor 都已經在第 2 步中完成例項化了,因此接下來例項化的是普通 Bean,因此普通 Bean 的初始化過程可以正常執行。 \4. 呼叫 getBean 時,委託給 BeanFactory,此時只是簡單的返回每個 Bean 單例,因為所有的 Bean 例項在第三步都已經生成了。

JDK 對動態代理的支援

JDK 中幾個關鍵的類:

類名 說明
Proxy 來自 JDK API。提供生成物件的動態代理的功能,通過Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法返回一個代理物件。
InvocationHandler 來自 JDK API。通過 Object invoke(Object proxy, Method method,Object[] args) 方法實現代理物件中方法的呼叫和其他處理。

假設以下的情況:

  • 物件 obj 實現了 IObj 介面,介面中有一個方法 func(Object[] args)
  • 物件 handlerInvocationHandler 的例項。

那麼,通過 ProxynewProxyInstance(obj.getClassLoader(), obj.getClass().getInterfaces(), handler,可以返回 obj 的代理物件 proxy

當呼叫 proxy.func(args) 時,物件內部將委託給 handler.invoke(proxy, func, args) 函式實現。

因此,在 handler 的 invoke 中,可以完成對方法攔截的處理。可以先判斷是不是要攔截的方法,如果是,進行攔截(比如先做一些操作,再呼叫原來的方法,對應了 Spring 中的前置通知);如果不是,則直接呼叫原來的方法。

AOP 的植入與實現細節

Bean 初始化過程中完成 AOP 的植入

解決 AOP 的植入問題,首先要解決 在 IoC 容器的何處植入 AOP 的問題,其次要解決 為哪些物件提供 AOP 的植入 的問題。 tiny-spring 中 AspectJAwareAdvisorAutoProxyCreator 類(以下簡稱 AutoProxyCreator)是實現 AOP 植入的關鍵類,它實現了兩個介面:

  1. BeanPostProcessor :在 postProcessorAfterInitialization 方法中,使用動態代理的方式,返回一個物件的代理物件。解決了 在 IoC 容器的何處植入 AOP 的問題。
  2. BeanFactoryAware :這個介面提供了對 BeanFactory 的感知,這樣,儘管它是容器中的一個 Bean,卻可以獲取容器的引用,進而獲取容器中所有的切點物件,決定對哪些物件的哪些方法進行代理。解決了 為哪些物件提供 AOP 的植入 的問題。

AOP 中動態代理的實現步驟

動態代理的內容

首先,要知道動態代理的內容(攔截哪個物件、在哪個方法攔截、攔截具體內容),下面是幾個關鍵的類:

類名 說明
PointcutAdvisor 切點通知器,用於提供 對哪個物件的哪個方法進行什麼樣的攔截 的具體內容。通過它可以獲取一個切點物件 Pointcut 和一個通知器物件 Advisor
Pointcut 切點物件可以獲取一個 ClassFilter 物件和一個 MethodMatcher 物件。前者用於判斷是否對某個物件進行攔截(用於 篩選要代理的目標物件),後者用於判斷是否對某個方法進行攔截(用於 在代理物件中對不同的方法進行不同的操作)。
Advisor 通知器物件可以獲取一個通知物件 Advice 。就是用於實現 具體的方法攔截,需要使用者編寫,也就對應了 Spring 中的前置通知、後置通知、環切通知等。
動態代理的步驟

接著要知道動態代理的步驟:

  1. AutoProxyCreator(實現了 BeanPostProcessor 介面)在例項化所有的 Bean 前,最先被例項化。
  2. 其他普通 Bean 被例項化、初始化,在初始化的過程中,AutoProxyCreator 載入 BeanFactory 中所有的 PointcutAdvisor(這也保證了 PointcutAdvisor 的例項化順序優於普通 Bean。),然後依次使用 PointcutAdvisor 內建的 ClassFilter,判斷當前物件是不是要攔截的類。
  3. 如果是,則生成一個 TargetSource(要攔截的物件和其型別),並取出 AutoProxyCreatorMethodMatcher(對哪些方法進行攔截)、Advice(攔截的具體操作),再,交給 AopProxy 去生成代理物件。
  4. AopProxy 生成一個 InvocationHandler,在它的 invoke 函式中,首先使用 MethodMatcher 判斷是不是要攔截的方法,如果是則交給 Advice 來執行(Advice 由使用者來編寫,其中也要手動/自動呼叫原始物件的方法),如果不是,則直接交給 TargetSource 的原始物件來執行。

設計模式

代理模式

通過動態代理實現,見分析1中的內容,不再贅述。

策略模式

生成代理物件時,可以使用 JDK 的動態代理和 Cglib 的動態代理,對於不同的需求可以委託給不同的類實現。

為 tiny-spring 新增攔截器鏈

tiny-spring 不支援攔截器鏈,可以模仿 Spring 中攔截器鏈的實現,實現對多攔截器的支援。 tiny-spring 中的 proceed() 方法是呼叫原始物件的方法 method.invoke(object,args)。(參見 ReflectiveMethodInvocation 類) 為了支援多攔截器,做出以下修改:

  • proceed() 方法修改為呼叫代理物件的方法 method.invoke(proxy,args)
  • 在代理物件的 InvocationHandlerinvoke 函式中,檢視攔截器列表,如果有攔截器,則呼叫第一個攔截器並返回,否則呼叫原始物件的方法。

相關文章