造輪子:實現一個簡易的 Spring IoC 容器

来源:https://www.cnblogs.com/deppwang/archive/2020/04/19/12733747.html
-Advertisement-
Play Games

作者: "DeppWang" 、 "原文地址" 我通過實現一個簡易的 Spring IoC 容器,算是入門了 Spring 框架。本文是對實現過程的一個總結提煉, 需要配合源碼閱讀 , "源碼地址" 。 結合本文和源碼,你應該可以學到:Spring 的原理和 Spring Boot 的原理。 Spr ...


作者:DeppWang原文地址

source:https://fernandofranzini.wordpress.com/

我通過實現一個簡易的 Spring IoC 容器,算是入門了 Spring 框架。本文是對實現過程的一個總結提煉,需要配合源碼閱讀源碼地址

結合本文和源碼,你應該可以學到:Spring 的原理和 Spring Boot 的原理。

Spring 框架是 Java 開發的,Java 是面向對象的語言,所以 Spring 框架本身有大量的抽象、繼承、多態。對於初學者來說,光是理清他們的邏輯就很麻煩,我摒棄了那些包裝,只實現了最本質的功能。代碼不是很嚴謹,但只為了理解 Spring 思想卻夠了。

下麵正文開始。

零、前言

在沒有 Spring 框架的遠古時代,我們業務邏輯長這樣:

public class PetStoreService {
    AccountDao accountDao = new AccountDao();
}

public class AccountDao {
}

PetStoreService petStoreService = new PetStoreService();

到處都是 new 關鍵字,需要開發人員顯式的使用 new 關鍵字來創建業務類對象(實例)。這樣有很多弊端,如,創建的對象太多,耦合性太強,等等。

有個叫 Rod Johnson 老哥對此很不爽,就開發了一個叫 Spring 的框架,就是為了幹掉 new 關鍵字(哈哈,我杜撰的,只是為了說明 Spring 的作用)。

有了 Spring 框架,由框架來新建對象,管理對象,並處理對象之間的依賴,我們程式員再也不用 new 業務類對象了。我們來看看 Spring 框架是如何實現的吧。

註:以下 Spring 框架簡寫為 Spring

本節源碼對應:v0

一、實現「實例化 Bean 」

首先,Spring 需要實例化類,將其轉換為對象。在 Spring 中,我們管(業務)類叫 Bean,所以實例化類也可以稱為實例化 Bean。

早期 Spring 需要藉助 xml 配置文件來實現實例化 Bean,可以分為三步(配合源碼 v1 閱讀):

  1. 從 xml 配置文件獲取 Bean 信息,如全限定名等,將其作為 BeanDefinition(Bean 定義類)的屬性
  2. 使用一個 Map 存放所有 BeanDefinition,此時 Spring 本質上是一個 Map,存放 BeanDefinition
  3. 當獲取 Bean 實例時,通過類載入器,根據全限定名,得到其類對象,通過類對象利用反射創建 Bean 實例

關於類載入和反射,前者可以看看《深入理解 Java 虛擬機》第 7 章,後者可以看看《廖雪峰 Java 教程》反射 部分。本文只學習 Spring,這兩個知識點不做深入討論。

名詞解釋:

  • 全限定名:指編譯後的 class 文件在 jar 包中的路徑

本節源碼對應:v1

二、實現「填充屬性(依賴註入)」

實現實例化 Bean 後,此時成員變數(引用)還為 null:

此時需要通過一種方式實現,讓引用指向實例,我們管這一步叫填充屬性。

當一個 Bean 的成員變數類型是另一個 Bean 時,我們可以說一個 Bean 依賴於另一個 Bean。所以填充屬性,也可以稱為依賴註入(Dependency Injection,簡稱 DI)。

拋開 Spring 不談,在正常情況下,我們有兩種方式實現依賴註入,1、使用 Setter() 方法,2、使用構造方法。使用 Setter() 方法如下:

public class PetStoreService {
    private AccountDao accountDao;
    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
}

public class AccountDao {
}

PetStoreService petStore = new PetStoreService();
petStore.setAccountDao(new AccountDao()); // 將依賴 new AccountDao() 註入 petStore

其實早期 Spring 也是通過這兩種方式來實現依賴註入的。下麵是 Spring 通過 xml 文件 + Setter() 來實現依賴註入的步驟(配合源碼 v2 閱讀):

  1. 給 PetStoreService 添加 Setter() 方法,並稍微修改一下 xml 配置文件,添加 <property>,代表對應 Setter() 方法。
  2. 從 xml 配置文件獲取 Bean 的屬性 <property>,存放到 BeanDefinition 的 propertyNames 中。
  3. 通過 propertyName 獲取屬性實例,利用反射,通過 Setter() 方法實現填充屬性(依賴註入)

