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

三國夢迴發表於2021-09-16

Google Guice容器內部有什麼

前言

Maven系列,好幾天沒寫了,主要是這幾天被Google Guice卡住了,本來是可以隨便帶過Guice,講講guice的用法就夠了(Maven容器的下半場:Guice,聽說僅次於Spring),但是,想著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,然後構造/取出物件即可。

總結

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

相關文章