造輪子:實現一個簡易的 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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...