控制反轉 依賴注入 基本概念 與 Spring IOC 源碼學習

控制反轉 依賴注入 基本概念 與 Spring IOC 源碼學習

1. Background

1996年,Michael Mattson在一篇有關探討面向對象框架的文章中,首先提出了IOC 這個概念。IOC是Inversion of Control的縮寫,多數書籍翻譯成“控制反轉”,還有些書籍翻譯成爲“控制反向”或者“控制倒置”。IOC理論提出的觀點大體是這樣的:藉助於“第三方”實現具有依賴關係的對象之間的解耦

2004年,Martin Fowler探討了同一個問題,既然IOC是控制反轉,那麼到底是“哪些方面的控制被反轉了呢?”,經過詳細地分析和論證後,他得出了答案:“獲得依賴對象的過程被反轉了”。控制被反轉之後,獲得依賴對象的過程由自身管理變爲了由IOC容器主動注入。於是,他給“控制反轉”取了一個更合適的名字叫做“依賴注入(Dependency Injection)”。他的這個答案,實際上給出了實現IOC的方法:注入。所謂依賴注入,就是由IOC容器在運行期間,動態地將某種依賴關係注入到對象之中。

所以,依賴注入(DI)和控制反轉(IOC)是從不同的角度的描述的同一件事情,就是指通過引入IOC容器,利用依賴關係注入的方式,實現對象之間的解耦。

IOC中最基本的技術就是“反射(Reflection)”編程,通俗來講就是根據給出的類名(字符串方式)來動態地生成對象。這種編程方式可以讓對象在生成時才決定到底是哪一種對象。

舉個簡單例子:
Person(人)每天都要吃早餐(食物)。我們可以用如下程序表示

public class Person {
    public void eat() {
        Food food = new food();
        System.out.println("I eat food:{}", food.toString());
    }
}

在我們吃飯之前必須先new food()(做飯),要不然就吃不上。
Ioc 會怎麼樣做呢

public class Person {
    private Food food;
 public void eat() {
        System.out.println("I eat food:{}", food.toString());
 }
}

我們可以把IOC容器的工作模式看做是工廠模式的昇華,可以把IOC容器看作是一個工廠,這個工廠裏要生產的對象都在配置文件中給出定義,然後利用編程語言的的反射編程,根據配置文件中給出的類名生成相應的對象。從實現來看,IOC是把以前在工廠方法裏寫死的對象生成代碼,改變爲由配置文件來定義,也就是把工廠和對象生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。

2. IOC實現方式

接下來的問題是如何將依賴的對象準備好呢(依賴注入),常用的有兩種方式:構造方法注入setter注入

  • 構造器注入

    public Person(Food food) {
        this.food = food;
    }
    
  • setter注入

    public void setFood(Food food) {
        this.food = food;
    }
    

3. Spring IOC

Spring IOC的初始化過程:

IOC要實現卻並不那麼容易。它需要一系列技術要實現。首先它需要知道服務的對象是誰,以及需要爲服務對象提供什麼樣的服務。提供的服務指:要完成對象的構建(即把飯做好),將其送到服務對象即完成對象的綁定(即把飯端到我面前)。

IOC需要實現兩個技術:

  • 對象的構建
  • 對象的綁定

對於這兩個方面技術的實現具有很多的方式:硬編碼(Ioc 框架都支持),配置文件(重點),註解(簡潔)。但無論哪種方式都是在Ioc容器裏面實現的(我們可以理解爲一個大池子,裏面躺着各種各樣的對象,並能通過一定的方式將它們聯繫起來)spring提供了兩種類型的容器,一個是BeanFactory,一個是ApplicationContext(可以認爲是BeanFactory的擴展),下面我們將介紹這兩種容器如何實現對對象的管理。

3.1 BeanFactory

