天天看點

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?

作者:小傅哥

部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收獲!😄

👨‍💻連讀同僚寫的代碼都費勁,還讀Spring?

咋的,Spring 很難讀!

這個與我們碼農朝夕相處的 Spring,就像睡在你身邊的媳婦,你知道找她要吃、要喝、要零花錢、要買皮膚。但你不知道她的倉庫共有多少存糧、也不知道她是買了理财還是存了銀行。🍑開個玩笑,接下來我要正經了!

一、為什麼Spring難讀懂?

為什麼 Spring 天天用,但要想去讀一讀源碼,怎麼就那麼難!因為由

Java和J2EE開發領域的專家

Rod Johnson 于 2002 年提出并随後建立的 Spring 架構,随着 JDK 版本和市場需要發展至今,至今它已經越來越大了!

當你閱讀它的源碼你會感覺:

  1. 怎麼這代碼跳來跳去的,根本不是像自己寫代碼一樣那麼

    單純

  2. 為什麼那麼多的接口和接口繼承,類A繼承的類B還實作了類A實作的接口X
  3. 簡單工廠、工廠方法、代理模式、觀察者模式,怎麼用了會有這樣多的設計模式使用
  4. 又是資源加載、又是應用上下文、又是IOC、又是AOP、貫穿的還有 Bean 的聲明周期,一片一片的代碼從哪下手

怎樣,這就是你在閱讀 Spring 遇到的一些列問題吧?其實不止你甚至可以說隻要是從事這個行業的碼農,想讀 Spring 源碼都會有種不知道從哪下手的感覺。是以我想了個辦法,既然 Spring 太大不好了解,那麼我就嘗試從一個小的 Spring 開始,手撸 實作一個 Spring 是不可以了解的更好,别說效果還真不錯,

在花了将近2個月的時間,實作一個簡單版本的 Spring 後

現在對 Spring 的了解,有了很大的提升,也能讀懂 Spring 的源碼了。

二、分享手撸 Spring

通過這樣手寫簡化版 Spring 架構,了解 Spring 核心原理。在手寫的過程中會簡化 Spring 源碼,摘取整體架構中的核心邏輯,簡化代碼實作過程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、資源處理等内容實作。

源碼:https://github.com/fuzhengwei/small-spring

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?

1. 實作一個簡單的Bean容器

凡是可以存放資料的具體資料結構實作,都可以稱之為容器。例如:ArrayList、LinkedList、HashSet等,但在 Spring Bean 容器的場景下,我們需要一種可以用于存放和名稱索引式的資料結構,是以選擇 HashMap 是最合适不過的。

這裡簡單介紹一下 HashMap,HashMap 是一種基于擾動函數、負載因子、紅黑樹轉換等技術内容,形成的拉鍊尋址的資料結構,它能讓資料更加散列的分布在哈希桶以及碰撞時形成的連結清單和紅黑樹上。它的資料結構會盡可能最大限度的讓整個資料讀取的複雜度在 O(1) ~ O(Logn) ~O(n)之間,當然在極端情況下也會有 O(n) 連結清單查找資料較多的情況。不過我們經過10萬資料的擾動函數再尋址驗證測試,資料會均勻的散列在各個哈希桶索引上,是以 HashMap 非常适合用在 Spring Bean 的容器實作上。

另外一個簡單的 Spring Bean 容器實作,還需 Bean 的定義、注冊、擷取三個基本步驟,簡化設計如下;

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 定義:BeanDefinition,可能這是你在查閱 Spring 源碼時經常看到的一個類,例如它會包括 singleton、prototype、BeanClassName 等。但目前我們初步實作會更加簡單的處理,隻定義一個 Object 類型用于存放對象。
  • 注冊:這個過程就相當于我們把資料存放到 HashMap 中,隻不過現在 HashMap 存放的是定義了的 Bean 的對象資訊。
  • 擷取:最後就是擷取對象,Bean 的名字就是key,Spring 容器初始化好 Bean 以後,就可以直接擷取了。

2. 運用設計模式,實作 Bean 的定義、注冊、擷取

