
作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成長,讓自己和他人都能有所識訓!😄

👨?💻連讀同事寫的代碼都費勁,還讀Spring? 咋的,Spring 很難讀!
這個與我們碼農朝夕相處的 Spring,就像睡在你身邊的媳婦,你知道找她要吃、要喝、要零花錢、要買皮膚,但你不知道她的倉庫共有多少存糧、也不知道她是買了理財還是存了銀行,🍑開個玩笑,接下來我要正經了!
一、為什么Spring難讀懂?
為什么 Spring 天天用,但要想去讀一讀原始碼,怎么就那么難!因為由Java和J2EE開發領域的專家 Rod Johnson 于 2002 年提出并隨后創建的 Spring 框架,隨著 JDK 版本和市場需要發展至今,至今它已經越來越大了!
當你閱讀它的原始碼你會感覺:
- 怎么這代碼跳來跳去的,根本不是像自己寫代碼一樣那么
單純 - 為什么那么多的介面和介面繼承,類A繼承的類B還實作了類A實作的介面X
- 簡單工廠、工廠方法、代理模式、觀察者模式,怎么用了會有這樣多的設計模式使用
- 又是資源加載、又是應用背景關系、又是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實作含建構式的類實體化策略
填平這個坑的技術設計主要考慮兩部分,一個是串流程從哪合理的把建構式的入參資訊傳遞到實體化操作里,另外一個是怎么去實體化含有建構式的物件,

- 參考 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 框架中非常具有重量級的兩個介面:
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 容器在處理的程序中進行呼叫而已,整體設計結構如下圖:

- 在 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物件的初始化和銷毀方法
可能面對像 Spring 這樣龐大的框架,對外暴露的介面定義使用或者xml配置,完成的一系列擴展性操作,都讓 Spring 框架看上去很神秘,其實對于這樣在 Bean 容器初始化程序中額外添加的處理操作,無非就是預先執行了一個定義好的介面方法或者是反射呼叫類中xml中配置的方法,最終你只要按照介面定義實作,就會有 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 方法關閉容器,
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_SINGLETON、SCOPE_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生命周期、背景關系、作用域、資源處理等內容實作,
-
此專欄為實戰編碼類資料,在學習的程序中需要結合文中每個章節里,要解決的目標,進行的思路設計,帶入到編碼實操程序,在學習編碼的同時也最好理解關于這部分內容為什么這樣的實作,它用到了哪樣的設計模式,采用了什么手段做了什么樣的職責分離,只有通過這樣的學習才能更好的理解和掌握 Spring 原始碼的實作程序,也能幫助你在以后的深入學習和實踐應用的程序中打下一個扎實的基礎,
-
另外此專欄內容的學習上結合了設計模式,下對應了SpringBoot 中間件設計和開發,所以讀者在學習的程序中如果遇到不理解的設計模式可以翻閱相應的資料,在學習完 Spring 后還可以結合中間件的內容進行練習,
-
原始碼:此專欄涉及到的原始碼已經全部整合到當前工程下,可以與章節中對應的案例原始碼一一匹配上,大家拿到整套工程可以直接運行,也可以把每個章節對應的原始碼工程單獨打開運行,
-
如果你在學習的程序中遇到什么問題,包括:不能運行、優化意見、文字錯誤等任何問題都可以提交issue
-
在專欄的內容撰寫中,每一個章節都提供了清晰的設計圖稿和對應的類圖,所以學習程序中一定不要只是在乎代碼是怎么撰寫的,更重要的是理解這些設計的內容是如何來的,
😁 好嘞,希望你可以學的愉快!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/290426.html
標籤:java
