1. 程式人生 > >解析 .Net Core 註入——註冊服務

解析 .Net Core 註入——註冊服務

text 代碼 read 不必要 factory 一行代碼 ros bsp 思考

在學習 Asp.Net Core 的過程中,註入可以說是無處不在,對於 .Net Core 來說,它是獨立的一個程序集,沒有復雜的依賴項和配置文件,所以對於學習 Asp.Net Core 源碼的朋友來說,註入作為一個起點非常合適,園子裏確實有許多關於註入的博客,不過 .Net Core2.0 已經出來了,註入這一塊做了一些 更新,其實有不少 .net 開發人員對微軟改來改去這一點不是很滿意,加大了學習成本,其實改動分為兩種,一種是 Asp.Net Core Mvc 常用 Api 接口的更改(或者配置的更改),這點在 2.0 以來很少有這樣的情況了,也就是說 Asp.Net Core Mvc 基本趨於穩定了,另一類就是對代碼的優化,前者對研發的跟進造成了很大的傷害值,而後者對於研發而言無關緊要,對於樂於學習源碼的程序員而言或許能從中帶來許多思考。   所以我打算重新分析 .Net Core2.0 的註入 ,實際發布版本為 .netstandard2.0 程序集為 Microsoft.Extensions.DependencyInjection.dll。   在 .Net Core 中,註入描述為為三個過程,註冊服務->創建容器->創建對象,所以我也會分為三個模塊來介紹 技術分享圖片
  註入元數據   如果接觸過 .Net Core 則或多或少已經接觸過註入,下面的代碼註冊了具有三種生命周期的服務,然後創建一個容器,最後使用容器提供這三個服務的實例對象,我們觀察他們的生命周期,看到輸出結果基本對 AddTransient 以及 AddSingleton 這兩種方式註冊的服務具有怎樣的生命周期都會有所判斷,而 AddScoped 方式註冊的服務就復雜一點。   我們看到通過 BuilderServiceProvider 方法創建了一個容器,而容器調用 CreateScope 就可以創建了兩個具有範圍的容器,而 AddScoped 方式註冊的服務在不同範圍內的生命周期是不一樣的,而相同範圍下的生命周期和 AddSingleton 是一致的。
  interface ITransient { }   class Transient : ITransient { }   interface ISingleton { }   class Singleton : ISingleton { }   interface IScoped { }   class Scoped : IScoped { }   class Program   {    static void Main(string[] args)    {    IServiceCollection services = new ServiceCollection();    services = services.AddTransient<ITransient, Transient>();    services = services.AddScoped<IScoped, Scoped>();    services = services.AddSingleton<ISingleton, Singleton>();    IServiceProvider serviceProvider = services.BuildServiceProvider();       Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ITransient>(), serviceProvider.GetService<ITransient>()));    Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IScoped>(), serviceProvider.GetService<IScoped>()));    Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ISingleton>(), serviceProvider.GetService<ISingleton>()));    IServiceProvider serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;    IServiceProvider serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;    Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider1.GetService<IScoped>()));    Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider2.GetService<IScoped>()));    Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<ISingleton>(), serviceProvider2.GetService<ISingleton>()));    /* False    * True    * True    * True    * False    * True    */    }   }
  IServiceCollection
  public interface IServiceCollection : IList<ServiceDescriptor>   {   }
  是一個集合,用來存放用戶註冊的服務元數據   ServiceDescriptor   看上面的例子我們如何添加註入應該也能猜到 ServiceDescriptor 包含哪些屬性了吧!至少包含一個接口類型、實現類型和生命周期,是的就是如此。
  public class ServiceDescriptor   {    public ServiceLifetime Lifetime { get; }    public Type ServiceType { get; }    public Type ImplementationType { get; }    public object ImplementationInstance { get; }    public Func<IServiceProvider, object> ImplementationFactory { get; }   }
  在第一個代碼塊中,都是使用的是 IServiceCollection 如下簽名拓展方法註冊服務的,這裏我把它稱為“服務類型實例類型”(提供一個服務類型,一個實例類型)的註冊方式,相應的服務類型和實例類型通過解析泛型參數傳遞給 ServiceDescriptor 的ServiceType、ImplementationInstance,值得註意的是,創建 ServiceDescriptor 並不會校驗實例類型的可創建性(驗證其是否是抽象類,接口)
  public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)    where TService : class    where TImplementation : class, TService   {    if (services == null)    {    throw new ArgumentNullException(nameof(services));    }    return services.AddTransient(typeof(TService), typeof(TImplementation));   }
  此外,微軟還提供了“服務實例”(提供一個服務類型,一個實例對象)以及“服務實例工廠”(提供一個服務類型,一個實例對象工廠)的註冊方式,前者只供單例服務使用,使用起來也很簡單   services.AddTransient<ITransient>(_=>new Transient());   services.AddSingleton<ISingleton>(new Singleton());   關於 ServiceDescriptor,還有一個要說的就是服務的生命周期了,使用 AddSingleton、AddScoped、AddTransient 三種方式註冊的服務在 ServiceDescriptor 中的 LifeTime 屬性分別對應下面這個枚舉類型
  public enum ServiceLifetime   {    Singleton,    Scoped,    Transient   }
  1、Transient:每次從容器 (IServiceProvider)中獲取的時候都是一個新的實例   2、Singleton:每次從同根容器中(同根 IServiceProvider)獲取的時候都是同一個實例   3、Scoped:每次從同一個容器中獲取的實例是相同的、   關於服務的生命周期,如果還不清楚也沒關系,因為接下來會不斷的學習它   自定義創建容器和創建對象的過程   在文章的開頭就介紹了該註入框架的三個過程,註冊服務->創建容器->創建對象,然而註冊服務的步驟是非常簡單的,將一個個類似 AddTransient、AddSingleton 的方法提供的泛型參數或者實參轉換成一個 ServiceDescriptor 對象存儲在 IServiceCollection 中,而創建容器和床對象是否也是這樣簡單呢?如果是,想必很容易寫出下面的代碼
  public class MyServiceProvider : IServiceProvider   {    private List<ServiceDescriptor> serviceDescriptors = new List<ServiceDescriptor>();    private Dictionary<Type, object> SingletonServices = new Dictionary<Type, object>();    public MyServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors)    {    this.serviceDescriptors.AddRange(serviceDescriptors);    }    public object GetService(Type serviceType)    {    var descriptor = serviceDescriptors.FirstOrDefault(t => t.ServiceType == serviceType);    if(descriptor == null)    {    throw new Exception($"服務‘{serviceType.Name}’未註冊");    }    else    {    switch (descriptor.Lifetime)    {    case ServiceLifetime.Singleton:    if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))    {    return obj;    }    else    {    var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);    SingletonServices.Add(descriptor.ServiceType, singletonObject);    return singletonObject;    }    case ServiceLifetime.Scoped:    throw new NotSupportedException($"創建失敗,暫時不支持 Scoped");    case ServiceLifetime.Transient:    var transientObject = Activator.CreateInstance(descriptor.ImplementationType);    return transientObject;    default:    throw new NotSupportedException("創建失敗,不能識別的 LifeTime");    }    }    }   }   public static class ServiceCollectionContainerBuilderExtensions   {public static MyServiceProvider BuildeMyServiceProvider(this IServiceCollection services)    {    return new MyServiceProvider(services);    }   }
  由於 Scoped 的特殊性,部分人寫到這裏就戛然而止了,然而還有一個問題,我們知道註冊服務的時候可能采取多種方式,這裏只給出了"服務實例類型"的情形,稍作修改
  case ServiceLifetime.Singleton:    if (SingletonServices.TryGetValue(descriptor.ServiceType,out var obj))    {    return obj;    }    else    {    if(descriptor.ImplementationType != null)    {    var singletonObject = Activator.CreateInstance(descriptor.ImplementationType);    SingletonServices.Add(descriptor.ServiceType, singletonObject);    return singletonObject;    }    else if(descriptor.ImplementationInstance != null)    {    SingletonServices.Add(descriptor.ServiceType, descriptor.ImplementationInstance);    return descriptor.ImplementationInstance;    }    else if(descriptor.ImplementationFactory != null)    {    var singletonObject = descriptor.ImplementationFactory.Invoke(this);    SingletonServices.Add(descriptor.ServiceType, singletonObject);    return singletonObject;    }    else    {    throw new Exception("創建服務失敗,無法找到實例類型或實例");    }    }
  雖然這裏只重寫了 Singleton 方式,但是其他的也應如此,實際上可以一直這麽寫下去,但是作為 C# 開發者就顯得有些不優雅,因為這是面向過程(或者說是基於對象)的開開發模式   此外,微軟的註入是不支持屬性註入的,但是別忘了,仍然是支持構造函數註入的,要不然這個註入那也太雞助了吧!是的,按照上述的代碼段我們可以繼續寫下去,在解析出實例類型的時候,我們找到它的構造函數,找到構造函數的所有參數,以同樣的方式創建參數的實例,這是一個遞歸的過程,最後回調,仍然可以創建我們需要的對象,但是這一切如何健壯、優雅的實現呢?這就是學習源碼原因所在吧!   微軟是如何進一步處理元數據的?   其實上面的代碼最主要的問題就是創建容器和創建對象這兩個過程過度耦合了,並且存在一個最大的問題,仔細想想每次創建對象的時候都要去翻一遍 ServiceDescriptor 判斷它是以“服務實例類型”、“服務實例對象”、“服務實例對象工廠”中的哪種方式註冊的,這樣就進行了一些不必要的性能消耗,然而這個工作微軟是在創建容器的時候完成的。跟隨著創建容器的過程我們義無反顧的向源碼走去!去哪?尋找微軟和如何處理 ServiceDescriptor 的!   這裏我們遇到的第一個攔路虎就是 ServiceProvider,我們創建的容器最終就是一個這樣的類型,看看它是如何創建對象的?
  public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback   {    private readonly IServiceProviderEngine _engine;    internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)    {    //此處省略了一些代碼    switch (options.Mode)    {    case ServiceProviderMode.Dynamic:    _engine = new DynamicServiceProviderEngine(serviceDescriptors, callback);    break;    //此處省略了一些代碼    default:    throw new ArgumentOutOfRangeException(nameof(options.Mode));    }    }    public object GetService(Type serviceType) => _engine.GetService(serviceType);    public void Dispose() => _engine.Dispose();   }
  這裏我們知道,最終提供對象並非 ServiceProvide,而是它的一個字段 _engine 類型為 IServiceProviderEngine,在 switch 語句中,我只貼出了 Dynamic 這個分支的代碼,因為該枚舉變量 options 的默認值總是 Dynamic,這裏我們僅僅需要知道 ServiceProvider 中提供對象的核心是一個 ServiceProviderEngine,並且它的默認實例是一個 DynamicServiceProviderEngine,因為這次探險我們是去分析微軟是如何處理元數據的。這一切肯定在 DynamicServiceProviderEngine 創建過程中完成,所以我們只管尋找它的構造函數,終於,我們在父類 ServiceProviderEngine 找到了!
  internal abstract class ServiceProviderEngine : IServiceProviderEngine, IServiceScopeFactory   {    internal CallSiteFactory CallSiteFactory { get; }    protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptors, IServiceProviderEngineCallback callback)    {    //省略了一些代碼    CallSiteFactory = new CallSiteFactory(serviceDescriptors);    CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());    CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());    }   }
  CallSiteFactory   這裏只貼出了該類中三個字段,然而該類型也只有該三個字段,如果這三個字段具體的作用理解了,那麽對於微軟如何處理元數據這一問題也就知道答案了
  internal class CallSiteFactory   {    private readonly List<ServiceDescriptor> _descriptors;    private readonly Dictionary<Type, IServiceCallSite> _callSiteCache = new Dictionary<Type, IServiceCallSite>();    private readonly Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup = new Dictionary<Type, ServiceDescriptorCacheItem>();    private struct ServiceDescriptorCacheItem    {    private ServiceDescriptor _item;    private List<ServiceDescriptor> _items;    //省略了一些代碼    }   }   internal interface IServiceCallSite   {    Type ServiceType { get; }    Type ImplementationType { get; }   }
   第一個字段 _descriptors 是一個元數據集合,我們註冊的服務都在這裏,然後我們看第三個字段 _descriptorLookup,因為註冊服務的時候第一沒有驗證實例類型的有效性(接口,抽象類等),此外我們可以針對同一個服務進行多冊註冊,對於多次註冊的服務微軟又是如何確定創建的對象呢?這對這些問題,微軟設計了一個類概括了具體一個服務的所有註冊的實例類型 ServiceDescriptorCacheItem,具體針對一個服務,第一次註冊的元數據存在 _item 中,後續該服務的所有元數據都存在 _items,而默認的總是認同最後一個元數據。最後最難理解的就是 _callSiteCache 這個字段了,簡單的說,它的值 IServiceCallSite 是創建服務實例的依據,包含了服務類型和實例類型。我們知道從 _descriptorLookup 獲取的是確定的實例類型,然而這個實例類型的構造函數中的類型如何創建呢,這些都在 IServiceCallSite 中體現,既然說 IServiceCallSite 是創建實例的依據,通過觀察這個接口的定義發現也並沒有和生命周期相關的屬性,有點失望!   我們回到創建 ServiceProviderEngine 創建 CallSiteFactory 的那一行代碼,在創建CallSiteFactory 完成後,它調用了 Add 方法添加了兩個鍵值對。第一行代碼的鍵是啥? IServiceProvider,是的微軟默認的允許 IServiceProvider 提供自己!   CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());   CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());   可以看到 Add 添加的鍵值對是存儲在 _callSiteCache 中的
  public void Add(Type type, IServiceCallSite serviceCallSite)   {    _callSiteCache[type] = serviceCallSite;   }
  接著我們觀察 ServiceProviderCallSite、ServiceScopeFactoryCallSite 這兩個類型,出了增加了兩個不認識的類型,並沒有其他收獲
  internal class ServiceProviderCallSite : IServiceCallSite   {    public Type ServiceType { get; } = typeof(IServiceProvider);    public Type ImplementationType { get; } = typeof(ServiceProvider);   }   internal class ServiceScopeFactoryCallSite : IServiceCallSite   {    public Type ServiceType { get; } = typeof(IServiceScopeFactory);    public Type ImplementationType { get; } = typeof(ServiceProviderEngine);   }
  關於註入的一些猜想   從上述的學習我們有了一個較為意外的收獲,IServiceProvider 是可以提供自己的,這不得不使我們猜想,IServiceProvider 具有怎樣的生命周期?如果不斷的用一個 IServiceProvider 創建一個新的,如此下去,又是如何?
  static void Main(string[] args)   {    IServiceCollection services = new ServiceCollection();    var serviceProvider = services.BuildServiceProvider();    Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IServiceProvider>(), serviceProvider.GetService<IServiceProvider>()));    var serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;    var serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;    Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IServiceProvider>(), serviceProvider2.GetService<IServiceProvider>()));    var serviceProvider3 = serviceProvider.GetService<IServiceProvider>();    var serviceProvider4 = serviceProvider.GetService<IServiceProvider>();    var serviceProvider3_1 = serviceProvider3.GetService<IServiceProvider>();    var serviceProvider4_1 = serviceProvider4.GetService<IServiceProvider>();    Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider4));    Console.WriteLine(ReferenceEquals(serviceProvider3_1, serviceProvider4_1));    Console.WriteLine(ReferenceEquals(serviceProvider3, serviceProvider3_1));    Console.WriteLine(ReferenceEquals(serviceProvider3,serviceProvider));    /* True    * False    * True    * True    * True    * False    */   }
  這裏對 CreateScope 我們僅需要知道它創建的是一個具有限定範圍的容器即可,我們根據第一個輸出結果為 True 和第二個輸出結果為 False,從這點看 IServiceProvider 的生命周期和 Scoped 的定義一致,但是由於 IServiceProvider 的特殊性,它可以一直不斷的創建自己,並且他們都是同一個對象,但是和最初的 ServiceProvider 都不一樣。這讓我們又懷疑 IServiceProvider 究竟是不是 Scoped。   小結   這一節主要介紹了服務的三種生命周期,以及服務是如何註冊到元數據的,並且在創建容器的過程中,我們知道了微軟是如何進一步處理元數據的,以及創建實例對象的最終依據是 IServiceCallSite,但是想要真正的搞明白 IServiceCallSite 還必須詳細的了解創建容器和創建實例的過程。

解析 .Net Core 註入——註冊服務