如果沒有特殊指定,默認採用延遲初始化策略(lazy-load)。只有當客戶端對象需要訪問容器中的某個受管對象的時候,纔對該受管對象進行初始化以及依賴注入操作。所以,相對來說,容器啓動初期速度較快,所需要的資源有限。對於資源有限,並且功能要求不是很嚴格的場景,BeanFactory是比較合適的 IoC容器選擇。

我們先來看一下BeanFactory類的關係圖(如下所示)

有三個很重要的部分:

  • BeanDefinition 實現Bean的定義(即對象的定義),且完成了對依賴的定義

  • BeanDefinitionRegistry,將定義好的bean,註冊到容器中(此時會生成一個註冊碼)

  • BeanFactory 是一個bean工廠類,從中可以取到任意定義過的bean

最重要的部分就是BeanDefinition,它完成了Bean的生成過程。一般情況下我們都是通過配置文件(xml,properties)的方式對bean進行配置,每種文件都需要實現BeanDefinitionReader,因此是reader本身現了配置文字到bean對象的轉換過程。

Bean 生命週期

Bean的生成大致可以分爲兩個階段:容器啓動階段bean實例化階段

容器啓動階段

  • 加載配置文件(通常是xml文件)

  • 通過reader生成beandefinition

  • beanDefinition註冊到beanDefinitionRegistry

bean實例化階段

當某個bean 被 getBean()調用時, bean需要完成初時化,以及其依賴對象的初始化如果bean本身有回調,還需要調用其相應的回調函數。從上面我們也可以知道,beanDefinition(容器啓動階段)只完成bean的定義,並未完成初始化。初始是通過beanFactory的getBean()時才進行的。

Spring IOC在初始化完成之後,給了我們提供一些方法,讓我們來改變一些bean的定義

  1. org.springframework.beans.factory.config.PropertyPlaceholderConfigurer:使我們可能通過配置文件的形式,配置一些參數
  2. PropertyOverrideConfigurer :則可以覆蓋原本的bean參數
  3. CustomEditorConfigurer:則提供類型轉換支持(配置文件都是string,它需要知道轉換成何種類型)

Bean的初始化過程:

如果你認爲實例化的對象就是通過我們定義的類new出來的就錯了,其實這裏用到了AOP機制,生成了其代理對象(通過反射機制生成接口對象,或者是通過CGLIB生成子對象),具體可以參見在上一篇博客

bean的具體裝載過程是由beanWrapper實現的,它繼承了PropertyAccessor (可以對屬性進行訪問)、PropertyEditorRegistryTypeConverter接口 (實現類型轉換,就上前面說的)。

完成設置對象屬性之後,則會檢查是否實現了Aware類型的接口,如ApplicationContextAware;如果實現了,則主動加載。

BeanPostprocessor 可以幫助完成在初始化bean之前或之後 幫我們完成一些必要工作,比如我們在連接數據庫之前將密碼存放在一個加密文件,當我們連接數據庫之前,需要將密碼進行加載解密。只要實現相應的接口即可, 下面是BeanPostprocessor 接口定義:

public interface BeanPostProcessor {

   /**
    * Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean
    * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
    * or a custom init-method). The bean will already be populated with property values.
    * The returned bean instance may be a wrapper around the original.
    * @param bean the new bean instance
    * @param beanName the name of the bean
    * @return the bean instance to use, either the original or a wrapped one; if
    * {@code null}, no subsequent BeanPostProcessors will be invoked
    * @throws org.springframework.beans.BeansException in case of errors
    * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
    */
   Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

