12. 依赖注入/控制反转
阅前必读
由于很多朋友第一次接触 依赖注入/控制反转
的架构理念,所以没搞明白 作用域
和 多线程解析服务
的问题,从而不正确的使用导致内存不断飙高,正确的方式应该是:
- 尽可能的采用构造函数注入(如果这个类支持)
- 在非静态中(但在 Web 请求有效的声明周期内)可安全使用
App.GetService<>
解析服务,如果是单例服务
,优先推荐构造函数注入或App.RootServices.GetService<>
方式 - 🤐 在非
Web
环境、多线程环境、物联网等环境(含事件总线、定时任务等)🏒 除单例服务以外 🏒 必须采用Scoped.Create()
方式创建作用域且服务在内部委托中解析! 🤐
12.1 依赖注入
所谓依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。
通俗来讲,就是把有依赖关系的类放到容器中,然后在我们需要这些类时,容器自动解析出这些类的实例。
依赖注入最大的好处时实现类的解耦,利于程序拓展、单元测试、自动化模拟测试等。
依赖注入的英文为:Dependency Injection
,简称 DI
12.2 控制反转
控制反转只是一个概念,也就是将创建对象实例的控制权(原本是程序员)从代码控制权剥离到 IOC 容器
中控制。
控制反转的英文为:Inversion of Control
,简称 IOC
12.3 IOC/DI
优缺点
传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。
优点
- 依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制
- 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试
缺点
- 目前主流的
IOC/DI
基本采用反射的方式来实现依赖注入,在一定程度会影响性能
- 目前主流的
特别说明
在本章节不打算细讲 依赖注入/控制反转
具体实现和应用场景,想了解更多知识,可查阅 【ASP.NET Core 依赖注入】 官方文档。
12.4 依赖注入的三种方式
12.4.1 构造方法注入
目前构造方法注入是依赖注入推荐使用方式。
优点
- 在构造方法中体现出对其他类的依赖,一眼就能看出这个类需要依赖哪些类才能工作
- 脱离了 IOC 框架,这个类仍然可以工作,POJO 的概念
- 一旦对象初始化成功了,这个对象的状态肯定是正确的
缺点
- 构造函数会有很多参数(Bad smell)
- 有些类是需要默认构造函数的,比如 MVC 框架的 Controller 类,一旦使用构造函数注入,就无法使用默认构造函数
- 这个类里面的有些方法并不需要用到这些依赖(Bad smell)
代码示例:
public class FurionService
{
private readonly IRepository _repository;
public FurionService(IRepository repository)
{
_repository = repository;
}
}
12.4.2 属性方式注入
特别声明
在 Furion
新版本中,已经移除属性注入功能,建议使用构造函数或方法方式注入,也可以通过 App.GetService<TService>
方式注入。
通过属性方式注入容易和类的实例属性混淆,不建议使用。
优点
- 在对象的整个生命周期内,可以随时动态的改变依赖
- 非常灵活
缺点
- 对象在创建后,被设置依赖对象之前这段时间状态是不对的
- 不直观,无法清晰地表示哪些属性是必须的
public class FurionService
{
public IRepository Repository { get; set; }
}
12.4.3 方法参数注入
方法参数注入的意思是在创建对象后,通过自动调用某个方法来注入依赖。
优点:
- 比较灵活
缺点:
- 新加入依赖时会破坏原有的方法签名,如果这个方法已经被其他很多模块用到就很麻烦
- 与构造方法注入一样,会有很多参数
public class FurionService
{
public Person GetById([FromServices]IRepository repository, int id)
{
return repository.Find(id);
}
}
12.5 注册对象生存期
12.5.1 暂时/瞬时
生存期
暂时生存期服务是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。
在处理请求的应用中,在请求结束时会释放暂时服务。
通常我们使用 ITransient
接口依赖表示该生命周期。
12.5.2 作用域
生存期
作用域生存期服务针对每个客户端请求(连接)创建一次。在处理请求的应用中,在请求结束时会释放有作用域的服务。
通常我们使用 IScoped
接口依赖表示该生命周期。
12.5.3 单例
生存期
在首次请求它们时进行创建,之后每个后续请求都使用相同的实例。
通常我们使用 ISingleton
接口依赖表示该生命周期。
了解更多
想了解更多 服务生存期
知识可查阅 ASP.NET Core - 依赖注入 - 服务生存期 章节。
12.6 内置依赖接口
Furion
框架提供三个接口依赖分别对应不同的服务生存期:
ITransient
:对应暂时/瞬时作用域服务生存期IScoped
:对应请求作用域服务生存期ISingleton
:对应单例作用域服务生存期
特别注意
以上三个接口只能实例类实现,其他静态类、抽象类、及接口不能实现。
12.7 常见使用
12.7.1 第一个例子
创建 IBusinessService
接口和 BusinessService
实现类,代码如下:
using Furion.Core;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
namespace Furion.Application
{
public interface IBusinessService
{
Person Get(int id);
}
public class BusinessService : IBusinessService, ITransient
{
private readonly IRepository<Person> _personRepository;
public BusinessService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public Person Get(int id)
{
return _personRepository.Find(id);
}
}
}
创建 PersonController
控制器,代码如下:
using Furion.Application;
using Microsoft.AspNetCore.Mvc;
namespace Furion.Web.Entry.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
private readonly IBusinessService _businessService;
public PersonController(IBusinessService businessService)
{
_businessService = businessService;
}
[HttpGet]
public IActionResult Get(int id)
{
var person = _businessService.Get(id);
return new JsonResult(person);
}
}
}
例子解说
Furion
框架提供了非常灵活且方便的实现依赖注入的方式,只需要实例类继承对应生存期的接口即可,这里继承了 ITransient
,也就表明了这是一个 暂时/瞬时
作用域实例类。该类就可以作为被注入对象,同时也能注入其他接口对象。
上面的例子中,BusinessService
注入了 IRepository<Person>
仓储接口,同时 PersonController
控制器注入了 IBusinessService
接口。
这样 PersonController
和 BusinessService
之间就实现了解耦,不再依赖于具体的 BusinessService
实例。
这就是依赖注入/控制反转最经典的例子。
12.7.2 注册泛型实例
创建 IBusinessService<T>
接口和 BusinessService<T>
实现类,代码如下:
using Furion.Core;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
namespace Furion.Application
{
public interface IBusinessService<T>
{
Person Get(int id);
}
public class BusinessService<T> : IBusinessService<T>, ITransient
{
private readonly IRepository<Person> _personRepository;
public BusinessService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public Person Get(int id)
{
return _personRepository.Find(id);
}
}
}
创建 PersonController
控制器,代码如下:
using Furion.Application;
using Microsoft.AspNetCore.Mvc;
namespace Furion.Web.Entry.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
private readonly IBusinessService<int> _businessService;
public PersonController(IBusinessService<int> businessService)
{
_businessService = businessService;
}
[HttpGet]
public IActionResult Get(int id)
{
var person = _businessService.Get(id);
return new JsonResult(person);
}
}
}
12.7.3 一个接口多个实现
默认情况下,一个接口只对应一个实现类,但有些特殊情况,需要多个实现类注册同一个接口,如 DbContext
多数据库情况。
这个时候我们可以通过依赖注入 Func<string, IPrivateDependency, object>
委托来解析多个实例,其中委托的参数分别为:
- 参数 1:
string
类型,不同实现类唯一标识,默认为nameof(实现类)
名称 - 参数 2:
Type
类型,IPrivateDependency
派生接口,也就是ITransient
、IScoped
、ISingleton
- 返回值:
object
类型,返回具体的实现类实例
创建 IBusinessService
接口和 BusinessService
、OtherBusinessService
两个实现类,代码如下:
using Furion.DependencyInjection;
namespace Furion.Application
{
public interface IBusinessService
{
string GetName();
}
public class BusinessService : IBusinessService, ITransient
{
public string GetName()
{
return "我是:" + nameof(BusinessService);
}
}
public class OtherBusinessService : IBusinessService, ITransient
{
public string GetName()
{
return "我是:" + nameof(OtherBusinessService);
}
}
}
创建 ValueController
控制器,代码如下:
using Furion.Application;
using Furion.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using System;
namespace Furion.Web.Entry.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValueController : ControllerBase
{
private readonly IBusinessService _businessService;
private readonly IBusinessService _otherBusinessService;
public ValueController(Func<string, ITransient, object> resolveNamed)
{
_businessService = resolveNamed("BusinessService", default) as IBusinessService;
_otherBusinessService = resolveNamed("OtherBusinessService", default) as IBusinessService;
}
[HttpGet]
public string GetName()
{
return _businessService.GetName() + "----------" + _otherBusinessService.GetName();
}
}
}
小知识
如果需要自定义解析名称,只需要贴 [Injection(Named = "名称")]
即可,如:
using Furion.DependencyInjection;
namespace Furion.Application
{
[Injection(Named = "BusName1")]
public class BusinessService : IBusinessService, ITransient
{
// ...
}
[Injection(Named = "BusName2")]
public class OtherBusinessService : IBusinessService, ITransient
{
// ...
}
}
解析服务:
_businessService = resolveNamed("BusName1", default) as IBusinessService;
_otherBusinessService = resolveNamed("BusName2", default) as IBusinessService;
12.7.4 无接口方式
有些时候,我们不想定义接口,而是想把实例类作为可依赖注入的对象,如 MVC 中的控制器。
创建 SelfService
实例类,代码如下:
using Furion.Core;
using Furion.DatabaseAccessor;
using Furion.DependencyInjection;
namespace Furion.Application
{
public class SelfService : ITransient
{
private readonly IRepository<Person> _personRepository;
public SelfService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public Person Get(int id)
{
return _personRepository.Find(id);
}
}
}
创建 ValueController
控制器,代码如下:
using Furion.Application;
using Furion.Core;
using Microsoft.AspNetCore.Mvc;
namespace Furion.Web.Entry.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValueController : ControllerBase
{
private readonly SelfService _selfService;
public ValueController(SelfService selfService)
{
_selfService = selfService;
}
[HttpGet]
public Person Get(int id)
{
return _selfService.Get(id);
}
}
}
12.8 [Injection]
特性配置
Furion
框架提供 [Injection]
特性可以改变注册方式,同时还能配置 AOP
拦截。
[Injection]
提供以下配置支持:
Action
:配置注册行为,InjectionActions
类型,可选值:Add
:默认值,表示无限制添加注册服务,该方式支持一个接口多个实现TryAdd
:表示注册已存在则跳过注册
Pattern
:配置注册选项,InjectionPatterns
类型,可选值:Self
:只注册自己FirstInterface
:只注册第一个接口SelfWithFirstInterface
:注册自己和第一个接口ImplementedInterfaces
:注册所有接口All
:注册自己包括所有接口 ,默认值
Named
:配置实例别名,通过别名可以解析接口,如同一个接口有多个实现,那么可以通过别名解析不同的实现,默认只为实现类的类名Order
:注册排序,数字越大,则越在最后注册,默认0
Proxy
:配置代理拦截类型,也就是AOP
,代理类型必须继承AspectDispatchProxy
类和IDispatchProxy
接口,无默认值ExpectInterfaces
:配置忽略注册的接口,Type[]
类型
12.9 自定义高级注册
默认情况下,Furion
提供的注册方式可以满足大多数依赖注入的需求,如有特别注册需求,只需要在 Startup
中配置即可,如:
services.AddScoped(typeof(ISpecService), provider = > {
// 自定义任何创建实例的方式
var instance = new SpecService(); // 或者可以通过 AOP插件返回代理实例
return instance;
});
补充说明
Furion
框架中的 AppDbContext
数据库上下文还有 ISqlDispatchProxy
都是通过这种方式创建的。
知识导航
想了解更多自定义高级中注册,可查阅 【ASP.NET Core 依赖注入】 官方文档。
12.10 appsettings.json
配置注册
除了在代码中实现依赖注入,也可以实现动态依赖注入,无需修改代码或重新编译即可实现热插拔(插件)效果。配置如下:
{
"DependencyInjectionSettings": {
"Definitions": [
{
"Interface": "Furion.Application;Furion.Application.ITestService",
"Service": "Furion.Application;Furion.Application.TestService",
"RegisterType": "Transient",
"Action": "Add",
"Pattern": "SelfWithFirstInterface",
"Named": "TestService",
"Order": 1,
"Proxy": "Furion.Application;Furion.Application.LogDispathProxy"
}
]
}
}
配置说明:
DependencyInjectionSettings
:依赖注入配置根节点Definitions
:动态依赖注入配置节点,ExternalService
数组类型ExternalService
:配置单个依赖注入信息Interface
:配置依赖接口信息,格式:程序集名称;接口完整名称
,如:Furion.Application;Furion.Application.ITestService
Service
:配置接口实现信息,格式同上RegisterType
:配置依赖注入的对象生存期,取值:Transient
,Scoped
,Singleton
Action
:注册行为,可选值:Add
,TryAdd
,参见 #128-injection-特性配置Pattern
:注册选项,参见 #128-injection-特性配置Named
:注册别名,参见 #128-injection-特性配置Order
:注册排序,参见 #128-injection-特性配置Proxy
:配置代理拦截,格式:程序集名称;代理类完整名称
,参见 #128-injection-特性配置
关于外部程序集
如果动态注入的对象是外部程序集,那么首先先注册外部程序集:
{
"AppSettings": {
"ExternalAssemblies": ["外部程序集名称", "Taobao.Pay"] // 支持多个
}
}
12.11 注册顺序和优先级
Furion
框架中,默认注册顺序是按照程序集扫描顺序进行注册,如果需要改变注册顺序,可通过 [Injection(Order)]
特性指定,Order
值越大,则越在最后注册。
另外 appsettings.json
配置的优先级最大,appsettings.json
配置的注册会覆盖之前所有注册。
12.12 Aop
注册拦截
关于动态 API 和服务的区别
如果您的服务是动态 API,那么请使用 动态 API - AOP 拦截,原因是动态 API 本质是控制器,所以采用 Filter
方式。
AOP
是非常重要的思想和技术,也就是 面向切面
编程,可以让我们在不改动原来代码的情况下进行动态篡改业务代码。
在 Furion
框架中,实现 Aop
非常简单,如:
假设我们有 ITestService
和 TestService
两个类型:
public interface ITestService
{
string SayHello(string word);
}
public class TestService: ITestService, ITransient
{
public string SayHello(string word)
{
return $"Hello {word}";
}
}
现在我们有一个需求,我们希望调用 SayHello
的时候可以记录日志和权限控制(之前没有考虑到的需求)。
这个时候我们只需要创建一个代理类即可,如 LogDispatchProxy
using Furion.DependencyInjection;
using System;
using System.Reflection;
namespace Furion.Application
{
public class LogDispatchProxy : AspectDispatchProxy, IDispatchProxy
{
/// <summary>
/// 当前服务实例
/// </summary>
public object Target { get; set; }
/// <summary>
/// 服务提供器,可以用来解析服务,如:Services.GetService()
/// </summary>
public IServiceProvider Services { get; set; }
/// <summary>
/// 拦截方法
/// </summary>
/// <param name="method"></param>
/// <param name="args"></param>
/// <returns></returns>
public override object Invoke(MethodInfo method, object[] args)
{
Console.WriteLine("SayHello 方法被调用了");
var result = method.Invoke(Target, args);
Console.WriteLine("SayHello 方法返回值:" + result);
return result;
}
// 异步无返回值
public override async Task InvokeAsync(MethodInfo method, object[] args)
{
Console.WriteLine("SayHello 方法被调用了");
var task = method.Invoke(Target, args) as Task;
await task;
Console.WriteLine("SayHello 方法调用完成");
}
// 异步带返回值
public override async Task<T> InvokeAsyncT<T>(MethodInfo method, object[] args)
{
Console.WriteLine("SayHello 方法被调用了");
var taskT = method.Invoke(Target, args) as Task<T>;
var result = await taskT;
Console.WriteLine("SayHello 方法返回值:" + result);
return result;
}
}
}
获取特性
如果需要获取方法的特性,只需要通过 method.GetActualCustomAttribute<TArrbute>()
即可。所有获取真实的特性统一采用 method.GetActual....()
方法开头。
之后我们只需要为 TestService
增加 [Injection]
特性即可,如:
[Injection(Proxy = typeof(LogDispatchProxy))]
public class TestService: ITestService, ITransient
{
public string SayHello(string word)
{
return $"Hello {word}";
}
}
之后 SayHello
方法被调用的时候就可以实现动态拦截了,比如这里写日志。
12.12.1 全局Aop拦截
Furion
框架也提供了全局拦截的方式,只需要将 IDispatchProxy
修改为 IGlobalDispatchProxy
即可。
using Furion;
using System.Reflection;
namespace Furion.Application
{
public class LogDispatchProxy : AspectDispatchProxy, IGlobalDispatchProxy
{
// ....
}
}
这样就会拦截所有的 Service
,当然也可以通过给特定类贴 [SuppressProxy]
跳过全局拦截操作。
拦截优先级
[SuppressProxy]
> [Injection(Proxy = typeof(LogDispatchProxy))]
> 全局拦截
。
12.12.2 AOP
注入解析服务
Furion
框架未提供 Proxy
构造函数注入功能,但是提供了 Services
属性,如果需要解析服务,则可以通过以下方式:
var someServices = Services.GetService<ISomeService>(); // 推荐方式
// 或
var someServices = App.GetService<ISomeService>();
12.12.3 AOP
的作用
这种面向切面的能力(动态拦截/代理)可以实现很多很多功能,如:
- 动态日志记录
- 动态修改参数
- 动态修改返回值
- 动态方法重定向
- 动态修改代码逻辑
- 动态实现异常监听
还可以做更多更多的事情。
12.13 反馈与建议
与我们交流
给 Furion 提 Issue。