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


作者:小傅哥
博客://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生命周期、上下文、作用域、資源處理等內容實現。

源碼//github.com/fuzhengwei/small-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 的定義、註冊、獲取三個基本步驟,簡化設計如下;

  • 定義: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

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

3. 基於Cglib實現含構造函數的類實例化策略

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

圖 4-1

  • 參考 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的簡化版,後續對照學習會更加易於理解

  • 屬性填充要在類實例化創建之後,也就是需要在 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 容器中。整體設計結構如下圖:

  • 資源加載器屬於相對獨立的部分,它位於 Spring 框架核心包下的IO實現內容,主要用於處理Class、本地和雲環境中的文件信息。
  • 當資源可以加載後,接下來就是解析和註冊 Bean 到 Spring 中的操作,這部分實現需要和 DefaultListableBeanFactory 核心類結合起來,因為你所有的解析後的註冊動作,都會把 Bean 定義信息放入到這個類中。
  • 那麼在實現的時候就設計好接口的實現層級關係,包括我們需要定義出 Bean 定義的讀取接口 BeanDefinitionReader 以及做好對應的實現類,在實現類中完成對 Bean 對象的解析和註冊。

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

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

  • 滿足於對 Bean 對象擴展的兩個接口,其實也是 Spring 框架中非常具有重量級的兩個接口:BeanFactoryPostProcessBeanPostProcessor,也幾乎是大家在使用 Spring 框架額外新增開發自己組建需求的兩個必備接口。
  • BeanFactoryPostProcessor,是由 Spring 框架組建提供的容器擴展機制,允許在 Bean 對象註冊後但未實例化之前,對 Bean 的定義信息 BeanDefinition 執行修改操作。
  • BeanPostProcessor,也是 Spring 提供的擴展機制,不過 BeanPostProcessor 是在 Bean 對象實例化之後修改 Bean 對象,也可以替換 Bean 對象。這部分與後面要實現的 AOP 有着密切的關係。
  • 同時如果只是添加這兩個接口,不做任何包裝,那麼對於使用者來說還是非常麻煩的。我們希望於開發 Spring 的上下文操作類,把相應的 XML 加載 、註冊、實例化以及新增的修改和擴展都融合進去,讓 Spring 可以自動掃描到我們的新增服務,便於用戶使用。

7. 實現應用上下文,自動識別、資源加載、擴展機制

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

  • 在 spring.xml 配置中添加 init-method、destroy-method 兩個註解,在配置文件加載的過程中,把註解配置一併定義到 BeanDefinition 的屬性當中。這樣在 initializeBean 初始化操作的工程中,就可以通過反射的方式來調用配置在 Bean 定義屬性當中的方法信息了。另外如果是接口實現的方式,那麼直接可以通過 Bean 對象調用對應接口定義的方法即可,((InitializingBean) bean).afterPropertiesSet(),兩種方式達到的效果是一樣的。
  • 除了在初始化做的操作外,destroy-methodDisposableBean 接口的定義,都會在 Bean 對象初始化完成階段,執行註冊銷毀方法的信息到 DefaultSingletonBeanRegistry 類中的 disposableBeans 屬性里,這是為了後續統一進行操作。這裡還有一段適配器的使用,因為反射調用和接口直接調用,是兩種方式。所以需要使用適配器進行包裝,下文代碼講解中參考 DisposableBeanAdapter 的具體實現
    -關於銷毀方法需要在虛擬機執行關閉之前進行操作,所以這裡需要用到一個註冊鉤子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 這段代碼你可以執行測試,另外你可以使用手動調用 ApplicationContext.close 方法關閉容器。

8. 向虛擬機註冊鉤子,實現Bean對象的初始化和銷毀方法

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

  • 在 spring.xml 配置中添加 init-method、destroy-method 兩個註解,在配置文件加載的過程中,把註解配置一併定義到 BeanDefinition 的屬性當中。這樣在 initializeBean 初始化操作的工程中,就可以通過反射的方式來調用配置在 Bean 定義屬性當中的方法信息了。另外如果是接口實現的方式,那麼直接可以通過 Bean 對象調用對應接口定義的方法即可,((InitializingBean) bean).afterPropertiesSet(),兩種方式達到的效果是一樣的。
  • 除了在初始化做的操作外,destroy-methodDisposableBean 接口的定義,都會在 Bean 對象初始化完成階段,執行註冊銷毀方法的信息到 DefaultSingletonBeanRegistry 類中的 disposableBeans 屬性里,這是為了後續統一進行操作。這裡還有一段適配器的使用,因為反射調用和接口直接調用,是兩種方式。所以需要使用適配器進行包裝,下文代碼講解中參考 DisposableBeanAdapter 的具體實現
    -關於銷毀方法需要在虛擬機執行關閉之前進行操作,所以這裡需要用到一個註冊鉤子的操作,如:Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("close!"))); 這段代碼你可以執行測試,另外你可以使用手動調用 ApplicationContext.close 方法關閉容器。

