.netcore入門26:asp.net core源碼分析之依賴注入

環境:

  • .netcore 3.1
  • vs2019 16.5.1

一、依賴注入說明

依賴注入的概念這裏就不說了,隨便百度一大堆。在aspnetcore框架中,依賴注入是一個很基礎的東西,所以我們必須要很熟悉才行。但現在我們要講的依賴注入不在aspnetcore框架中,而是在.netcore中,因爲依賴注入相關的代碼都是在包:Microsoft.Extensions.DependencyInjection包:Microsoft.Extensions.DependencyInjection.Abstractions中的,aspnetcore框架只不過做了集成和一點擴展。

二、.netcore框架中的依賴注入特點

市面上有很多依賴注入框架(比如:spring、autofac、tinyioc等),他們都有共同的目的(即:解耦),但他們又都有自己的特點。.netcore中的依賴注入框架的特點是:簡單輕量、僅支持構造函數注入

2.1 簡單輕量

在.netcore控制檯工程中,你只需要安裝nuget包Microsoft.Extensions.DependencyInjection即可使用依賴注入,而這個包又僅依賴於包Microsoft.Extensions.DependencyInjection.Abstractions,這兩個包的代碼量非常少,所以比較輕量,看下圖:
在這裏插入圖片描述

2.2 僅支持構造函數注入

不像其他的框架(比如:spring、autofac)支持字段、屬性等多種方式注入,.netcore的依賴注入框架僅支持構造函數注入,我認爲:這種注入方式雖然單一,但它能提高我們代碼的可讀性。
看下面的示例,當我們需要容器內的什麼服務時,直接在構造函數中聲明即可:
在這裏插入圖片描述

三、服務的生命週期

在說服務的生命週期之前先來探討一個問題:當我們向依賴容器請求一個服務實例的時候,容器是新創建一個返回給我們?還是使用上次創建的實例返回給我們?
答:容器需要兼容這幾種情況,不能只考慮單一的情況。
所以,依賴容器就有了服務生命週期的概念,即:在向容器中註冊服務的時候,我們就應該聲明這個服務的生命週期,這樣當我們向容器請求服務實例的時候,容器就可以根據服務註冊時指定的生命週期採取不同的策略了。

依賴容器中的服務有三類生命週期:

  • Singleton(容器內單例)
    全容器內唯一,即:無論你何時向容器中請求這個服務實例,容器返回的都是同一個。
  • Scoped(範圍內單例)
    範圍內唯一。首先說下什麼是範圍:比如一次http請求,定時任務的一次調用執行等等。範圍內唯一就是說在這個範圍內,我們向容器請求服務實例,容器返回的總是同一個。不同的範圍可以有不同的服務實例。
  • Transient(瞬時,非單例)
    Transient是瞬時的,即:當我們任何時候向依賴容器請求服務實例的時候,容器都會新創建一個返回給我們。

依賴服務容器生命週期的實現原理:
我們可以根據一個依賴容器創建一個新的依賴容器(var scopedProvider = provider.CreateScope().ServiceProvider;),這樣就可以由一個依賴容器衍生出很多依賴容器,但是只能有一個根容器(也就是使用ServiceCollection創建的依賴容器),其他的容器都是平級的,它們的關係如下圖所示:
在這裏插入圖片描述
從上圖我們應該能猜到服務生命週期的實現原理了,簡單來說:

  • 當我們向容器請求一個Singleton服務的時候,依賴容器從根容器中查找有沒有已經創建好的實例,如果沒有的話就新創建一個實例返回給我們並同時保存到根容器中。
  • 當我們向容器中請求一個Scoped服務的時候,依賴容器查找自己有沒有已經創建好的實例,如果沒有的話就新創建一個實例返回給我們並同時自己保存一份。
  • 當我們向容器請求一個Transient服務的時候,依賴容器直接新創建一個實例返回給我們(不做任何緩存)。

講到這裏,我們來思考1個問題:能不能從根容器中解析出Scoped類型的服務?

  • 認爲可以,此時根容器類似於其他的容器,它在自己內部緩存Scoped服務的實例。
  • 認爲不可以,此時根容器只能緩存Singleton服務實例,而Scoped服務只能由非根容器緩存,這也間接導致了Singleton服務不能依賴Scoped服務。

上面說的兩種情況都有道理,下面的更嚴格一些,但.netcore的依賴注入框架默認是第一種情況,即:認爲可以。我們可以在Build的時候改變這個默認:

static void Main(string[] args)
{
   var services = new ServiceCollection();
   services.AddScoped<Person>();
   var provider = services.BuildServiceProvider(new ServiceProviderOptions()
   {
       ValidateOnBuild = true,
       ValidateScopes = true
   });
}

