ABP vNext系列文章03---依赖注入
一、依赖注入的类型注册
ABP的依赖注入系统是基于Microsoft的依赖注入扩展库(Microsoft.Extensions.DependencyInjection nuget包)开发的.因此,它的文档在ABP中也是有效的.
也就是说我们在ABP中要想向IOC容器中注入类有两种方式:
一是可以使用.netcore自带的注入方法
public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { //在此处注入依赖项 context.Services.AddTransient<IMyCurrentUser, MyCurrentUser>(); } }
二是依照ABP约定的规则注册
1、依赖接口
如果实现这些接口,则会自动将类注册到依赖注入:
ITransientDependency
注册为transient生命周期.ISingletonDependency
注册为singleton生命周期.IScopedDependency
注册为scoped生命周期.public class MyClass: ITransientDependency { }
MyClass
因为实现了ITransientDependency
,所以它会自动注册为transient生命周期.同理,其它的也是一样
2、Dependency 特性
我们也可以给某个类打上特性标签的方法来确定要注入的类
配置依赖注入服务的另一种方法是使用DependencyAttribute
.它具有以下属性:
Lifetime
: 注册的生命周期:Singleton,Transient或Scoped.TryRegister
: 设置true
则只注册以前未注册的服务.使用IServiceCollection的TryAdd ... 扩展方法.ReplaceServices
: 设置true
则替换之前已经注册过的服务.使用IServiceCollection的Replace扩展方法.
示例:
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] public class MyClass { }
如果定义了Lifetime
属性,则Dependency
特性具有比其他依赖接口更高的优先级.
3、ExposeServices 特性
ExposeServicesAttribute
用于控制相关类提供了什么服务.例:
[ExposeServices(typeof(ITaxCalculator))] public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency { }
TaxCalculator
类只公开ITaxCalculator
接口.这意味着你只能注入ITaxCalculator
,但不能注入TaxCalculator
或ICalculator
到你的应用程序中.
依照约定公开的服务
如果你未指定要公开的服务,则ABP依照约定公开服务.以上面定义的TaxCalculator
为例:
- 默认情况下,类本身是公开的.这意味着你可以按
TaxCalculator
类注入它. - 默认情况下,默认接口是公开的.默认接口是由命名约定确定.在这个例子中,
ICalculator
和ITaxCalculator
是TaxCalculator
的默认接口,但ICanCalculate
不是.
只要有意义,特性和接口是可以组合在一起使用的.
[Dependency(ReplaceServices = true)] [ExposeServices(typeof(ITaxCalculator))] public class TaxCalculator : ITaxCalculator, ITransientDependency { }
4、固定的注册类型
一些特定类型会默认注册到依赖注入.例子:
- 模块类注册为singleton.
- MVC控制器(继承
Controller
或AbpController
)被注册为transient. - MVC页面模型(继承
PageModel
或AbpPageModel
)被注册为transient. - MVC视图组件(继承
ViewComponent
或AbpViewComponent
)被注册为transient. - 应用程序服务(实现
IApplicationService
接口或继承ApplicationService
类)注册为transient. - 存储库(实现
IRepository
接口)注册为transient. - 域服务(实现
IDomainService
接口)注册为transient.
示例:
public class BlogPostAppService : ApplicationService { }
BlogPostAppService
由于它是从已知的基类派生的,因此会自动注册为transient生命周期.
如何选择?
如果使用的是ABP框架,使用自带的规则注入IOC是比较方便的,以下情况可以考虑手动注册
在某些情况下,你可能需要向IServiceCollection
手动注册服务,尤其是在需要使用自定义工厂方法或singleton实例时.在这种情况下,你可以像Microsoft文档描述的那样直接添加服务.例:
public class BlogModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { //注册一个singleton实例 context.Services.AddSingleton<TaxCalculator>(new TaxCalculator(taxRatio: 0.18)); //注册一个从IServiceProvider解析得来的工厂方法 context.Services.AddScoped<ITaxCalculator>(sp => sp.GetRequiredService<TaxCalculator>()); } }
二、依赖注入的使用
使用已注入的服务有三种方法
1、构造函数注入
这是将服务注入类的最常用方法.例如:
public class TaxAppService : ApplicationService { private readonly ITaxCalculator _taxCalculator; public TaxAppService(ITaxCalculator taxCalculator) { _taxCalculator = taxCalculator; } public void DoSomething() { //...使用 _taxCalculator... } }
TaxAppService
在构造方法中得到ITaxCalculator
.依赖注入系统在运行时自动提供所请求的服务.
构造方法注入是将依赖项注入类的首选方式.这样,除非提供了所有构造方法注入的依赖项,否则无法构造类.因此,该类明确的声明了它必需的服务.
2、属性注入
Microsoft依赖注入库不支持属性注入.但是,ABP可以与第三方DI提供商(例如Autofac)集成,以实现属性注入.例:
public class MyService : ITransientDependency { public ILogger<MyService> Logger { get; set; } public MyService() { Logger = NullLogger<MyService>.Instance; } public void DoSomething() { //...使用 Logger 写日志... } }
对于属性注入依赖项,使用公开的setter声明公共属性.这允许DI框架在创建类之后设置它.
属性注入依赖项通常被视为可选依赖项.这意味着没有它们,服务也可以正常工作.Logger
就是这样的依赖项,MyService
可以继续工作而无需日志记录.
为了使依赖项成为可选的,我们通常会为依赖项设置默认/后备(fallback)值.在此示例中,NullLogger用作后备.因此,如果DI框架或你在创建MyService
后未设置Logger属性,则MyService
依然可以工作但不写日志.
属性注入的一个限制是你不能在构造函数中使用依赖项,因为它是在对象构造之后设置的.
当你想要设计一个默认注入了一些公共服务的基类时,属性注入也很有用.如果你打算使用构造方法注入,那么所有派生类也应该将依赖的服务注入到它们自己的构造方法中,这使得开发更加困难.但是,对于非可选服务使用属性注入要非常小心,因为它使得类的要求难以清楚地看到.
3、从IServiceProvider解析服务
你可能希望直接从IServiceProvider
解析服务.在这种情况下,你可以将IServiceProvider
注入到你的类并使用GetService
方法,如下所示:
public class MyService : ITransientDependency { private readonly IServiceProvider _serviceProvider; public MyService(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void DoSomething() { var taxCalculator = _serviceProvider.GetService<ITaxCalculator>(); //... } }
重点:给服务注册回调方法
IServiceCollection.OnRegistred 事件
你可能想在注册到依赖注入的每个服务上执行一个操作, 在你的模块的 PreConfigureServices
方法中, 使用 OnRegistred
方法注册一个回调(callback) , 如下所示:
public class AppModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.OnRegistred(ctx => { var type = ctx.ImplementationType; //... }); } }
ImplementationType
提供了服务类型. 该回调(callback)通常用于向服务添加拦截器. 例如:
public class AppModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.OnRegistred(ctx => { if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true)) { ctx.Interceptors.TryAdd<MyLogInterceptor>(); } }); } }
这个示例判断一个服务类是否具有 MyLogAttribute
特性, 如果有的话就添加一个 MyLogInterceptor
到拦截器集合中.
注意, 如果服务类公开了多于一个服务或接口,
OnRegistred
回调(callback)可能被同一服务类多次调用. 因此, 较安全的方法是使用Interceptors.TryAdd
方法而不是Interceptors.Add
方法.
三、依赖注入源码解析
在接口注册时你可以看到
ABP vNext 仍然在其 Core 库为我们提供了三种接口,即 ISingletonDependency
和 ITransientDependency
、IScopedDependency
接口,方便我们的类型/组件自动注册,这三种接口分别对应了对象的 单例、瞬时、范围 生命周期。只要任何类型/接口实现了以上任意接口,ABP vNext 就会在系统启动时候,将这些对象注册到 IoC 容器当中。
那么究竟是在什么时候呢?回顾上一章的文章ABP vNext系列文章01---模块化 - zhengwei_cq - 博客园 (cnblogs.com),在模块系统调用模块的 ConfigureService()
的时候,就会有一个 services.AddAssembly(module.Type.Assembly)
,他会将模块的所属的程序集传入。
那我们就从AbpApplicationBase类这个ConfigureService()方法开始分析源码吧
继续进入ConfigureService()方法,我们知道在这个方法中主要是拿出之前加载的所有的模块,分别调用模块中的各种生命周期的方法
其中有一如下一段非常重要的代码
SkipAutoServiceRegistration是默认的值为false,核心的代码为 Services.AddAssembly(assembly);
进入这个扩展方法
发现里有一个比较核心的扩展方法GetConventionalRegistrars(),,主要是/获得所有规约注册器,然后调用规约注册器的 AddAssmbly 方法注册类型。
那这个规约器到底是什么呢,继续进入
在这个方法中可以看到,如果没有获取到规约器就是默认的DefaultConventionalRegistrar对象
那就从这个类开始研究了,进入此类,发现他的基类是ConventionalRegistrarBase,基类又实现了接口IConventionalRegistrar,看看此接口的方标准:
该接口定义了三个方法,支持传入程序集、类型数组、具体类型
再将回到抽像基类ConventionalRegistrarBase,最终调用的方法实现就是该类的扩展方法AddAssembly
先是获取了程序集中的所有类,去掉了抽像类和泛型类,再调用AddTypes方法 来将类型注册到 IServiceCollection
当中的。
查看这个方法,发现这个设计比较巧了,抽象类中这个方法也是抽象的,此方法被延迟到了子类中去实现了,我们就只能回到默认的规约类中看看:
该方法中主要是根据类的注入规则注入到服务中的,除了对三种生命周期接口处理之外,如果类型使用了 DependencyAttribute
特性,也会根据该特性的参数配置进行不同的注册逻辑。
但是从这个方法中我们只知道是否确定要不要把这个类注入到容器中,到底是怎么确定他注入的的生命周期的呢?
此时我们注意到方法GetLifeTimeOrNull(),在此方法前看到还获取了此类的特性GetDependencyAttributeOrNull(type),可能,发特性标签要优先于接口处理的
进入GetLifeTimeOrNull(type, dependencyAttribute);方法
这里补充一个小知道点 IsAssignableFrom方法,主要是用于判断两个类是否兼容,也就是说后者如果前者的实现类或者子类或者相同的类,则返回true.
那么到为此,我们就知道了,为什么我们在定义自己的类如果实现了指定的接口 或者将类上打上指定的特性后会将自己定义的类注入到容器了。