   /**
    * Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean
    * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}
    * or a custom init-method). The bean will already be populated with property values.
    * The returned bean instance may be a wrapper around the original.
    * <p>In case of a FactoryBean, this callback will be invoked for both the FactoryBean
    * instance and the objects created by the FactoryBean (as of Spring 2.0). The
    * post-processor can decide whether to apply to either the FactoryBean or created
    * objects or both through corresponding {@code bean instanceof FactoryBean} checks.
    * <p>This callback will also be invoked after a short-circuiting triggered by a
    * {@link InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation} method,
    * in contrast to all other BeanPostProcessor callbacks.
    * @param bean the new bean instance
    * @param beanName the name of the bean
    * @return the bean instance to use, either the original or a wrapped one; if
    * {@code null}, no subsequent BeanPostProcessors will be invoked
    * @throws org.springframework.beans.BeansException in case of errors
    * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
    * @see org.springframework.beans.factory.FactoryBean
    */
   Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;

}

在完成postProcessor之後,則會看對象是否定義了InitializingBean 接口,如果是,則會調用其afterProper-tiesSet()方法進一步調整對象實例的狀態,這種方式並不常見。spring還提供了另外一種指定初始化的方式,即在bean定義中指定init-method 。

當這一切完成之後,還可以指定對象銷燬的一些回調,比如數據庫的連接池的配置,則銷燬前需要關閉連接等。相應的可以實現DisposableBean 接口或指定destroy-method

3.2 ApplicationContext

ApplicationContextBeanFactory 的基礎上構建,是相對比較高級的容器實現,除了擁有 BeanFactory的所有支持,ApplicationContext還提供了其他高級特性,比如事件發佈、國際化信息支持等。

ApplicationContext 所管理 的對象,在該類型容器啓動之後,默認全部初始化並綁定完成。所以,相對於 BeanFactory來說,ApplicationContext 要求更多的系統資源,同時,因爲在啓動時就完成所有初始化,容器啓動時間較之 BeanFactory 也會長一些。在那些系統資源充足,並且要求更多功能的場景中,ApplicationContext 類型的容器是比較合適的選擇。

具體差異

  1. bean的加載方式
    BeanFactory提供BeanReader來從配置文件中讀取bean配置。相應的ApplicationContext也提供幾個讀取配置文件的方式:

    • FileSystemXmlApplicationContext:該容器從 XML 文件中加載已被定義的 bean。在這裏,你需要提供給構造器 XML 文件的完整路徑

    • ClassPathXmlApplicationContext:該容器從 XML 文件中加載已被定義的 bean。在這裏,你不需要提供 XML 文件的完整路徑,只需正確配置 CLASSPATH 環境變量即可,因爲,容器會從 CLASSPATH 中搜索 bean 配置文件。

    • WebXmlApplicationContext:該容器會在一個 web 應用程序的範圍內加載在 XML 文件中已被定義的 bean。

    • AnnotationConfigApplicationContext

    • ConfigurableWebApplicationContext

  2. ApplicationContext採用的非懶加載方式。

    它會在啓動階段完成所有的初始化,並不會等到getBean()才執行。所以,相對於BeanFactory來說,ApplicationContext要求更多的系統資源,同時,因爲在啓動時就完成所有初始化,容器啓動時間較之BeanFactory也會長一些。在那些系統資源充足,並且要求更多功能的場景中,ApplicationContext類型的容器是比較合適的選擇。

  1. ApplicationContext 還額外增加了三個功能:ApplicationEventPublisher, ResourceLoader, MessageResource,以下爲具體解釋:

3.2.1 ResourceLoader

ResourceLoader並不能將其看成是Spring獨有的功能,spring IOC只是藉助於ResourceLoader來實現資源加載。也提供了各種各樣的資源加載方式:

  • DefaultResourceLoader 首先檢查資源路徑是否以classpath:前綴打頭,如果是,則嘗試構造ClassPathResource類 型資源並返回。否則, 嘗試通過URL,根據資源路徑來定位資源

  • FileSystemResourceLoader 它繼承自Default-ResourceLoader,但覆寫了getResourceByPath(String)方法,使之從文件系統加載資源並以 FileSystemResource類型返回

  • ResourcePatternResolver
    批量查找的ResourceLoader

spring與ResourceLoader之間的關係