将 Spring Bean 容器完善起來,首先非常重要的一點是在 Bean 注冊的時候隻注冊一個類資訊,而不會直接把執行個體化資訊注冊到 Spring 容器中。那麼就需要修改 BeanDefinition 中的屬性 Object 為 Class,接下來在需要做的就是在擷取 Bean 對象時需要處理 Bean 對象的執行個體化操作以及判斷目前單例對象在容器中是否已經緩存起來了。整體設計如圖 3-1

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 首先我們需要定義 BeanFactory 這樣一個 Bean 工廠,提供 Bean 的擷取方法

    getBean(String name)

    ,之後這個 Bean 工廠接口由抽象類 AbstractBeanFactory 實作。這樣使用模闆模式的設計方式,可以統一收口通用核心方法的調用邏輯和标準定義,也就很好的控制了後續的實作者不用關心調用邏輯,按照統一方式執行。那麼類的繼承者隻需要關心具體方法的邏輯實作即可。
  • 那麼在繼承抽象類 AbstractBeanFactory 後的 AbstractAutowireCapableBeanFactory 就可以實作相應的抽象方法了,因為 AbstractAutowireCapableBeanFactory 本身也是一個抽象類,是以它隻會實作屬于自己的抽象方法,其他抽象方法由繼承 AbstractAutowireCapableBeanFactory 的類實作。這裡就展現了類實作過程中的各司其職,你隻需要關心屬于你的内容,不是你的内容,不要參與。
  • 另外這裡還有塊非常重要的知識點,就是關于單例 SingletonBeanRegistry 的接口定義實作,而 DefaultSingletonBeanRegistry 對接口實作後,會被抽象類 AbstractBeanFactory 繼承。現在 AbstractBeanFactory 就是一個非常完整且強大的抽象類了,也能非常好的展現出它對模闆模式的抽象定義。

3. 基于Cglib實作含構造函數的類執行個體化政策

填平這個坑的技術設計主要考慮兩部分,一個是串流程從哪合理的把構造函數的入參資訊傳遞到執行個體化操作裡,另外一個是怎麼去執行個體化含有構造函數的對象。

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 參考 Spring Bean 容器源碼的實作方式,在 BeanFactory 中添加

    Object getBean(String name, Object... args)

    接口,這樣就可以在擷取 Bean 時把構造函數的入參資訊傳遞進去了。
  • 另外一個核心的内容是使用什麼方式來建立含有構造函數的 Bean 對象呢?這裡有兩種方式可以選擇,一個是基于 Java 本身自帶的方法

    DeclaredConstructor

    ,另外一個是使用 Cglib 來動态建立 Bean 對象。Cglib 是基于位元組碼架構 ASM 實作,是以你也可以直接通過 ASM 操作指令碼來建立對象

4. 為Bean對象注入屬性和依賴Bean的功能實作

鑒于屬性填充是在 Bean 使用

newInstance

或者

Cglib

建立後,開始補全屬性資訊,那麼就可以在類

AbstractAutowireCapableBeanFactory

的 createBean 方法中添加補全屬性方法。這部分大家在實習的過程中也可以對照Spring源碼學習,這裡的實作也是Spring的簡化版,後續對照學習會更加易于了解

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 屬性填充要在類執行個體化建立之後,也就是需要在

    AbstractAutowireCapableBeanFactory

    的 createBean 方法中添加

    applyPropertyValues

    操作。
  • 由于我們需要在建立Bean時候填充屬性操作,那麼就需要在 bean 定義 BeanDefinition 類中,添加 PropertyValues 資訊。
  • 另外是填充屬性資訊還包括了 Bean 的對象類型,也就是需要再定義一個 BeanReference,裡面其實就是一個簡單的 Bean 名稱,在具體的執行個體化操作時進行遞歸建立和填充,與 Spring 源碼實作一樣。Spring 源碼中 BeanReference 是一個接口

5. 設計與實作資源加載器,從Spring.xml解析和注冊Bean對象

依照本章節的需求背景,我們需要在現有的 Spring 架構雛形中添加一個資源解析器,也就是能讀取classpath、本地檔案和雲檔案的配置内容。這些配置内容就是像使用 Spring 時配置的 Spring.xml 一樣,裡面會包括 Bean 對象的描述和屬性資訊。 在讀取配置檔案資訊後,接下來就是對配置檔案中的 Bean 描述資訊解析後進行注冊操作,把 Bean 對象注冊到 Spring 容器中。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 資源加載器屬于相對獨立的部分,它位于 Spring 架構核心包下的IO實作内容,主要用于處理Class、本地和雲環境中的檔案資訊。
  • 當資源可以加載後,接下來就是解析和注冊 Bean 到 Spring 中的操作,這部分實作需要和 DefaultListableBeanFactory 核心類結合起來,因為你所有的解析後的注冊動作,都會把 Bean 定義資訊放入到這個類中。
  • 那麼在實作的時候就設計好接口的實作層級關系,包括我們需要定義出 Bean 定義的讀取接口

    BeanDefinitionReader

    以及做好對應的實作類,在實作類中完成對 Bean 對象的解析和注冊。

