.Net7 源码浅析之默认容器实现

182 阅读7分钟

.Net7 源码浅析之默认容器实现

由零开始直接从程序启动入口Startup类开查看对应的代码(dotnet/aspnetcore: ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux. (github.com))

在WebHost初始化完成后 调用Build时候 执行BuildCommonServices开始程序容器的初始化和构建过程并返回IServiceCollection image.png image-20230912213933079.png

查看IServiceCollection具体实现的ServiceCollection对象内部是什么(以下代码均在[dotnet/runtime: .NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps. (github.com)](dotnet runtime)库)

image-20230920202115583.png

image-20230920202129793.png

image-20230920202201393.png 发现平时使用的容器服务增减都是基于对内部List属性的一系列封装,不由得大拍大腿!!怎得如此简单? 接下来我们看看ServiceDescriptor是何方神圣?

image-20230920201938126.png 查看ServiceDescriptor四个构造参数,serviceType表示需要注册服务的类型,implementationType注册服务对应实例类型,lifetime注册服务实例生命周期,Func<IServiceProvider, object> factory 支持传入一个委托工厂用来注册服务对应的实例;

这里出现贯穿整个.Net Core容器注册的服务生命周期枚举 ServiceLifetime(Singleton,Scoped,Transient)

结合ServiceCollection内对应的List便可知。.Net Core默认容器就是把注册的服务Type,注册服务类型实例的ImplementationType和注册服务的生命周期保存在类型为ServiceDescriptor数据列表中,然后在ServiceCollection类中通过Insert,RemoveAt,Remove,Contains,Clear一系列的方法来增加获取或者删除对应注册服务Type的数据。结合这部分代码来看其实不难整个源码这部分的内容比较简单就是一个容器存储一系列不同生命周期类型数据然后提供增删改。

可能还有以下几个疑问需要继续往下梳理

不同生命周期的服务实例原理是什么呢?

在了解不同生命周期的服务实例原理前提下 需要熟悉一下以下的类和其职责:

​初始化ServiceProvider时候默认提供了CreateServiceAccessor方法,在调用获取实例GetService方法的实现中,为每一个实例对象缓存了一个Func<ServiceProviderEngineScope, object?>的委托,用以获取当前注入对象获取实例的时间方法。

image-20231007205110577.png 从这里可以看到实际获取注入的实例对象就是通过执行Func<ServiceProviderEngineScope, object?>的委托来获取?

image-20231008204053953.png 执行GetCallSite获取ServiceCallSite对象经历了什么? 在调用ServiceCallSite的GetCallSite的方法通过缓存获取所需的ServiceCallSite对象如果不存在则再调用CreateCallSite来新建并添加进缓存

​ CreateCallSite中为每一个注入的实例都添加了缓存对象锁,依次通过TryCreateExact(精确获取实例对象一般是实例直接注入,或者泛型注入,或者工厂方法注入),TryCreateOpenGeneric(获取注入的泛型对象),TryCreateEnumerable(获取注入的集合对象)

image-20231007213000432.png TryCreateExact()方法的内部实现是根据注入的获取实例方法不一致分别执行各自逻辑,

image-20231010211610724.png 来看不同生命周期实例维度构建缓存key内部构造函数的构由此可见不能服务生命周期对应不同的CallSiteResultCacheLocation类型

 public ResultCache(ServiceLifetime lifetime, Type? type, int slot)
 {
     Debug.Assert(lifetime == ServiceLifetime.Transient || type != null);

     switch (lifetime)
     {
         case ServiceLifetime.Singleton:
             Location = CallSiteResultCacheLocation.Root;
             break;
         case ServiceLifetime.Scoped:
             Location = CallSiteResultCacheLocation.Scope;
             break;
         case ServiceLifetime.Transient:
             Location = CallSiteResultCacheLocation.Dispose;
             break;
         default:
             Location = CallSiteResultCacheLocation.None;
             break;
     }
     Key = new ServiceCacheKey(type, slot);
 }

image-20231010213747969.png 深入解析例如类型注入的获取方法CreateConstructorCallSite()看看内部是如何实现:

根据注入类型注入的实例类型,通过反射获取对象的所有构造函数,如图可知如果需要使用默认容器的注入则注入的实例类型必须存在一个构造函数(包含默认无参数构造)否则直接报错,然后基本都逻辑是根据构造函数的个数来获取所需的ServiceCallSite对象,判断构造参数的入参数量后如果存在入参参数需要获取则再调用CreateArgumentCallSites()方法处理所需入参的初始化

image-20231010212134603.png CreateArgumentCallSites()内部其实也就是进行了套娃,遍历所有的入参数然后循环再次调用GetCallSite()进行注入,此时逻辑又来到了前面初始化的时候然后依次再继续往下执行

image-20231010212830023.png 梳理完类型注入的获取方法CreateConstructorCallSite()内部实现后再返回看获取的ServiceCallSite对象后续的操作

