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,然後構造/取出物件即可。
總結
不知道大家清晰一點了沒,希望對大家有幫助。後續會視情況,再看看是否分析構造容器的原始碼。