6. 設計與實作資源加載器,從Spring.xml解析和注冊Bean對象

為了能滿足于在 Bean 對象從注冊到執行個體化的過程中執行使用者的自定義操作,就需要在 Bean 的定義和初始化過程中插入接口類,這個接口再有外部去實作自己需要的服務。那麼在結合對 Spring 架構上下文的處理能力,就可以滿足我們的目标需求了。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 滿足于對 Bean 對象擴充的兩個接口,其實也是 Spring 架構中非常具有重量級的兩個接口:

    BeanFactoryPostProcess

    BeanPostProcessor

    ,也幾乎是大家在使用 Spring 架構額外新增開發自己組建需求的兩個必備接口。
  • BeanFactoryPostProcessor,是由 Spring 架構組建提供的容器擴充機制,允許在 Bean 對象注冊後但未執行個體化之前,對 Bean 的定義資訊

    BeanDefinition

    執行修改操作。
  • BeanPostProcessor,也是 Spring 提供的擴充機制,不過 BeanPostProcessor 是在 Bean 對象執行個體化之後修改 Bean 對象,也可以替換 Bean 對象。這部分與後面要實作的 AOP 有着密切的關系。
  • 同時如果隻是添加這兩個接口,不做任何包裝,那麼對于使用者來說還是非常麻煩的。我們希望于開發 Spring 的上下文操作類,把相應的 XML 加載 、注冊、執行個體化以及新增的修改和擴充都融合進去,讓 Spring 可以自動掃描到我們的新增服務,便于使用者使用。

7. 實作應用上下文,自動識别、資源加載、擴充機制

可能面對像 Spring 這樣龐大的架構,對外暴露的接口定義使用或者xml配置,完成的一系列擴充性操作,都讓 Spring 架構看上去很神秘。其實對于這樣在 Bean 容器初始化過程中額外添加的處理操作,無非就是預先執行了一個定義好的接口方法或者是反射調用類中xml中配置的方法,最終你隻要按照接口定義實作,就會有 Spring 容器在處理的過程中進行調用而已。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 在 spring.xml 配置中添加

    init-method、destroy-method

    兩個注解,在配置檔案加載的過程中,把注解配置一并定義到 BeanDefinition 的屬性當中。這樣在 initializeBean 初始化操作的工程中,就可以通過反射的方式來調用配置在 Bean 定義屬性當中的方法資訊了。另外如果是接口實作的方式,那麼直接可以通過 Bean 對象調用對應接口定義的方法即可,

    ((InitializingBean) bean).afterPropertiesSet()

    ,兩種方式達到的效果是一樣的。
  • 除了在初始化做的操作外,

    destroy-method

    DisposableBean

    接口的定義,都會在 Bean 對象初始化完成階段,執行注冊銷毀方法的資訊到 DefaultSingletonBeanRegistry 類中的 disposableBeans 屬性裡,這是為了後續統一進行操作。這裡還有一段擴充卡的使用,因為反射調用和接口直接調用,是兩種方式。是以需要使用擴充卡進行包裝,下文代碼講解中參考 DisposableBeanAdapter 的具體實作

    -關于銷毀方法需要在虛拟機執行關閉之前進行操作,是以這裡需要用到一個注冊鈎子的操作,如:

    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!")));

    這段代碼你可以執行測試,另外你可以使用手動調用 ApplicationContext.close 方法關閉容器。

8. 向虛拟機注冊鈎子,實作Bean對象的初始化和銷毀方法

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • init-method、destroy-method

    ((InitializingBean) bean).afterPropertiesSet()

  • destroy-method

    DisposableBean

    Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!")));

9. 定義标記類型Aware接口,實作感覺容器對象

如果說我希望拿到 Spring 架構中一些提供的資源,那麼首先需要考慮以一個什麼方式去擷取,之後你定義出來的擷取方式,在 Spring 架構中該怎麼去承接,實作了這兩項内容,就可以擴充出你需要的一些屬于 Spring 架構本身的能力了。

在關于 Bean 對象執行個體化階段我們操作過一些額外定義、屬性、初始化和銷毀的操作,其實我們如果像擷取 Spring 一些如 BeanFactory、ApplicationContext 時,也可以通過此類方式進行實作。那麼我們需要定義一個标記性的接口,這個接口不需要有方法,它隻起到标記作用就可以,而具體的功能由繼承此接口的其他功能性接口定義具體方法,最終這個接口就可以通過

instanceof

