天天看點

Spring的輕量級實作

作者: Grey

原文位址:Spring的輕量級實作

本文是參考公衆号:碼農翻身 的從零開始造Spring 教程的學習筆記

github

使用TDD的開發方法,TDD的開發流程是:

寫一個測試用例

運作:失敗

寫Just enough的代碼,讓測試通過

重構代碼保持測試通過,

然後循環往複。

僅實作核心功能

基于spring-framework-3.2.18.RELEASE版本

解析XML檔案,拿到Bean的id和完整路徑,通過反射方式執行個體化一個對象。

XML格式如下,檔案名為:<code>bean-v1.xml</code>

需要解析上述XML并生成<code>userService</code>對象,調用者隻需要做如下調用即可:

思路為:

解析XML,并把XML中的類通過反射方式生成對象,最後,把這個生成的對象放到一個Map中,其中Map的key為<code>beanId</code>,如上例就是:<code>userService</code>, Map的Value是UserService的全路徑<code>org.spring.service.v1.UserService</code>

實作細節參考代碼見:step1

增加日志支援:log4j2 + SLF4j

增加異常處理,所有異常的父類設計為<code>BeansException</code>

封裝BeanDefinition

由于DefaultBeanFactory中的BEAN_MAP目前隻包括了beanClassName資訊,後續如果要擴充其他的資訊,肯定需要增加字段,是以我們需要抽象出一個接口BeanDefinition,友善後續擴充其他的字段。

封裝Resource

在BeanFactory初始化的時候,傳入的是XML格式的配置資訊,比如bean-v1.xml, Spring會把這個抽象成一個Resource,常見Resource有 FileSystemResource: 從檔案位址讀配置 ClassPathResource: 從classpath下讀配置 BeanFactory在建立Bean的時候,隻關注Resource即可。

實作細節參考代碼見:vstep4-2-resource

設計<code>XmlBeanDefinitionReader</code>,用于解析XML,傳入<code>Resource</code>,即可擷取所有<code>BeanDefinition</code>,

由于要把<code>BeanDefinition</code>放入<code>BEAN_MAP</code>中,是以<code>XmlBeanDefinitionReader</code>需要持有一個<code>DefaultBeanFactory</code>,且<code>DefaultBeanFactory</code>需要有注冊<code>BeanDefinition</code>和擷取<code>BeanDefintion</code>的能力,這樣<code>DefaultBeanFactory</code>的職責就不單一了,是以需要抽象出一個<code>BeanDefinitionRegistry</code>,這個<code>BeanDefinitionRegistry</code>專門負責注冊<code>BeanDefinition</code>和擷取<code>BeanDefintion</code>,

<code>XmlBeanDefinitionReader</code>隻需要持有<code>BeanDefinitionRegistry</code>,即可将解析生成的<code>BeanDefinition</code>注入<code>BEAN_MAP</code>中。

實作細節參考代碼見:vstep5-final

XML檔案中會增加一個屬性,如下:

其中<code>orgService</code>這個bean配置成了<code>prototype</code>的屬性,是以在<code>BeanDefinition</code>這個資料結構要增加是否單例,是否多例的邏輯

在<code>DefaultBeanFactory</code>調用<code>getBean</code>的時候,判斷是否單例,如果是單例,則複用對象,如果是多例,則new新的對象。

抽象<code>SingletonBeanRegistry</code>這個接口,專門用于注冊和擷取單例對象,

<code>DefaultSingletonBeanRegistry</code>實作這個接口,實作對單例對象的注冊

<code>DefaultBeanFactory</code>繼承<code>DefaultSingletonBeanRegistry</code>這個類,就有了擷取單例Bean和注冊單例Bean的能力。

實作細節參考代碼見:vstep6-scope

我們使用Spring的時候,一般是這樣做的:

現在,我們需要抽象出<code>ApplicationContext</code>這個接口來實作如上的功能,其中有如下兩個類去實作這個接口。

ClassPathXmlApplicationContext

從classpath中讀取配置檔案

FileSystemApplicationContext

從檔案中讀取配置檔案

這兩個子類都需要持有<code>DefaultBeanFactory</code>才能有<code>getBean</code>的能力,

<code>ClassPathXmlApplicationContext</code>代碼如下:

<code>FileSystemApplicationContext</code>代碼如下:

實作細節參考代碼見:vstep7-applicationcontext-v1

通過觀察發現,<code>ClassPathXmlApplicationContext</code>和<code>FileSystemApplicationContext</code>大部分代碼都是相同的,隻有在擷取<code>Resource</code>的時候,方法不一樣,是以,我們通過模闆方法這個設計模式,設計一個抽象類<code>AbstractApplicationContext</code>,代碼如下:

這個抽象類實作除了擷取<code>Resource</code>以外的所有邏輯,<code>ClassPathXmlApplicationContext</code>和<code>FileSystemApplicationContext</code>都繼承這個抽象類,完成<code>Resource</code>的擷取邏輯的編寫即可。以<code>FileSystemApplicationContext</code>為例,示例代碼如下:

實作細節參考代碼見:vstep7-applicationcontext-v2

我們需要對于如下類型的XML配置檔案進行解析:

需要達到的目的就是:可以把整型,字元串類型,簡單對象類型注入到一個Bean中,我們需要解決如下兩個問題:

