【曹工雜談】Maven IOC 容器– Guice內部有什麼

Google Guice容器內部有什麼

前言

Maven系列,好幾天沒寫了,主要是這幾天被Google Guice卡住了,本來是可以隨便帶過Guice,講講guice的用法就夠了(這個已經講了,在前面的文章),但是,想著guice作為maven的底層IOC容器,對guice的理解深入一些,對後續的Maven源碼學習也會比較有幫助,因此,就在那開始分析guice的源碼。

guice作為一個僅次於Spring的IOC容器,程式碼也不是那麼好懂的,畢竟也迭代了十來年了;程式碼量不少,另外,我感覺程式碼也有點繞,就看得真心有點打瞌睡。

因為下班回來也9點多了,學習的時間也不多,因此,花了好幾天時間來單步debug,有一點點眉目,因此,這裡先分享給大家,等後續理解深入了再補充。

針對Guice的源碼分析法

一般來說,我debug源碼,都是從頭開始,單步debug過去,很多時候,這種IOC框架啥的,啟動非常複雜,一個小時也跟不完一趟;過程冗長,一篇幾千字的文章基本都講不完,讀者也記不住那麼多東西,部落客也很難講清那麼多東西。

我今天也想著換個思路吧,IOC容器,不是分兩個階段嗎,啟動時,一般是準備IOC容器;而運行時,就是去容器拿東西。根據我的發現,一般為了保證運行時足夠快,都會預先把數據準備好,比如,針對singleton類型的實例,都會預先生成(eager-initilization),存放到容器中,就無需運行時再去生成,歸根結底,就是一個空間換時間的方法。

採用這種空間換時間的方法,就會有個問題,就是在數據準備階段(比如容器初始化階段),要做的工作相當多,debug過程也非常長;甚至,有時候準備的很多數據,對於我們的場景,根本用不上。

因此,下面我會先給大家看看,初始化成功後的容器,是什麼樣的;再去簡單分析背後的啟動過程。

簡單demo

一共三個類。


public interface HelloInterface {
    void hello();
}

public class HelloInterfaceImpl implements HelloInterface {
    @Override
    public void hello() {
        System.out.println("hello world");
    }
}

再下邊是啟動類:

這個啟動類,也就是三個部分:

  • 第一個部分,就是配置:HelloInterface這個class,要映射到 HelloInterfaceImpl這個實現類,後續,容器才能根據HelloInterface來new一個HelloInterface的實例出來。
  • 初始化容器
  • 運行時,從容器獲取HelloInterface的對象

容器中有什麼

假設我們跳過初始化容器的階段,不關心容器如何構造,如何啟動,只看:構造好的容器,是什麼樣的。

// 構造容器        
Injector injector = Guice.createInjector(module);

在執行完上面這句後,容器就已經初始化完畢,此時,我們打上斷點,看看容器的內部:

類型

真實類型是:

// Default Injector implementation.
final class InjectorImpl implements Injector, Lookups

從它實現的介面com.google.inject.Injector來看,主要有以下一些核心方法:

// 獲取當前容器內的全部綁定關係
Map<Key<?>, Binding<?>> getBindings();
// 根據key,獲取這個key對應的綁定關係。key其實基本就是一個介面的Class類名
<T> Binding<T> getBinding(Key<T> key);
// 根據class,獲取這個class對應的綁定
<T> Binding<T> getBinding(Class<T> type);

// 根據key,獲取對應的工廠類
<T> Provider<T> getProvider(Key<T> key);
// 根據class,獲取對應的工廠類
<T> Provider<T> getProvider(Class<T> type);

//根據key/class,直接獲取對應的實例
<T> T getInstance(Key<T> key);
<T> T getInstance(Class<T> type);

大家看到這裡,是不是覺得和Spring的容器很像呢?

欄位

  • 父容器

    final InjectorImpl parent;
    

    類似於spring,spring也有父子容器的概念;大體就是,當前容器找不到實例,還可以去父容器找

    我們這個demo里,parent是null

  • 綁定map

    final ListMultimap<TypeLiteral<?>, Binding<?>> bindingsMultimap;
    

    存儲了一些綁定關係,包括了三個默認的綁定,如:容器injector本身、日誌logger、stage。

  • 容器選項

    final InjectorOptions options;
    

    這邊是一些配置項,比如jitdisabled,禁止隱式依賴。禁止後,你要向容器獲取Class X的實例,那麼必須先配置X對應的實例化方式,不會再默認嘗試調用Class X的構造器(如果有的話)

  • 隱式綁定

    final Map<Key<?>, BindingImpl<?>> jitBindings = Maps.newHashMap();
    

    比如我們的這個實現類,就是個隱式綁定,因為我們沒配置如何實例化HelloInterfaceImpl。

  • 構造器快取

      final ConstructorInjectorStore constructors = new ConstructorInjectorStore(this);
    

    比如我們實現類的構造器,就被快取了。

  • 內部狀態:state

    看了以上幾個欄位,感覺也沒有很特別。其實,真正重要的欄位,是下面將出場的這個。

    final State state;
    

    大家看下圖,會發現state下有不少欄位,主要就有:每個class對應的綁定(value就是這個class的實例化方式)、還有我們程式碼里配了個切面也在這裡;基本上,這裡才是真正的容器的各種數據的存放處

    接下來,我們再看看這個綁定關係的map。key就是對應的介面類,value就是說:怎麼去實例化一個這個類型的實例出來,所以呢,guice內部,為了統一,基本把value這部分統一成了一個工廠。如下:

    而工廠類里是什麼樣呢?

    就是包含了對應的實現類的構造器了。

    在真正要找容器獲取這個HelloInterface的實例時,就可以找到HelloInterfaceImpl的構造函數,從而構造一個實例出來。

不同的binding方式,內部不同的工廠類

當我們配置了一個如下的綁定關係時:

binder.bind(String.class).toInstance("xxx");

此時,內部又是什麼樣呢?

這裡,我們發現內部工廠internalFactory的類型,和之前也不太一樣了。同時,下圖可以看見,工廠內部直接存了這個String實例的值。

總之呢,也是保證後續直接就能在容器需要一個String類型實例時,找到「xxx」這個對象返回回去。

從容器中獲取

容器初始化好了,怎麼獲取呢?即如下程式碼怎麼執行呢?

HelloInterface instance = injector.getInstance(HelloInterface.class);

我們稍微跟了下,發現就會走到如下地方,會去查詢state內部的顯示綁定map。

獲取到binding後,即取出internalFactory,然後構造/取出對象即可。

總結

不知道大家清晰一點了沒,希望對大家有幫助。後續會視情況,再看看是否分析構造容器的源碼。