基於構造函數實現依賴註入的方式跟 Setter() 方法差不多,感興趣可以 Google 搜索查看。

因為 Spring 實現了依賴註入,所以我們程式員沒有了創建對象的控制權,所以也被稱為控制反轉(Inversion of Control,簡稱 IoC)。因為 Spring 使用 Map 管理 BeanDefinition,我們也可以將 Spring 稱為 IoC 容器。

本節源碼對應:v2

三、使用「單例模式、工廠方法模式」

前面兩步實現了獲取 Bean 實例時創建 Bean 實例,但 Bean 實例經常使用,不能每次都新創建。其實在 Spring 中,一個業務類只對應一個 Bean 實例,這需要使用單例模式。

單例模式:一個類有且只有一個實例

Spring 使用類對象創建 Bean 實例,是如何實現單例模式的?

Spring 其實使用一個 Map 存放所有 Bean 實例。創建時,先看 Map 中是否有 Bean 實例,沒有就創建;獲取時,直接從 Map 中獲取。這種方式能保證一個類只有一個 Bean 實例。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(64);

早期 Spring 使用 Bean 的策略是用到時再實例化所用 Bean,傑出代表是 XmlBeanFactory,後期為了實現更多的功能,新增了 ApplicationContext,兩者都繼承於 BeanFactory 介面。這使用了工廠方法模式。

工廠方法模式:定義一個用於創建對象的介面,讓子類決定實例化哪一個類。Factory Method 使一個類的實例化延遲到其子類。

我們將 BeanIocContainer 修改為 BeanFactory 介面,只提供 getBean() 方法。創建(IoC)容器由其子類自己實現。

ApplicationContext 和 BeanFactory 的區別:ApplicationContext 初始化時就實例化所有 Bean,BeanFactory 用到時再實例化所用 Bean。

本節源碼對應:v3

三、實現「註解」

前面使用 xml 配置文件的方式,實現了實例化 Bean 和依賴註入。這種方式比較麻煩,還容易出錯。Spring 從 2.5ref 開始可使用註解替代 xml 配置文件。比如:

  1. 使用 @Component 註解代替 <bean>
  2. 使用 @Autowired 註解代替 <property>

@Component 用於生成 BeanDefinition,原理(配合源碼 v4 閱讀):

  • 根據 component-scan 指定路徑,找到路徑下所有包含 @Component 註解的 Class 文件,作為 BeanDefinition
  • 如何判斷 Class 是否有 @Component:利用位元組碼技術,獲取 Class 文件中的元數據(註解等),判斷元數據中是否有 @Componet

@Autowired 用於依賴註入,原理(配合源碼 v4 閱讀):

  • 通過反射,查看 Field 中是否有 @Autowired 類型的註解,有,則使用反射實現依賴註入

至此,我們還是在需要通過配置文件來實現組件掃描。有沒有完全不使用配置文件的方式?有!

我們可以使用 @Configuration 替代配置文件,並使用 @ComponentScan 來替代配置文件的 <context:component-scan>

@Configuration // 將類標記為 @Configuration,代表這個類是相當於一個配置文件
@ComponentScan // ComponentScan 掃描 PetStoreConfig.class 所在路徑及其所在路徑所有子路徑的文件
public class PetStoreConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PetStoreConfig.class);
        PetStoreService userService = context.getBean(PetStoreService.class);
        userService.getAccountDao();
    }
}

使用註解其實跟使用 xml 配置文件一樣,目的是將配置類作為入口,實現掃描組件,將其載入進 IoC 容器中的功能。

AnnotationConfigApplicationContext 是專為針對配置類的啟動類。其實現機制,可以 Google 查閱。

名詞解釋:

  • Component:組件
  • Autowired:自動裝配

本節源碼對應:v4

四、Spring Boot 原理

說到了 @Configuration 和 @ComponentScan,就不得不提 Spring Boot。因為 Spring Boot 就使用了 @Configuration 和 @ComponentScan,你可以點開 @SpringBootApplication 看到。

我們發現,Spring Boot 啟動時,並沒有使用 AnnotationConfigApplicationContext 來指定啟動某某 Config 類。這是因為它使用了 @EnableAutoConfiguration 註解。

Spring Boot 利用了 @EnableAutoConfiguration 來自動載入標識為 @Configuration 的配置類到容器中。Spring Boot 還可以將需要自動載入的配置類放在 spring.factories 中,Spring Boot 將自動載入 spring.factories 中的配置類。spring.factories 需放置於META-INF 下。

如 Spring Boot 項目啟動時,autocofigure 包中將自動載入到容器的(部分)配置類如下:

以上也是 Spring Boot 的原理。

在 Spring Boot 中,我們引入的 jar 包都有一個欄位,starter,我們叫 starter 包。

