理解Spring(一):Spring 與 IoC

張永恆發表於2020-07-15

什麼是 Spring

Spring 是一個輕量級的企業級軟體開發框架,它於2004年釋出第一個版本,其目的是用於簡化企業級應用程式的開發。

在傳統應用程式開發中,一個完整的應用是由一組相互協作的物件組成,開發一個應用除了要開發業務邏輯之外,更多的是關注如何使這些物件協作來完成所需功能,而且要高內聚,低耦合。雖然一些設計模式可以幫我們達到這個目的,可是這又徒增了我們的負擔。如果能通過配置的方式來建立物件,管理物件之間依賴關係,那麼就能夠減少許多工作,提高開發效率。Spring 框架主要就是來完成這個功能的。

Spring 框架除了幫我們管理物件及其依賴關係之外,還提供了面向切面程式設計的能力,在此基礎上,允許我們對一些通用任務如日誌記錄、安全控制、異常處理等進行集中式管理,還能幫我們管理最頭疼的資料庫事務。此外,它還提供了粘合其他框架的能力,使我們可以方便地與各種第三方框架進行整合,而且不管是 Java SE 應用程式還是JavaEE應用程式都可以使用這個平臺進行開發。

Spring 是基於 IoCAOP 兩大思想理論衍生而來的,可以說,Spring是一個同時實現了 IoC 和 AOP 的框架。

Spring 的整體架構

Spring 的整體架構如圖所示:

理解Spring(一):Spring 與 IoC

核心模組只有3個:Beans、Core、和 Context ,它們構建起了整個 Spring 的骨架,沒有它們就不可能有 AOP、Web 等上層的特性功能。如果在它們3箇中選出一個最核心的模組的話,那就非 Beans 模組莫屬了,其實 Spring 就是面向 Bean 的程式設計(BOP, Bean Oriented Programming),Bean 在 Spring 中才是真正的主角。關於 Bean 的觀念,會在後面進行介紹。

什麼是 IoC

IoC(Inverse Of Control,控制反轉)是一種設計思想,目標是實現解耦。所謂控制反轉,是指對資源的控制方式反轉了。這裡說的資源主要指我們的業務物件,物件之間往往會存在某種依賴關係,當一個物件依賴另一個物件時,傳統的做法是在它內部直接 new 一個出來,即由物件自己負責建立並管理它所依賴的資源,這是傳統的對資源的控制方式。IoC 就是將其顛倒過來,物件由主動控制所需資源變成了被動接受,由第三方(IoC 容器)對資源進行集中管理,物件需要什麼資源就從IoC容器中取,或者讓容器主動將所需資源注入進來。

IoC 之後,物件與所需資源之間不再具有強耦合性,資源可以被直接替換,而無需改動需求方的程式碼。舉個例子,董事長需要一個祕書,傳統的做法是,董事長自己去指定一個祕書,控制權在他自己手上,但是這會導致他與祕書之間的耦合性較強,一旦想換祕書了,就得修改自己的程式碼。IoC 的做法是,董事長宣告自己需要一個祕書,由IoC 容器為他指定一個祕書,至於是哪個祕書,男的還是女的,一切由容器說了算,如果要換祕書,也是修改容器的配置檔案,與董事長無關,這樣就實現了兩者間的解耦。

IoC 的兩種實現方式:

  • DI(Dependency Injection,依賴注入)。所謂依賴注入,是指物件所依賴的資源通過被動注入的方式得到,換言之,容器會主動地根據預先配置的依賴關係將資源注入進來。
  • DL(Dependency Lookup,依賴查詢)。依賴查詢是早先的一種 IoC 實現方式,現已過時。物件需要呼叫容器的API查詢它所依賴的資源。

Bean 的概念

  • 在 Java 中,“Bean”是對用Java語言編寫的“可重用元件”的慣用叫法。可以從字面意思去理解,Java 原本指爪哇咖啡,bean 指咖啡豆,而咖啡的“可重用元件”就是咖啡豆嘛。官方並沒有說明所謂的“元件”具體指的是什麼,因為“元件”本身就是一個抽象的概念,是對軟體組成部分的抽象,因此,Bean作為可重用元件的代稱,既可指類,也可指物件。
  • 在 Spring 中,Bean 的概念同上,有時也稱 Component。 由 Spring 的 IoC容器所管理的 Bean 稱作 Spring Bean。
  • 擴充套件:

    Java Bean 的概念不同於 Bean,Java Bean 是指符合 JavaBeans 規範的一種特殊的 Bean,即:所有屬性均為 private,提供 getter 和 setter,提供預設構造方法。JavaBean 也可以認為是遵循特定約定的POJO。

    POJO(Plain Ordinary Java Object)是指簡單和普通的 Java 物件。嚴格來說,它不繼承類,不實現介面,不處理業務邏輯,僅用於封裝資料。

