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

小傅哥發表於2021-07-26


作者:小傅哥
部落格: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

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 的實現,讓使用者可以更簡化的使用切面功能。除此之外還包括需要包裝切面表示式以及攔截方法的整合,以及提供不同型別的代理方式的代理工廠,來包裝我們的切面服務。

三、 學習說明

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

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


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

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

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

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

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


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

相關文章