获取ServiceCallSite根据对应的ResultCache类型来判断是否单例生命周期(CallSiteResultCacheLocation.Root)来执行调用CallSiteRuntimeResolver.Instance.Resolve()方法

image-20231010213155123.png 以下为ServiceCallSite类 添加了部分中文注释


    /// <summary>
    /// Summary description for ServiceCallSite
    /// </summary>
    internal abstract class ServiceCallSite
    {
        protected ServiceCallSite(ResultCache cache)
        {
            Cache = cache;
        }
        /// <summary>
        /// 注入类型
        /// </summary>
        public abstract Type ServiceType { get; }

        /// <summary>
        /// 调用实例对象类型
        /// </summary>
        public abstract Type? ImplementationType { get; }

        /// <summary>
        /// 调用注入实例对象的方法的类型
        /// </summary>
        public abstract CallSiteKind Kind { get; }

        /// <summary>
        /// 缓存key
        /// </summary>
        public ResultCache Cache { get; }

        /// <summary>
        /// 第一次获取后缓存了的注入类型的实例
        /// </summary>
        public object? Value { get; set; }

        /// <summary>
        /// 是否可释放标识
        /// </summary>
        public bool CaptureDisposable =>
            ImplementationType == null ||
            typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
            typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
    }

依次查看该方法内部的实现

image-20231010214623964.png **VisitCallSite(ServiceCallSite callSite, TArgument argument)**方法内部通过判断注入类型的不同生命周期来调用方法来执行不同逻辑

image-20231010214643257.png 继续往下看看对应的**VisitXXX()**方法内部实现

image-20231010214708917.png 发现最终都是执行到**VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)**方法并执行了返回

image-20231011205352373.png 以下为最终执行不同方法的具体实现

直接把传入参数的ServiceProviderEngineScope传入Factory()方法最终返回实例对象
此处的FactoryCallSite.Factory()方法就是注入使用的方法

 protected override object VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
  {
      return factoryCallSite.Factory(context.Scope);
  }
  
 循环遍历执行VisitCallSite()方法最终返回多实例集合
 
protected override object VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
 {
     var array = Array.CreateInstance(
         enumerableCallSite.ItemType,
         enumerableCallSite.ServiceCallSites.Length);

     for (int index = 0; index < enumerableCallSite.ServiceCallSites.Length; index++)
     {
         object? value = VisitCallSite(enumerableCallSite.ServiceCallSites[index], context);
         array.SetValue(value, index);
     }
     return array;
 }
循环遍历构造函数所需的入参再依次执行 VisitCallSite()获取所有入参的实例后在调用构造函数执行构造最终返回
protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
        {
            object?[] parameterValues;
            if (constructorCallSite.ParameterCallSites.Length == 0)
            {
                parameterValues = Array.Empty<object>();
            }
            else
            {
                parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];
                for (int index = 0; index < parameterValues.Length; index++)
                {
                    parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);
                }
            }

 return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);
        }
直接返回实例对象 
protected override object? VisitConstant(ConstantCallSite constantCallSite, RuntimeResolverContext context)
 {
     return constantCallSite.DefaultValue;
 }
直接返回对应的  RuntimeResolverContext.ServiceProviderEngineScope对象
protected override object VisitServiceProvider(ServiceProviderCallSite serviceProviderCallSite, RuntimeResolverContext context)
  {
      return context.Scope;
  }

在调用ServiceProviderEngine的RealizeService()方法来获取提供服务实例的Func<ServiceProviderEngineScope, object?>的委托,委托内部最终调用VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)的方法然后根据不同的注入方法类型来构建不同的注入实例最后返回。

Singleton

来根据注入类型的实例获取不同类型来调用具体的方法,然后获取过一次后直接把获取实例放到ServiceCallSite.Value 后续获取时候直接根据生命类型判断后返回从而达到全局获取一致并且只实例化一次

Transient

和Singleton实现逻辑类似,只不过该作用域生命周期不会读取对应的缓存每次都会执行**VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)**的方法最终返回对象实例

Scoce

使用Score生命周期时候调用CreateScope方法返回一个新建的ServiceProviderEngineScope对象,该对象中存在RootProvider属性保存整个容器的ServiceProvider可以提供获取整个容器的其他实例对象,并且也支持递归循环调用Score

image-20231007194955654.png 在使用使用Score生命周期的时候,往容器中新增实例的注入其实就保存在ServiceProviderEngineScope实例中的List _disposables当中 ,在调用Dispose就释放掉了_disposables中的数据从而达到范围内周期生命一致的功能

image-20230921210913482.png

image-20230921211040067.png

梳理的默认容器的逻辑以及不同生命周期原理,接下来两章进行以下两个拓展

  1. 替换默认容器的原理是什么?

  2. 如何拓展自定义容器?

参考代码库来源

  1. dotnet/runtime dotnet/runtime: .NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps. (github.com)

  2. dotnet/aspnetcore dotnet/aspnetcore: ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux. (github.com)