所有ApplicationContext的具體實現類都會直接或者間接地實現AbstractApplicationContext, AbstactApplicationContext 依賴了DeffaultResourceLoader, ApplicationContext繼承了ResourcePatternResolver,所到頭來ApplicationContext的具體實現類都會具有DefaultResourceLoaderPathMatchingResourcePatterResolver的功能。這也就是會什麼ApplicationContext可以實現統一資源定位。

3.2.2 ApplicationEventPublisher Spring事件

Spring的文檔對Event的支持翻譯之後描述如下:

ApplicationContext通過ApplicationEvent類和ApplicationListener接口進行事件處理。 如果將實現ApplicationListener接口的bean注入到上下文中,則每次使用ApplicationContext發佈ApplicationEvent時,都會通知該bean。 本質上,這是標準的觀察者設計模式。

  1. ApplicationEvent:繼承自EventObject,同時是spring的application中事件的父類,需要被自定義的事件繼承。

  2. ApplicationListener:繼承自EventListener,spring的application中的監聽器必須實現的接口,需要被自定義的監聽器實現其onApplicationEvent方法

  3. ApplicationEventPublisherAware:在spring的context中希望能發佈事件的類必須實現的接口,該接口中定義了設置ApplicationEventPublisher的方法,由ApplicationContext調用並設置。在自己實現的ApplicationEventPublisherAware子類中,需要有ApplicationEventPublisher屬性的定義。

  4. ApplicationEventPublisher:spring的事件發佈者接口,定義了發佈事件的接口方法publishEvent。因爲ApplicationContext實現了該接口,因此spring的ApplicationContext實例具有發佈事件的功能(publishEvent方法在AbstractApplicationContext中有實現)。在使用的時候,只需要把ApplicationEventPublisher的引用定義到ApplicationEventPublisherAware的實現中,spring容器會完成對ApplicationEventPublisher的注入。

3.2.3 MessageSource

ApplicationContext接口擴展了MessageSource接口,因而提供了消息處理的功能(i18n或者國際化)。與HierarchicalMessageSource一起使用,它還能夠處理嵌套的消息。

當一個ApplicationContext被加載時,它會自動在context中查找已定義爲MessageSource類型的bean。此bean的名稱須爲messageSource。如果找到,那麼所有對上述方法的調用將被委託給該bean。否則ApplicationContext會在其父類中查找是否含有同名的bean。如果有,就把它作爲MessageSource。如果它最終沒有找到任何的消息源,一個空的StaticMessageSource將會被實例化,使它能夠接受上述方法的調用。

Spring目前提供了兩個MessageSource的實現:ResourceBundleMessageSourceStaticMessageSource。它們都繼承NestingMessageSource以便能夠處理嵌套的消息。StaticMessageSource很少被使用,但能以編程的方式向消息源添加消息。ResourceBundleMessageSource會用得更多一些.

4. 舉個栗子

4.1 註解掃描

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="org.spring21"/>
</beans>

4.2 component/service/controller註解

@Component
public class Person {
    @Resource
    private Food food;

    public void setFood(Food food) {
        this.food = food;
    }
}

4.3 bean的前置後置

@Component
public class Person {
    @Resource
    private Food food;

    public setFood(Food food) {
        this.food = food;
    }

    @PostConstruct
    public void wash() {
        System.out.println("飯前洗手");
    }

    @PreDestroy
    public void brush() {
        System.out.println("飯後刷牙");
    }
}

Ref

  1. https://www.cnblogs.com/wang-meng/p/5597490.html
  2. https://javadoop.com/post/spring-ioc
  3. https://yikun.github.io/2015/05/29/Spring-IOC核心源碼學習/
  4. https://juejin.im/post/593386ca2f301e00584f8036
  5. http://blog.sina.com.cn/s/blog_85d71fb70101cyp5.html MessageSource
  6. https://www.cnkirito.moe/event-1/ Spring事件
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章