進行判斷和調用了。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 定義接口 Aware,在 Spring 架構中它是一種感覺标記性接口,具體的子類定義和實作能感覺容器中的相關對象。也就是通過這個橋梁,向具體的實作類中提供容器服務
  • 繼承 Aware 的接口包括:BeanFactoryAware、BeanClassLoaderAware、BeanNameAware和ApplicationContextAware,當然在 Spring 源碼中還有一些其他關于注解的,不過目前我們還是用不到。
  • 在具體的接口實作過程中你可以看到,一部分(BeanFactoryAware、BeanClassLoaderAware、BeanNameAware)在 factory 的 support 檔案夾下,另外 ApplicationContextAware 是在 context 的 support 中,這是因為不同的内容擷取需要在不同的包下提供。是以,在 AbstractApplicationContext 的具體實作中會用到向 beanFactory 添加 BeanPostProcessor 内容的

    ApplicationContextAwareProcessor

    操作,最後由 AbstractAutowireCapableBeanFactory 建立 createBean 時處理相應的調用操作。關于 applyBeanPostProcessorsBeforeInitialization 已經在前面章節中實作過,如果忘記可以往前翻翻

10. 關于Bean對象作用域以及FactoryBean的實作和使用

關于提供一個能讓使用者定義複雜的 Bean 對象,功能點非常不錯,意義也非常大,因為這樣做了之後 Spring 的生态種子孵化箱就此提供了,誰家的架構都可以在此标準上完成自己服務的接入。

但這樣的功能邏輯設計上并不複雜,因為整個 Spring 架構在開發的過程中就已經提供了各項擴充能力的

接茬

,你隻需要在合适的位置提供一個接茬的處理接口調用和相應的功能邏輯實作即可,像這裡的目标實作就是對外提供一個可以二次從 FactoryBean 的 getObject 方法中擷取對象的功能即可,這樣所有實作此接口的對象類,就可以擴充自己的對象功能了。MyBatis 就是實作了一個 MapperFactoryBean 類,在 getObject 方法中提供 SqlSession 對執行 CRUD 方法的操作 整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 整個的實作過程包括了兩部分,一個解決單例還是原型對象,另外一個處理 FactoryBean 類型對象建立過程中關于擷取具體調用對象的

    getObject

  • SCOPE_SINGLETON

    SCOPE_PROTOTYPE

    ,對象類型的建立擷取方式,主要區分在于

    AbstractAutowireCapableBeanFactory#createBean

    建立完成對象後是否放入到記憶體中,如果不放入則每次擷取都會重新建立。
  • createBean 執行對象建立、屬性填充、依賴加載、前置後置處理、初始化等操作後,就要開始做執行判斷整個對象是否是一個 FactoryBean 對象,如果是這樣的對象,就需要再繼續執行擷取 FactoryBean 具體對象中的

    getObject

    對象了。整個 getBean 過程中都會新增一個單例類型的判斷

    factory.isSingleton()

    ,用于決定是否使用記憶體存放對象資訊。

11. 基于觀察者實作,容器事件和事件監聽器

其實事件的設計本身就是一種觀察者模式的實作,它所要解決的就是一個對象狀态改變給其他對象通知的問題,而且要考慮到易用和低耦合,保證高度的協作。

在功能實作上我們需要定義出事件類、事件監聽、事件釋出,而這些類的功能需要結合到 Spring 的 AbstractApplicationContext#refresh(),以便于處理事件初始化和注冊事件監聽器的操作。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 在整個功能實作過程中,仍然需要在面向使用者的應用上下文

    AbstractApplicationContext

    中添加相關事件内容,包括:初始化事件釋出者、注冊事件監聽器、釋出容器重新整理完成事件。
  • 使用觀察者模式定義事件類、監聽類、釋出類,同時還需要完成一個廣播器的功能,接收到事件推送時進行分析處理符合監聽事件接受者感興趣的事件,也就是使用 isAssignableFrom 進行判斷。
  • isAssignableFrom 和 instanceof 相似,不過 isAssignableFrom 是用來判斷子類和父類的關系的,或者接口的實作類和接口的關系的,預設所有的類的終極父類都是Object。如果A.isAssignableFrom(B)結果是true,證明B可以轉換成為A,也就是A可以由B轉換而來。

12. 基于JDK和Cglib動态代理,實作AOP核心功能

在把 AOP 整個切面設計融合到 Spring 前,我們需要解決兩個問題,包括:

如何給符合規則的方法做代理

以及做完代理方法的案例後,把類的職責拆分出來