第一個問題是:把字元串轉成各種各樣的Value,比如把<code>String</code>轉換成<code>Integer</code>或者轉換成<code>Boolean</code>。jdk中<code>java.bean</code>包中的<code>PropertyEditorSupport</code>這個類來完成的,我們建立了<code>CustomBooleanEditor</code>和<code>CustomNumberEditor</code>兩個類,這兩個類都繼承于<code>PropertyEditorSupport</code>,分别實作了<code>String</code>類型轉換成<code>Boolean</code>類型和<code>String</code>類型轉換成<code>Integer</code>類型的功能。其他的類型轉換也可以通過類似的方法來實作。然後抽象出了<code>TypeConvert</code>這個接口,并把這些轉換器加入一個特定的<code>Map</code>中,<code>Map</code>的<code>key</code>就是要轉換的目标的類型,<code>Value</code>就是對應的轉換器的實作類,即可實作類型轉換。

第二個問題是:我們調用Bean的setXXX方法把這些Value值set到目标Bean中,做法是抽象出<code>PropertyValue</code>

<code>BeanDefiniton</code>需要增加方法擷取<code>PropertyValue</code>的邏輯,<code>BeanDefiniton</code>的所有子類,例如:<code>GenericBeanDefinition</code>中需要增加

在解析XML檔案的時候,就需要把<code>List&lt;PropertyValue&gt;</code>識别出來并加入<code>BeanDefinition</code>中(RuntimeBeanReference,TypedStringValue),使用<code>BeanDefinitionValueResolver</code>把對應的<code>PropertyValue</code>給初始化好,如下代碼:

而setXXX的背後實作利用的是jdk原生<code>java.beans.Introspector</code>來實作,見<code>DefaultBeanFactory</code>的<code>populateBean</code>方法

其中

就是對bean的屬性進行指派操作(即:setXXX方法)

實作細節參考代碼見:vstep8-inject

處理形如以下的配置:

和上例中注入Bean和字元串常量一樣,我們抽象出<code>ConstructorArgument</code>用于表示一個構造函數資訊,每個<code>BeanDefinition</code>中持有這個對象,

在解析XML的時候,<code>XmlBeanDefinitionReader</code>需要負責解析出<code>ConstuctorArgument</code>,<code>DefaultBeanFactory</code>通過指定構造函數來生成Bean對象并通過<code>ConstructorResolver</code>注入Bean執行個體到構造方法中。

注:這裡指定的構造函數的查找邏輯為:解析出XML的構造函數的參數清單,和通過反射拿到對應的構造函數的參數清單進行對比(每個參數的類型和個數必須一樣)

實作細節參考代碼見:vstep9-constructor

實作兩個注解:@Component @Autowired(隻針對屬性注入,暫時不考慮方法注入)

且需要實作如下的XML的解析,即實作某個包下的Bean掃描。

我們首先需要定義注解Component ,Autowired,代碼如下:

其次,我們需要實作一個功能,即:給一個包名,掃描擷取到這個包以及子包下面的所有Class,示例代碼如下:

主要思路是将包名轉換成檔案路徑,然後遞歸擷取路徑下的Class檔案。

由于注解的Bean不像之前的xml定義的Bean那樣,會對Bean配置一個id,是以,這裡解析出來的Bean定義需要自動生成一個<code>BeanId</code>(預設先取注解中的<code>value</code>的配置,否則就就是類名第一個字母小寫,抽象<code>BeanNameGenerator</code>來專門對Bean定義ID),同時,Spring中單獨建立了一個<code>AnnotatedBeanDefinition</code>接口來定義包含注解的<code>BeanDefinition</code>。

我們得到了對應的Class檔案,我們需要通過某種方式去解析這個Class檔案,拿到這個Class中的所有資訊,特别是注解資訊。可以使用<code>ASM</code>這個來解析Class的資訊,用<code>ASM</code>的原生方式解析不太友善,解析<code>ClassMetaData</code>和<code>Annotation</code>都需要定義一個Visitor,是以Spring抽象了一個接口<code>MetadataReader</code>來封裝ASM的實作

然後,我們需要拿到Bean中的所有Field(帶注解的),并把他執行個體化成一個對象,并将這個對象注入目标Bean中,示例代碼如下:

針對于XML的解析,建立了一個<code>ScannedGenericBeanDefinition</code>來處理掃描包下的所有Bean定義。

使用<code>AutowiredAnnotationProcessor</code>來将上述流程整合起來,同時涉及Bean生命周期的鈎子函數設計, 相關示例代碼如下:

關于Bean的生命周期和Bean生命周期中各個鈎子函數,參考如下圖

Spring的輕量級實作
Spring的輕量級實作

實作細節參考代碼見:vstep10-annotation-final

即要實作如下XML格式的解析

首先,我們需要實作如下功能,即,給定一個表達式,然後判斷某個類的某個方法是否比對這個表達式,這需要依賴AspectJ這個元件來實作,具體使用參考<code>AspectJExpressionPointcut</code>和<code>PointcutTest</code>這兩個類。

其次,我們需要通過Bean的名稱("tx")和方法名("start")定位到這個Method,然後反射調用這個Method,具體可參考<code>MethodLocatingFactoryTest</code>

然後,我們需要使用AOP Alliance實作指定順序的鍊式調用,即根據配置的不同<code>advice</code>順序調用。

Spring的輕量級實作

具體可檢視<code>ReflectiveMethodInvocation</code>和<code>ReflectiveMethodInvocationTest</code>這兩個類。

就是驗證我們配置的advice是否按指定順序運作。

最後,我們需要實作動态代理,在一個方法前後增加一些邏輯,而不用改動原始代碼。如果是普通類就使用CGLib實作,如果有接口的類可以使用JDK自帶的動态代理,具體可參考<code>CGlibTest</code>和<code>CglibAopProxyTest</code>

實作細節參考代碼見:vaop-v3

lite-spring

從零開始造Spring