上面的ValidateScopes表示的就是是否驗證Scoped服務必須從非根容器中解析出來,默認是false,這裏我們將它改爲true。ValidateOnBuild表示我們是否在容器Build的時候就進行驗證還是解析服務實例的時候再驗證?默認是false,這裏我們也設置爲true,一旦設置爲true後,容器在Build的時候就會檢查是否有Singleton服務依賴了Scoped服務,如果有的話就直接報異常(因爲Singleton服務依賴Scoped服務必然要引發根容器解析Scoped服務實例,除非你一直不向容器請求這個Singleton服務實例)。

在Build時驗證失敗的案例:
在Build服務容器的時候檢查Singleton服務不能依賴Scoped服務: 在這裏插入圖片描述
在解析服務實例時驗證失敗的案例:
在解析Singleton服務實例的時候檢查是否依賴到了Scoped服務:在這裏插入圖片描述

在解析Scoped服務實例時檢查是否是從根容器中解析的:在這裏插入圖片描述
依賴注入的正確使用方式:
我們應儘可能的避免從根容器中解析Scoped服務,即遵循嚴格模式(ValidateScopes=true),代碼示例如下:

namespace ConsoleApp7
{
    public class Teacher { }
    public class Book { }
    public class Stucent
    {
        public readonly Teacher teacher;
        public readonly Book book;
        public Stucent(Teacher teacher, Book book)
        {
            this.teacher = teacher;
            this.book = book;
        }
    }
    class Program
    {
        public static void Main(string[] args)
        {
            var services = new ServiceCollection();
            services.AddSingleton<Teacher>();
            services.AddSingleton<Book>();
            services.AddScoped<Stucent>();

            var provider = services.BuildServiceProvider(new ServiceProviderOptions()
            {
                ValidateScopes = true,
                ValidateOnBuild = true
            });
            var teacher = provider.GetService<Teacher>();
            var book = provider.GetService<Book>();

            var scopedProvider = provider.CreateScope().ServiceProvider;
            var student = scopedProvider.GetService<Stucent>();

            Console.WriteLine($"student.teacher==teacher=>{student.teacher == teacher}\r\nstudent.book==book=>{student.book == book}");

            Console.WriteLine("ok");
            Console.ReadLine();
        }
    }
}

運行效果如下:
在這裏插入圖片描述

四、循環依賴問題

這裏我們思考一個問題,如果我們向容器中註冊的服務存在循環依賴,那麼當我們請求容器解析的時候容器會怎麼辦?
比如我們定義了三個服務:Teacher、Book、Student,然而它們相互依賴如下:
在這裏插入圖片描述
那麼當我們請求服務實例的時候,容器肯定解析不了,直接給我們報個異常! 看下面的代碼:

namespace ConsoleApp7
{
   public class Teacher
   {
       public Teacher(Book book) { }
   }
   public class Stucent
   {
       public Stucent(Teacher teacher) { }
   }
   public class Book
   {
       public Book(Stucent stucent) { }
   }
   class Program
   {
       public static void Main(string[] args)
       {
           var services = new ServiceCollection();
           services.AddSingleton<Teacher>();
           services.AddSingleton<Stucent>();
           services.AddSingleton<Book>();

           var provider = services.BuildServiceProvider(new ServiceProviderOptions()
           {
               ValidateScopes = true
           });
           provider.GetService<Teacher>();
           Console.WriteLine("ok");
           Console.ReadLine();
       }
   }
}

運行報異常:
在這裏插入圖片描述
異常的原因很明顯,產生了循環依賴。

正因爲我們註冊的服務可能存在循環依賴的問題,所以容器在初次解析某個服務實例時都會對這個服務的依賴鏈進行檢查和判斷,以防止再實例化過程中出現死循環的問題。
看下面的圖所示,依賴關係雖然複雜,但是沒有產生循環依賴:
在這裏插入圖片描述
再看下面的圖所示,依賴關係雖然簡單,但是卻產生了循環依賴:
在這裏插入圖片描述

五、依賴容器對象

從上面的分析中我們知道了依賴容器有根容器和非根容器之說,那麼根容器是哪個類表示的?非根容器又是哪個類表示的?他們是同一個類嗎?
答:按照正常的理解,它們既然都是依賴容器,應該是同一個類。但事實上他們是不同的類:根容器使用類ServiceProvider(public sealed class ServiceProvider),非根容器使用類ServiceProviderEngineScope(internal class ServiceProviderEngineScope),他們都實現了IServiceProvider接口。
我們知道將服務注入到容器後,我們就可以在需要的時候向容器請求服務實例了,但是我們能不能向容器請求容器自身呢?看下面的代碼:
在這裏插入圖片描述
看上面的代碼,Student類根本不需要在構造函數中聲明具體的服務依賴,它只需要引用依賴容器本身就行了,至於其他的服務,隨時需要隨時向容器請求就行了。首先,這種方式很靈活,其次,我們儘量不要使用這種方式!!!
下面我們結合provider.Get<IServiceProvider>()provider.CreateScope().ServiceProvider來看一下根容器和非根容器的區別:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章