3.4 一个简易版的依赖注入容器
前面从纯理论的角度对依赖注入进行了深入论述,第4章会对ASP.NET Core框架内部使用的依赖注入框架进行单独介绍。为了使读者能够更好地理解第 4 章的内容,我们按照类似的原理创建了一个简易版本的依赖注入框架,也就是前面多次提及的Cat。
3.4.1 编程体验
虽然我们对这个名为Cat的依赖注入框架进行了最大限度的简化,但是与ASP.NET Core框架内部使用的真实依赖注入框架相比,Cat不但采用了一致的设计,而且具备所有的功能特性。为了使读者对Cat具有感官方面的认识,下面先演示如何利用Cat提供所需的服务实例。
作为依赖注入容器的 Cat 对象不仅可以作为服务实例的提供者,还需要维护服务实例的生命周期。Cat提供了3种生命周期模式,如果要了解它们之间的差异,就必须对多个Cat之间的层次关系有充分的认识。一个代表依赖注入容器的 Cat 对象用来创建其他的 Cat 对象,后者将前者视为“父容器”,所以多个 Cat对象通过其“父子关系”维系一个树形层次化结构。但这仅仅是一个逻辑结构而已,实际上,每个Cat对象只会按照图3-6所示的方式引用整棵树的根。
图3-6 Cat对象之间的关系
了解了多个 Cat 对象之间的关系之后,就很容易理解 3 种预定义的生命周期模式。如下所示的 Lifetime 枚举代表 3 种生命周期模式:Transient 代表容器针对每次服务请求都会创建一个新的服务实例;Self 将提供服务实例保存在当前容器中,它代表针对某个容器范围内的单例模式;Root 将每个容器提供的服务实例统一存放到根容器中,所以该模式能够在多个“同根”容器范围内确保提供的服务是单例的。
代表依赖注入容器的 Cat 对象之所以能够为我们提供所需的服务实例,其根本前提是相应的服务注册在此之前已经添加到容器之中。服务总是针对服务类型(接口、抽象类或者具体类型)进行注册的,Cat通过定义的扩展方法提供了如下3种注册方式。除了直接提供服务实例的形式(默认采用Root模式),在注册服务时必须指定一个具体的生命周期模式。
● 指定具体的实现类型。
● 提供一个服务实例。
● 指定一个创建服务实例的工厂。
我们定义了如下所示的接口和对应的实现类型,来演示针对 Cat 的服务注册。其中,Foo、Bar、Baz 和 Qux 分别实现了对应的接口 IFoo、IBar、IBaz 和 IQux,其中 Qux 类型上标注的MapToAttribute特性,注册了与对应接口IQux之间的映射。为了反映Cat对服务实例生命周期的控制,可以让它们派生于同一个基类 Base。Base实现了 IDisposable接口,我们在其构造函数和实现的 Dispose 方法中输出相应的文本,以确定对应的实例何时被创建和释放。另外,我们还定义了一个泛型的接口IFoobar<T1,T2>和对应的实现类Foobar<T1,T2>,来演示Cat针对泛型服务实例的提供。
如下所示的代码片段创建了一个 Cat 对象,并采用上面提到的方式针对接口 IFoo、IBar 和IBaz注册了对应的服务,它们采用的生命周期模式分别为 Transient、Self和 Root。另外,我们还调用了另一个将当前入口程序集作为参数的 Register 方法,该方法会解析指定程序集中标注了 MapToAttribute 特性的类型并做相应的服务注册,对于我们演示的程序来说,该方法会完成针对 IQux/Qux类型的服务注册。接下来我们利用 Cat对象创建了它的两个子容器,并调用子容器的GetService<T>方法来提供相应的服务实例。
上面的程序运行之后会在控制台上输出图3-7所示的结果,输出结果不仅表明Cat能够根据添加的服务注册提供对应类型的服务实例,还体现了它对生命周期的控制。由于服务 IFoo被注册为 Transient服务,所以 Cat针对该接口的服务提供的 4次请求都会创建一个全新的 Foo对象。IBar服务的生命周期模式为Self,如果利用同一个Cat对象提供对应的服务实例,那么该Cat对象只会创建一个 Bar对象,所以整个过程中会创建两个 Bar对象。IBaz和 IQux服务采用 Root生命周期,所以具有同根的两个 Cat 对象提供的总是同一个 Baz/Qux 对象,后者只会被创建一次。(S301)
图3-7 Cat按照服务注册对应的生命周期模式提供服务实例
除了提供类似于 IFoo、IBar和 IBaz这种非泛型的服务实例,如果具有针对泛型定义(Generic Definition)的服务注册,Cat 同样可以提供泛型服务实例。如下面的代码片段所示,在为创建的Cat对象添加了针对IFoo接口和IBar接口的服务注册之后,我们调用Register方法注册了针对泛型定义 IFoobar<,>的服务注册,具体的实现类型为 Foobar<,>。当我们利用 Cat 对象提供一个类型为IFoobar<IFoo,IBar>的服务实例时,它会创建并返回一个Foobar<Foo,Bar>对象。(S302)
在进行服务注册时,可以为同一个类型添加多个服务注册。虽然添加的所有服务注册均是有效的,但由于 GetService<TService>扩展方法总是返回一个唯一的服务实例,所以可以对该方法采用“后来居上”的策略,即总是采用最近添加的服务注册创建服务实例。如果调用另一个GetServices<TService>扩展方法,该方法将返回根据所有服务注册提供的服务实例。
下面的代码片段为创建的Cat对象添加了 3个针对 Base类型的服务注册,对应的实现类型分别为 Foo、Bar和 Baz。我们将 Base作为泛型参数调用了 GetServices<Base>方法,该方法会返回包含3个Base对象的集合,集合元素的类型分别为Foo、Bar和Baz。(S303)
如果提供的服务实例实现了 IDisposable 接口,就应该在适当的时候调用其 Dispose 方法释放该服务实例。由于服务实例的生命周期完全由作为依赖注入容器的 Cat 对象来管理,所以通过调用 Dispose方法来释放服务实例也应该由它负责。Cat对象针对提供服务实例的释放策略取决于采用的生命周期模式,具体的策略如下。
● Transient和Self:所有实现了IDisposable接口的服务实例会被当前Cat对象保存起来,当Cat 对象自身的 Dispose方法被调用的时候,这些服务实例的 Dispose 方法会随之被调用。
● Root:由于服务实例保存在作为根容器的Cat对象上,所以当这个Cat对象的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
上述释放策略可以通过如下演示实例来印证。如下代码片段创建了一个 Cat 对象,并添加了相应的服务注册;然后调用 CreateChild方法创建了代表子容器的 Cat对象,并用它提供了 4个注册服务对应的实例。
由于两个 Cat对象的创建都是在 using块中进行的,所以它们的 Dispose方法都会在 using块结束的地方被调用。为了确定方法被调用的时机,我们特意在控制台上打印了相应的文字。该程序运行之后会在控制台上输出图 3-8 所示的结果,可以看到,当作为子容器的 Cat 对象的Dispose 方法被调用时,由它提供的两个生命周期模式分别为 Transient 和 Self 的两个服务实例(Foo和Bar)被正常释放。而生命周期模式为Root的服务实例是Baz和Qux,它的Dispose方法会延迟到作为根容器的Cat对象的Dispose方法被调用的时候。(S304)
图3-8 服务实例的释放
3.4.2 设计与实现
在完成针对 Cat 的编程体验之后,下面介绍依赖注入容器的设计原理和具体实现。由于作为依赖注入容器的 Cat 对象总是利用预先添加的服务注册来提供对应的服务实例,所以服务注册至关重要。如下所示的代码片段就是表示服务注册的 ServiceRegistry 类型的定义,它具有 3个核心属性(ServiceType、Lifetime和 Factory),分别代表服务类型、生命周期模式和用来创建服务实例的工厂。最终用来创建服务实例的工厂体现为一个类型为Func<Cat,Type[],object>的委托对象,它的两个输入分别代表当前使用的 Cat 对象以及提供服务类型的泛型参数,如果提供的服务类型并不是一个泛型类型,这个参数就会被指定为一个空的数组。
将针对同一个服务类型(ServiceType 属性相同)的多个 ServiceRegistry 组成一个链表,那么作为相邻节点的两个 ServiceRegistry对象将通过 Next属性关联起来。我们为 ServiceRegistry定义了一个 AsEnumerable方法,使它返回由当前及后续节点组成的 ServiceRegistry集合。如果当前ServiceRegistry为链表头,那么这个方法会返回链表上的所有ServiceRegistry对象。图3-9体现了服务注册的3个核心要素和链表结构。
图3-9 服务注册
在了解了表示服务注册的 ServiceRegistry 之后,下面着重介绍表示依赖注入容器的 Cat 类型。如下面的代码片段所示,Cat类型同时实现了 IServiceProvider接口和 IDisposable接口,定义在前者中的 GetService 方法用于提供服务实例。作为根容器的 Cat 对象通过公共构造函数创建,另一个内部构造函数则用来创建作为子容器的Cat对象,指定的Cat对象将作为父容器。
作为根容器的 Cat 对象通过_root 字段表示。_registries 字段返回的 ConcurrentDictionary<Type,ServiceRegistry>对象用来存储所有添加的服务注册,该字典对象的 Key和 Value分别表示服务类型与 ServiceRegistry链表,图 3-10可以体现这一映射关系。由于需要负责完成对提供服务实例的释放工作,所以需要将实现了IDisposable接口的服务实例保存在通过_disposables字段表示的集合中。
图3-10 服务类型与服务注册链表的映射
由当前 Cat对象提供的非 Transient服务实例保存在由_services字段表示的一个 Concurrent Dictionary<Key,object>对象上,该字典对象的键类型为如下代码片段中的 Key,它相当于创建服务实例所使用的ServiceRegistry对象和泛型参数类型数组的组合。
虽然我们为 Cat 类型定义了若干扩展方法来提供多种不同的服务注册,但是这些方法最终都会调用如下代码片段中的 Register 方法,该方法会将提供的 ServiceRegistry 对象添加到_registries字段表示的字典对象中。值得注意的是,无论调用哪个 Cat对象的 Register方法,指定的ServiceRegistry对象都会被添加到作为根容器的Cat对象上。
用来提供服务实例的核心操作实现在如下代码片段的 GetServiceCore 方法中。如下面的代码片段所示,在调用 GetServiceCore方法时需要指定对应的 ServiceRegistry对象的服务类型的泛型参数。当该方法被执行的时候,对于 Transient 生命周期模式,它会直接利用ServiceRegistry对象提供的工厂来创建服务实例。如果服务实例的类型实现了 IDisposable接口,它会被添加到_disposables 字段表示的待释放服务实例列表中。如果生命周期模式为 Root 和Self,该方法会先根据提供的 ServiceRegistry 对象判断对应的服务实例是否已经存在,存在的服务实例会直接返回。
GetServiceCore方法只有在指定 ServiceRegistry对应的服务实例不存在的情况下才会利用提供的工厂来创建服务实例,创建的服务实例会根据生命周期模式保存到作为根容器的 Cat 对象或者当前Cat对象上。如果提供的服务实例实现了IDisposable接口,在采用Root生命周期模式时会被保存到作为根容器的 Cat 对象的待释放列表中。如果生命周期模式为 Self,那么它会被添加到当前Cat对象的待释放列表中。
在实现的GetService方法中,Cat会根据指定的服务类型找到对应的ServiceRegistry对象,并最终调用GetServiceCore方法来提供对应的服务实例。GetService方法还会解决一些特殊服务的供给问题:若服务类型为 Cat 或者 IServiceProvider,该方法返回的就是它自己;如果服务类型为 IEnumerable<T>,GetService方法会根据泛型参数类型 T找到所有的 ServiceRegistry并利用它们来创建对应的服务实例,最终返回的是由这些服务实例组成的集合。除了这些,针对泛型服务实例的提供也是在GetService方法中解决的。
在实现的 Dispose 方法中,由于所有待释放的服务实例已经保存到_disposables 字段表示的集合中,所以依次调用它们的Dispose方法即可。释放了所有服务实例并清空待释放列表之后,Dispose方法还会清空_services字段表示的服务实例列表。
3.4.3 扩展方法
为了方便服务注册,可以定义如下 6 个 Register 扩展方法。由于服务注册的添加总是需要调用 Cat 自身的 Register 扩展方法来完成,所以这些方法最终都需要创建一个代表服务注册的ServiceRegistry对象。对于一个 ServiceRegistry对象来说,它最核心的元素就是表示服务实例创建工厂的Func<Cat,Type[],object>对象,所以这6个扩展方法需要解决的问题就是创建一个委托对象。
由于前两个重载指定的是服务实现类型,所以我们需要调用对应的构造函数来创建服务实例,这一逻辑在私有的 Create 方法中实现。第三个扩展方法直接指定服务实例,所以将提供的参数转换成一个Func<Cat,Type[],object>非常容易。
我们刻意简化了构造函数的筛选逻辑。为了解决构造函数的选择问题,可以引入InjectionAttribute 特性。如果将所有公共实例构造函数作为候选的构造函数,就会优先选择标注了该特性的构造函数。当构造函数被选择出来之后,我们需要通过分析其参数类型并利用 Cat对象来提供具体的参数值,这实际上是一个递归的过程。最终我们将针对构造函数的调用转换成Func<Cat,Type[],object>对象,进而创建出表示服务注册的ServiceRegistry对象。
上述 6 个扩展方法可以完成针对单一服务的注册,有时项目中可能会出现非常多的服务需要注册,完成针对它们的批量注册是一个不错的选择。依赖注入框架提供了针对程序集范围的批量服务注册。为了标识带注册的服务,我们需要在服务实现类型上标注如下所示的MapToAttribute类型,并指定服务类型(一般为它实现的接口或者继承的基类)和生命周期。
针对程序集范围的批量服务注册在Cat的如下所示的Register扩展方法中实现。如下面的代码片段所示,该方法会从指定程序集中获取所有标注了 MapToAttribute 特性的类型,并提取服务类型、实现类型和生命周期模型,然后利用它们批量完成所需的服务注册。
除了上述6个用来注册服务的Register扩展方法,我们还为Cat类型定义了3个扩展方法:GetService<T>方法以泛型参数的形式指定服务类型,GetServices<T>方法会提供指定服务类型的所有实例,而CreateChild方法则帮助我们创建一个代表子容器的Cat对象。