9. 定義標記類型Aware接口,實現感知容器對象

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

在關於 Bean 對象實例化階段我們操作過一些額外定義、屬性、初始化和銷毀的操作,其實我們如果像獲取 Spring 一些如 BeanFactory、ApplicationContext 時,也可以通過此類方式進行實現。那麼我們需要定義一個標記性的接口,這個接口不需要有方法,它只起到標記作用就可以,而具體的功能由繼承此接口的其他功能性接口定義具體方法,最終這個接口就可以通過 instanceof 進行判斷和調用了。整體設計結構如下圖:

  • 定義接口 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 方法的操作 整體設計結構如下圖:

  • 整個的實現過程包括了兩部分,一個解決單例還是原型對象,另外一個處理 FactoryBean 類型對象創建過程中關於獲取具體調用對象的 getObject 操作。
  • SCOPE_SINGLETONSCOPE_PROTOTYPE,對象類型的創建獲取方式,主要區分在於 AbstractAutowireCapableBeanFactory#createBean 創建完成對象後是否放入到內存中,如果不放入則每次獲取都會重新創建。
  • createBean 執行對象創建、屬性填充、依賴加載、前置後置處理、初始化等操作後,就要開始做執行判斷整個對象是否是一個 FactoryBean 對象,如果是這樣的對象,就需要再繼續執行獲取 FactoryBean 具體對象中的 getObject 對象了。整個 getBean 過程中都會新增一個單例類型的判斷factory.isSingleton(),用於決定是否使用內存存放對象信息。

11. 基於觀察者實現,容器事件和事件監聽器

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

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

  • 在整個功能實現過程中,仍然需要在面向用戶的應用上下文 AbstractApplicationContext 中添加相關事件內容,包括:初始化事件發佈者、註冊事件監聽器、發佈容器刷新完成事件。
  • 使用觀察者模式定義事件類、監聽類、發佈類,同時還需要完成一個廣播器的功能,接收到事件推送時進行分析處理符合監聽事件接受者感興趣的事件,也就是使用 isAssignableFrom 進行判斷。
  • isAssignableFrom 和 instanceof 相似,不過 isAssignableFrom 是用來判斷子類和父類的關係的,或者接口的實現類和接口的關係的,默認所有的類的終極父類都是Object。如果A.isAssignableFrom(B)結果是true,證明B可以轉換成為A,也就是A可以由B轉換而來。

12. 基於JDK和Cglib動態代理,實現AOP核心功能

在把 AOP 整個切面設計融合到 Spring 前,我們需要解決兩個問題,包括:如何給符合規則的方法做代理以及做完代理方法的案例後,把類的職責拆分出來。而這兩個功能點的實現,都是以切面的思想進行設計和開發。如果不是很清楚 AOP 是啥,你可以把切面理解為用刀切韭菜,一根一根切總是有點慢,那麼用手(代理)把韭菜捏成一把,用菜刀或者斧頭這樣不同的攔截操作來處理。而程序中其實也是一樣,只不過韭菜變成了方法,菜刀變成了攔截方法。整體設計結構如下圖:

  • 就像你在使用 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 的生命周期中,以及如何組裝各項切點、攔截、前置的功能和適配對應的代理器。整體設計結構如下圖:

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

三、 學習說明

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

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


  1. 此專欄為實戰編碼類資料,在學習的過程中需要結合文中每個章節里,要解決的目標,進行的思路設計,帶入到編碼實操過程。在學習編碼的同時也最好理解關於這部分內容為什麼這樣的實現,它用到了哪樣的設計模式,採用了什麼手段做了什麼樣的職責分離。只有通過這樣的學習才能更好的理解和掌握 Spring 源碼的實現過程,也能幫助你在以後的深入學習和實踐應用的過程中打下一個紮實的基礎。

  2. 另外此專欄內容的學習上結合了設計模式,下對應了SpringBoot 中間件設計和開發,所以讀者在學習的過程中如果遇到不理解的設計模式可以翻閱相應的資料,在學習完 Spring 後還可以結合中間件的內容進行練習。

  3. 源碼:此專欄涉及到的源碼已經全部整合到當前工程下,可以與章節中對應的案例源碼一一匹配上。大家拿到整套工程可以直接運行,也可以把每個章節對應的源碼工程單獨打開運行。

  4. 如果你在學習的過程中遇到什麼問題,包括:不能運行、優化意見、文字錯誤等任何問題都可以提交issue

  5. 在專欄的內容編寫中,每一個章節都提供了清晰的設計圖稿和對應的類圖,所以學習過程中一定不要只是在乎代碼是怎麼編寫的,更重要的是理解這些設計的內容是如何來的。


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