。而這兩個功能點的實作,都是以切面的思想進行設計和開發。如果不是很清楚 AOP 是啥,你可以把切面了解為用刀切韭菜,一根一根切總是有點慢,那麼用手(

代理

)把韭菜捏成一把,用菜刀或者斧頭這樣不同的攔截操作來處理。而程式中其實也是一樣,隻不過韭菜變成了方法,菜刀變成了攔截方法。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 就像你在使用 Spring 的 AOP 一樣,隻處理一些需要被攔截的方法。在攔截方法後,執行你對方法的擴充操作。
  • 那麼我們就需要先來實作一個可以代理方法的 Proxy,其實代理方法主要是使用到方法攔截器類處理方法的調用

    MethodInterceptor#invoke

    ,而不是直接使用 invoke 方法中的入參 Method method 進行

    method.invoke(targetObj, args)

    這塊是整個使用時的差異。
  • 除了以上的核心功能實作,還需要使用到

    org.aspectj.weaver.tools.PointcutParser

    處理攔截表達式

    "execution(* cn.bugstack.springframework.test.bean.IUserService.*(..))"

    ,有了方法代理和處理攔截,我們就可以完成設計出一個 AOP 的雛形了。

13. 把AOP動态代理,融入到Bean的生命周期

其實在有了AOP的核心功能實作後,把這部分功能服務融入到 Spring 其實也不難,隻不過要解決幾個問題,包括:怎麼借着 BeanPostProcessor 把動态代理融入到 Bean 的生命周期中,以及如何組裝各項切點、攔截、前置的功能和适配對應的代理器。整體設計結構如下圖:

CRUD搬磚兩三年了,怎麼閱讀Spring源碼?
  • 為了可以讓對象建立過程中,能把xml中配置的代理對象也就是切面的一些類對象執行個體化,就需要用到 BeanPostProcessor 提供的方法,因為這個類的中的方法可以分别作用與 Bean 對象執行初始化前後修改 Bean 的對象的擴充資訊。但這裡需要集合于 BeanPostProcessor 實作新的接口和實作類,這樣才能定向擷取對應的類資訊。
  • 但因為建立的是代理對象不是之前流程裡的普通對象,是以我們需要前置于其他對象的建立,是以在實際開發的過程中,需要在 AbstractAutowireCapableBeanFactory#createBean 優先完成 Bean 對象的判斷,是否需要代理,有則直接傳回代理對象。在Spring的源碼中會有 createBean 和 doCreateBean 的方法拆分
  • 這裡還包括要解決方法攔截器的具體功能,提供一些 BeforeAdvice、AfterAdvice 的實作,讓使用者可以更簡化的使用切面功能。除此之外還包括需要包裝切面表達式以及攔截方法的整合,以及提供不同類型的代理方式的代理工廠,來包裝我們的切面服務。

三、 學習說明

本代碼倉庫 https://github.com/fuzhengwei/small-spring 以 Spring 源碼學習為目的,通過手寫簡化版 Spring 架構,了解 Spring 核心原理。

在手寫的過程中會簡化 Spring 源碼,摘取整體架構中的核心邏輯,簡化代碼實作過程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、資源處理等内容實作。

  1. 此專欄為實戰編碼類資料,在學習的過程中需要結合文中每個章節裡,要解決的目标,進行的思路設計,帶入到編碼實操過程。在學習編碼的同時也最好了解關于這部分内容為什麼這樣的實作,它用到了哪樣的設計模式,采用了什麼手段做了什麼樣的職責分離。隻有通過這樣的學習才能更好的了解和掌握 Spring 源碼的實作過程,也能幫助你在以後的深入學習和實踐應用的過程中打下一個紮實的基礎。
  2. 另外此專欄内容的學習上結合了設計模式,下對應了SpringBoot 中間件設計和開發,是以讀者在學習的過程中如果遇到不了解的設計模式可以翻閱相應的資料,在學習完 Spring 後還可以結合中間件的内容進行練習。
  3. 源碼:此專欄涉及到的源碼已經全部整合到目前工程下,可以與章節中對應的案例源碼一一比對上。大家拿到整套工程可以直接運作,也可以把每個章節對應的源碼工程單獨打開運作。
  4. 如果你在學習的過程中遇到什麼問題,包括:不能運作、優化意見、文字錯誤等任何問題都可以送出issue
  5. 在專欄的内容編寫中,每一個章節都提供了清晰的設計圖稿和對應的類圖,是以學習過程中一定不要隻是在乎代碼是怎麼編寫的,更重要的是了解這些設計的内容是如何來的。

😁 好嘞,希望你可以學的愉快!

公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!