Spring 的基本使用

首先配置 Bean 資訊,向 Spring 的 IoC 容器(或簡稱 Spring 容器)中註冊 Bean。以 XML 方式為例,如下配置了兩個 Bean,其中第一個依賴第二個:

<bean id="John" class="Person">
	<property name="lover">
		<ref bean="Mary"/>
	</property>
</bean>
<bean id="Mary" class="Person"/>

然後建立 Spring 容器,同時繫結配置檔案。如下:

ApplicationContext container = new ClassPathXmlApplicationContext("bean-config.xml");

然後通過容器的 getBean 方法即可得到我們在配置檔案中所配置的 Bean 的例項。如下:

Person John = container.getBean("John");

Spring 的兩種 IoC 容器

Spring 提供了兩種 IoC 容器: BeanFactory 和 ApplicationContext 。

  • BeanFactory 提供基本的 IoC 服務支援。
  • ApplicationContext 對 BeanFactory 進行了擴充套件與增強,除了擁有 BeanFactory 的所有能力外,還提供了許多高階特性,如事件釋出、資源載入、國際化訊息等。ApplicationContext 介面繼承了 BeanFactory 介面,它的實現類中也是直接複用了 BeanFactory,因此可以說,ApplicationContext 是 BeanFactory 的增強版。

兩者在核心功能上的區別主要是預設的載入策略不同,這點區別幾乎可以忽略不計,通常情況下,我們總是使用更為強大的 ApplicationContext,很少會直接使用 BeanFactory。

以下是幾個最常用的 ApplicationContext 實現類:

  • ClassPathXmlApplicationContext
  • AnnotationConfigApplicationContext
  • AnnotationConfigWebApplicationContext

Spring 容器的基本工作原理

既然是容器,那它最底層肯定是一個資料結構。通過跟蹤 getBean 方法,我們發現它是從一個叫作 singletonObjects 的 Map 集合中獲取 Bean 例項的。singletonObjects 的定義如下:

可以斷定,它就是 Spring 容器的核心,凡是作用域為單例的 Bean 的例項都儲存在該 Map 集合中,我把它稱之為單例池

那麼 getBean 方法做了哪些事情呢?

getBean 方法首先會從單例池中獲取 Bean 例項,如果取到了就直接返回,否則,如果有父容器,嘗試從父容器中獲取,如果也沒獲取到,則建立例項。建立例項之前先確保該 Bean 所依賴的 Bean 全部初始化,然後,如果是原型 Bean,建立好例項後直接返回,如果是單例 Bean,建立好例項後將其放進單例池,然後再從單例池中獲取並返回。

當 Spring 容器被建立時,它又是如何完成初始化的呢?

