ASP.NET Core 原始碼閱讀之 Microsoft.Extensions.DependencyInjection

發表於2016-06-12

這篇隨筆主要記錄一下ASP.NET Core團隊實現預設的依賴注入容器的過程,我的理解可能並不是正確的。

DependencyInjection這個專案不大,但卻是整個ASP.NET Core的基礎,因為它提供了依賴注入(DI)容器的預設實現,而依賴注入貫穿整個ASP.NET Core。相關原始碼可以去GitHub AspNet 上下載。

要實現是一個依賴注入容器,主要是實現它新增依賴、描述依賴、儲存依賴和解析依賴的能力,可以分別用Add(A), Describe(D), Store(S), Resolve(R)表示。從功能的角度來講,分別對應著ServiceCollection,ServiceDescriptor,Service,ServiceEntry,ServiceTable,ServiceProvider,以及CallSite相關的類。

對於框架使用者來說,註冊一項服務最自然的方式就是提供服務的介面和實現這個介面的服務例項,比如IEmail是使用者需求的服務,而Outlook類就是服務的例項型別,用這兩種資訊註冊一項服務是最自然的。所以ASP.NET Core團隊提供了ServiceDescriptor型別來提供對服務的描述功能。

可以看到ServiceDescriptor已經儲存了服務的型別資訊以及生命週期,貌似已經可以憑藉著Dictionary儲存所有的服務關係了。但有個問題,如果同一個服務註冊了多個服務例項型別怎麼辦?比如IEmail服務同時註冊Outlook和GMail,該怎麼儲存,解析的時候又該用哪個?為了解決這個問題,ASP.NET Core團隊提供了Service和ServiceEntry。不要以為Service是非常牛逼的類,其實它非常簡單,Service就是一個儲存ServiceDescriptor的單向連結串列節點,而ServiceEntry就是以Service為節點的單向連結串列。

從上面的原始碼可以看出Service類和ServiceEntry類就是一個典型的連結串列節點和連結串列的關係,Service類中還有一個很重要的方法是CreateCallSite(),這是每個實現了IService的介面都要實現的方法。至於什麼是callsite,之後會說到。

用ServiceEntry解決了一個服務的儲存問題,自然一堆服務的儲存就是用ServiceTable來儲存。ServiceTable使用雜湊表作為底層容器,以ServiceType為Key,ServiceEntry為Value儲存在Dictionary中。為了優化儲存結構,快取一些已經實現過的服務,ServiceTable還新增了關於RealizedService的欄位和方法。主要原始碼見下面:

以上就是ASP.NET Core服務儲存的相關過程,就實現來說,還是比較簡單的,就是以K/V的形式,按照服務的類別儲存實現了服務的相應型別(普通類,泛型類,委託等)。

仔細觀察這些型別,你會發現它們都是internal級別的,那哪個才是公開型別呢?答案是ServiceCollection,這個類和Service一樣,看著很重要,其實就是一個ServiceDescriptor的List,因為它實現的介面繼承了IList。

ServiceCollection本質上是一個ServiceDescriptor的List,回憶一下,ServiceTable的建構函式正需要這樣的型別啊!那個這兩個類又有什麼關係,解開這個謎題的關鍵在於這整個解決方案真正的主角:ServiceProvider。我在這之前遲遲沒有提到一個依賴注入最關鍵的功能:解析依賴。對於一個服務A來說,它可能並不是獨立的,它還在依賴服務B和服務C,而服務B又依賴服務D和服務E。。。一個合格的容器得再我們需要服務A時,能夠正確的解析這個依賴鏈,並按照正確的順序例項化並返回服務A。ServiceProvider是ASP.NET Core團隊提供的預設的依賴注入容器。

首先需要注意的是,它有一個ServiceTable型別的欄位,所以一個ServiceProvider不僅是一個解析器,而且是一個容器,是一個依賴注入容器。第二點,仔細觀察它的建構函式,你會發現它向table欄位中新增了三個服務,而且這三個服務是自新增的,每個ServiceProvider都有。再研究一下這些服務的名字,更加有意思,ServiceProviderService!!也就是說ServiceProvider也是一種服務,解析服務也是一種服務,容器也是一種服務。這意味著我們可以使用其他依賴注入容器。第三點,也是最重要的一點,這個Service,RealizedService,ResolvedService以及我們一直避而不談的callsite究竟是啥?

當我們以型別的方式描述一種服務時,它就是所謂的Service,這時它的資訊全部以後設資料的方式儲存。

每一個Service都有一個CreateCallSite方法,所謂callsite,直接翻譯是“呼叫點”,但更好的理解方式我覺得是後設資料和服務例項之間的橋樑,而如果一種Service後設資料變成了Func委託,我們就把它稱為RealizedService,在Provider的table裡面,有這麼一個欄位專門管理RealizedService。那Func委託又怎麼理解呢?這種委託可以看作是服務的兌換券,它還不是解析的服務,但是離它很近了!因為只要把ServiceProvider傳進去,我們就能得到解析過的Service。

如果把Func委託當成兌換券,那麼ServiceProvider就是兌換人,把兌換券拿給兌換人,我們就能得到object型別的服務,這種服務稱之為ResolvedService,在ServiceProvider中專門有一個欄位快取這些解析過的服務。callsite的Invoke(provider)方法得到一個服務例項(Resolved),而callsite的Build().Complie()方式可以得到Func委託(Realized)。

—————————————————————————————————————–

總結一下整個流程:

  1. 當我們註冊一個服務時,最自然是通過它的型別和它的實現型別來註冊,比如IEmail型別和Outlook型別,所以要用到ServiceDescriptor;
  2. ServiceDescriptor包裝一下,搖身一變成為Service,並且得到了一個關鍵方法CreateCallSite();
  3. 為什麼要callsite這種東西,主要是為了配合Provider管理服務的生命週期,以及實現一些特殊的解析服務的功能。如上所述,callsite的Invoke()得到ResolvedService,callsite的Build()方法得到RealizedService;
  4. 由Provider根據生命週期負責回收服務。

相關文章