標識為 starter(啟動器)是因為引入這些包時,我們不用設置額外操作,它能被自動裝配,starter 包一般都包含自己的 spring.factories。如 spring-cloud-starter-eureka-server:

如 druid-spring-boot-starter:

有時候我們還需要自定義 starter 包,比如在 Spring Cloud 中,當某個應用要調用另一個應用的代碼時,要麼調用方使用 Feign(HTTP),要麼將被調用方自定義為 starter 包,讓調用方依賴引用,再 @Autowired 使用。此時需要在被調用方設置配置類和 spring.factories:

@Configuration
@ComponentScan
public class ProviderAppConfiguration {
}

// spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.amy.cloud.amycloudact.ProviderAppConfiguration

當然,你也可以把這兩個文件放在調用方(此時要指定掃描路徑),但一般放在被調用方。ps:如果你兩個應用的 base-package 路徑一樣,那麼可以不用這一步。

說了 Spring Boot,那麼在 Spring MVC,如何將引入 jar 包的組件註入容器?

  • 跟掃描本項目包一樣,在 xml ,增加引入 jar 包的掃描路徑:
<context:component-scan base-package="引入 jar 包的 base-package" />
...

嗯,本節沒有源碼

五、結語

以上實現了一個簡易的 Spring IoC 容器,順便說了一下 Spring Boot 原理。Spring 還有很多重要功能,如:管理 Bean 生命周期、AOP 的實現,等等。後續有機會再做一次分享。

來個註解小結:

  • Spring 只實例化標識為 @Component 的組件(即業務類對象)
  • @Component 作為組件標識
  • @Autowired 用於判斷是否需要依賴註入
  • @ComponentScan 指定組件掃描路徑,不指定即為當前路徑
  • @Configuration 代表配置類,作為入口
  • @EnableAutoConfiguration 實現載入配置類

有的童鞋可能還會有這樣的疑問:

jdk jar 包、工具 jar 包的類是否需要註入容器?

  • 回答是不需要,因為容器只管理業務類,註入容器的類都有 @Component 註解。

全文完。

本文由博客一文多發平臺 OpenWrite 發佈!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • springboot 項目使用阿裡雲簡訊服務發送手機驗證碼 (第一篇) 1、註冊阿裡雲賬戶進行賬號實名認證 2、申請簡訊簽名和模板 3、創建access_key和access_secret 4、然後就是代碼編寫 一、找到產品與服務裡面的雲通信模塊,然後找到簡訊服務,開通簡訊服務。我這裡已經開通,可直 ...
  • Python變數和數據類型 數據類型 print語句 註釋 Python的註釋以 # 開頭,後面的文字直到行尾都算註釋 # 這一行全部都是註釋... print 'hello' # 這也是註釋 這裡要註意註意:不管你是為了Python就業還是興趣愛好,記住:項目開發經驗永遠是核心,如果你沒有2020 ...
  • 頁面寫死el-select下拉框標簽: 通過v-for="item in stateArr"綁定,stateArr聲明在Vue組件裡面的data參數裡面代碼如下: <el-form class="small-space" :model="createdItem" label-position="le ...
  • 在C++11中, 不再只有邏輯與的含義,還可能是右值引用: 但也不盡然, 還可能是轉發引用: “轉發引用”(forwarding reference)舊稱“通用引用”(universal reference),它的“通用”之處在於你可以拿一個左值綁定給轉發引用,但不能給右值引用: 一個函數的參數要想 ...
  • 1、在主函數中使用join()方法。 t1.start(); t2.start(); t3.start(); t1.join();//不會導致t1和t2和t3的順序執行 t2.join(); t3.join(); System.out.println("Main finished"); 2、Coun ...
  • 本題要求兩個給定正整數的最大公約數和最小公倍數。輸入格式:輸入在一行中給出兩個正整數M和N(≤1000)。輸出格式:在一行中順序輸出M和N的最大公約數和最小公倍數,兩數字間以1空格分隔。代碼如下:#!/usr/bin/python# -*- coding: utf-8 -*-#定義求公約數的方法de... ...
  • 上一篇里已經成功的將一個golang的demo服務部署到k8s環境里了,部署的時候我們用到了yaml配置文件,今天這裡簡單的介紹下如何使用創建kubernetes里的資源。在kubernetes里,一切對象皆為資源,可以通過命令或配置文件來創建。 命令行創建資源 通過命令行可以創建namespace ...
  • gitlab-runner在Kubernetes里安裝的方法可以通過官方提供的chart來用helm3安裝。官方chart的倉庫地址為:https://gitlab.com/gitlab-org/charts/gitlab-runner,但這裡有個問題就是無法配置宿主機的掛載目錄,根據gitlab- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...