ClassPathXmlApplicationContext為例,它的構造方法主要做的事情就是呼叫 refresh() 方法。

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // 準備好自己
        prepareRefresh();
        // 建立並初始化BeanFactory
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        // 準備好要使用的BeanFactory
        prepareBeanFactory(beanFactory);
        try {
            // 對BeanFactory進行後置處理
            postProcessBeanFactory(beanFactory);
            // 呼叫BeanFactory的後置處理器
            invokeBeanFactoryPostProcessors(beanFactory);
            // 註冊Bean的後置處理器
            registerBeanPostProcessors(beanFactory);
            // 初始化訊息源
            initMessageSource();
            // 初始化事件多播器
            initApplicationEventMulticaster();
            // 初始化其他特殊的bean
            onRefresh();
            // 檢測並註冊監聽器Bean
            registerListeners();
            // 例項化其餘所有(非懶載入)的單例Bean
            finishBeanFactoryInitialization(beanFactory);
            // 最後一步:釋出相應的事件
            finishRefresh();

refresh() 方法的主要執行流程:

  1. 呼叫 refreshBeanFactory() 方法,該方法會首先新建或重建 BeanFactory 物件,然後使用相應的BeanDefinitionReader 讀取並解析 Bean 定義資訊,將每個 Bean 定義資訊依次封裝成 BeanDefinition 物件,並將這些 BeanDefinition 物件註冊到 BeanDefinitionRegistery。這一步,完成了 BeanFactory 的建立,以及 Bean 定義資訊的載入。
  2. 配置 BeanFactory,對 BeanFactory 做一些後置處理,註冊 Bean 的後置處理器,初始化訊息源和事件多播器,註冊監聽器等。這一步,完成了 Spring 容器的配置工作。
  3. 呼叫 finishBeanFactoryInitialization() 方法,該方法會遍歷之前註冊到 BeanDefinitionRegistery 中的所有 BeanDefinition ,依次例項化那些非抽象非懶載入的單例 Bean,並將其加入單例池。這一步,完成了 Bean 的例項化。

Spring 容器的幾個核心類:

  • DefaultListableBeanFactory 是一個通用的 BeanFactory 實現類,它還同時實現了BeanDefinitionRegistry 介面。從 ApplicationContext 實現類的原始碼中可以看到,在它內部維護著一個 DefaultListableBeanFactory 的例項,所有的 IoC 服務都委託給該 BeanFactory 例項來執行。
  • BeanDefinitionRegistry 負責維護 BeanDefinition 例項,該介面主要定義了registerBeanDefinition()getBeanDefinition() 等方法,用於註冊和獲取 Bean 資訊。
  • BeanDefinition 用於封裝 Bean 的資訊,包括類名、是否是單例Bean、構造方法引數等資訊。每一個 Spring Bean 都會有一個 BeanDefinition 的例項與之相對應。
  • BeanDefinitionReader 負責讀取相應配置檔案中的內容並將其對映到 BeanDefinition ,然後將對映後的 BeanDefinition 例項註冊到 BeanDefinitionRegistry,由 BeanDefinitionRegistry 來保管它們。

Spring Bean 的註冊與裝配

個人理解,註冊與裝配是不同的兩個過程。註冊指的是將 Bean 納入 IoC 容器。裝配指的是建立 Bean 之間的依賴關係。

Bean 的註冊方式有以下三種:

  • 在 XML檔案中配置
  • 在 JavaConfig 中配置
  • 使用@ComponentScan@Component等註解進行配置

Bean 的裝配分為手動裝配和自動裝配。

手動裝配同樣有三種方式:

  • 在 XML檔案 中配置
  • 在 JavaConfig 中配置
  • 使用 @Resource 等註解來配置。
    • 這種方式既可以算作手動裝配,也可以算作自動裝配。當我們在 @Resource 註解中明確指定注入哪一個 Bean 時,我們稱這是手動裝配,而當我們不進行指定時,則算作自動裝配。

自動裝配也稱自動注入,有兩種開啟方式:

  • 開啟粗粒度的自動裝配,即開啟 Bean 的預設自動裝配。在<bean>標籤中配置default-autowire屬性,或在@Bean註解中配置autowire屬性。開啟了預設自動裝配的 Bean,Spring 會對它的全部屬性都嘗試注入值,這種方式不安全,因此很少使用。
  • 開啟細粒度的自動裝配,即在元件類中使用@Autowired等註解對單個屬性開啟自動裝配。

Spring 支援以下四種用於自動裝配的註解:

  • Spring 自帶的 @Autowired 註解
  • JSR-330 的 @Inject 註解
  • JSR-250 的 @Resource 註解
  • Spring 新增的 @Value 註解,用於裝配 String 和基本型別的值。@Value註解經常配合 SpEL 表示式一起使用。

SpEL 表示式的主要語法:

  • ${},表示從 Properties 檔案中讀取相應屬性的值。通常需要同@PropertySource註解配合使用,該註解用於指定從哪個 Properties 檔案中讀取屬性。
  • #{},表示從 Spring Bean 中讀取相應屬性的值。如,#{user1.name}表示從名稱為 user1 的 Bean 中 讀取 name 屬性值。
  • 冒號:用於指定預設值,如${server.port:80}。

Spring Bean 的作用域與生命週期

未完待續

相關文章