依赖注入&控制反转 #

阅前必读 由于很多朋友第一次接触依赖注入/控制反转的架构理念,所以没搞明白作用域和多线程解析服务的问题,从而不正确的使用导致内存不断飙高,正确的方式应该是: 尽可能的采用构造函数注入(如果这个类支持)在非静态中(但在Web 请求有效的声明周期内)可安全使用App.GetService<〉解析服务,如果是单例服务,优先推荐构造函数注入或App.RootServices.GetService<〉方式

在非Web 环境、多线程环境、物联网等环境(含事件总线、定时任务等),除单例服务以外,必须采用Scoped.Create() 方式创建作用域且服务在内部委托中解析!

依赖注入 #

所谓依赖注入,是指程序运行过程中,如果需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部的注入。

通俗来讲,就是把有依赖关系的类放到容器中,然后在我们需要这些类时,容器自动解析出这些类的实例。

依赖注入最大的好处时实现类的解耦,利于程序拓展、单元测试、自动化模拟测试等。依赖注入的英文为:Dependency Injection ,简称DI

控制反转 #

控制反转只是一个概念,也就是将创建对象实例的控制权(原本是程序员)从代码控制权剥离到 IOC 容器中控制。

控制反转的英文为:Inversion of Control ,简称IOC

IOC/DI 优缺点 #

传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。

优点 。依赖注入把对象的创造交给外部去管理很好的解决了代码紧耦合(tight couple)的问题,是 —种让代码实现松耦合(loose couple)的机制 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试

缺点 目前主流的IOC/DI 基本采用反射的方式来实现依赖注入,在—定程度会影响性能

特别说明 在本章节不打算细讲依赖注入/控制反转具体实现和应用场景,想了解更多知识,可查阅【ASP.NET (opens new window) Core 依赖注入】官方文档。

依赖注入的三种方式 #

构造方法注入 #

目前构造方法注入是依赖注入推荐使用方式。

优点

在构造方法中体现出对其他类的依赖,—眼就能看出这个类需要依赖哪些类才能工作 脱离了 IOC 框架,这个类仍然可以工作,POJO 的概念 —旦对象初始化成功了,这个对象的状态肯定是正确的

缺点 构造函数会有很多参数(Bad smell) 有些类是需要默认构造函数的,比如MVC 框架的Controller 类,—旦使用构造函数注入,就 无法使用默认构造函数

这个类里面的有些方法并不需要用到这些依赖(Bad smell) 代码示例:

public class Mall3sService
{
    private readonly IRepository _repository;
    public Mall3sService(IRepository repository)
    {
    _repository = repository;
    }
}

方法参数注入 #

方法参数注入的意思是在创建对象后,通过自动调用某个方法来注入依赖。

优点: 比较灵活

缺点: 新加入依赖时会破坏原有的方法签名,如果这个方法已经被其他很多模块用到就很麻烦与构造方法注入一样,会有很多参数

public class Mall3sService
{
    public Person GetById([FromServices]IRepository repository, int id)
    {
    	return repository.Find(id);
    }
}

注册对象生存期 #

暂时/瞬时生存期 #

暂时生存期服务是每次从服务容器进行请求时创建的。这种生存期适合轻量级、无状态的服务。在处理请求的应用中,在请求结束时会释放暂时服务。通常我们使用ITransient 接口依赖表示该生命周期。

作用域生存期 #

作用域生存期服务针对每个客户端请求(连接)创建一次。在处理请求的应用中,在请求结束时会释放 有作用域的服务。通常我们使用IScoped 接口依赖表示该生命周期。

单例生存期 #

在首次请求它们时进行创建,之后每个后续请求都使用相同的实例。通常我们使用ISingleton 接口依赖表示该生命周期。

了解更多 想了解更多服务生存期知识可查阅ASP.NET Core-依赖注入-服务生存期 (opens new window) 章节。

内置依赖接口 #

Mall3s 框架提供三个接口依赖分别对应不同的服务生存期:

ITransient :对应暂时/瞬时作用域服务生存期

IScoped :对应请求作用域服务生存期

ISingleton :对应单例作用域服务生存期

特别注意 以上三个接口只能实例类实现,其他静态类、抽象类、及接口不能实现

常见使用 #

第一个例子 #

创建IBusinessService 接口和BusinessService 实现类,代码如下:

using Mall3s.Core;
using Mall3s.DatabaseAccessor;
using Mall3s.Dependency;
namespace Mall3s.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 Mall3s.Application;
using Microsoft.AspNetCore.Mvc;
namespace Mall3s.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);

例子解说 Mall3s 框架提供了非常灵活且方便的实现依赖注入的方式,只需要实例类继承对应生存期的接口即可,这里继承了ITransient ,也就表明了这是一个暂时/瞬时作用域实例类。该类就可以作为被注入对象,同时也能注入其他接口对象。

上面的例子中,BusinessService 注入了IRepository<Person> 仓储接口,同时PersonController 控制器注入了IBusinessService 接口。这样PersonController 和BusinessService 之间就实现了解耦,不再依赖于具体的BusinessService 实例。

这就是依赖注入/控制反转最经典的例子。

注册泛型实例 #

创建IBusinessService<T> 接口和BusinessService<T> 实现类,代码如下:

using Mall3s.Core;
using Mall3s.DatabaseAccessor;
using Mall3s.Dependency;
namespace Mall3s.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 Mall3s.Application;
using Microsoft.AspNetCore.Mvc;
namespace Mall3s.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);
        }
    }
}

一个接口多个实现 #

默认情况下,一个接口只对应一个实现类,但有些特殊情况,需要多个实现类注册同一个接口,如DbContext 多数据库情况。

这个时候我们可以通过依赖注入Func<string, IPrivateDependency, object> 委托来解析多个实例,其中委托的参数分别为:

参数1 : string类型,不同实现类唯一标识,默认为nameof(实现类)名称 参数2:Type 类型,IPrivateDependency 派生接口,也就是ITransient 、IScoped 、ISingleton

返回值:object 类型,返回具体的实现类实例

创建IBusinessService 接口和BusinessService 、OtherBusinessService 两个实现类,代码如下:

using Mall3s.Dependency;
namespace Mall3s.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 Mall3s.Application;
using Mall3s.Dependency;
using Microsoft.AspNetCore.Mvc;
using System;
namespace Mall3s.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 Mall3s.Dependency;
namespace Mall3s.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;

无接口方式 #

有些时候,我们不想定义接口,而是想把实例类作为可依赖注入的对象,如MVC 中的控制器。创建SelfService 实例类,代码如下:

using Mall3s.core;
using Mall3s.DatabaseAccessor;
using Mall3s.DependencyInjection;
namespace Mall3s.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 Mall3s.Application;
using Mall3s.core;
using Microsoft.AspNetcore.Mvc;
namespace Mall3s.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);
        }
     }
 }

[Injection] 特性配置 #

Mall3s 框架提供[Injection] 特性可以改变注册方式,同时还能配置AOP 拦截。

[Injection] 提供以下配置支持:

  • Action :配置注册行为,InjectionActions 类型,可选值:

    • Add :默认值,表示无限制添加注册服务,该方式支持一个接口多个实现
    • TryAdd :表示注册已存在则跳过注册
  • Pattern :配置注册选项,InjectionPatterns 类型,可选值:

    • ​ Self :只注册自己
    • ​ FirstInterface :只注册第一个接口
    • ​ SelfWithFirstInterface:注册自己和第一个接口
    • ​ ImplementedInterfaces :注册所有接口
    • ​ All :注册自己包括所有接口,默认值
  • Named :配置实例别名,通过别名可以解析接口,如同一个接口有多个实现,那么可以通过别名解析不同的实现,默认只为实现类的类名

  • Order :注册排序,数字越大,则越在最后注册,默认0

  • Proxy :配置代理拦截类型,也就是AOP ,代理类型必须继承AspectDispatchProxy 类和IDispatchProxy 接口,无默认值

  • ExpectInterfaces :配置忽略注册的接口,Type[] 类型

自定义高级注册 #

默认情况下,Mall3s 提供的注册方式可以满足大多数依赖注入的需求,如有特别注册需求,只需要在Startup 中配置即可,如:

services.AddScoped(typeof(ISpecService), provider = > {
// 自定义任何创建实例的方式
var instance = new SpecService() ;	//或者可以通过AOP插件返回代理实例
return instance;
});

知识导航

想了解更多自定义高级中注册,可查阅【ASP.NET Core依赖注入】 (opens new window) 官方文档。

appsettings.json 配置注册 #

除了在代码中实现依赖注入,也可以实现动态依赖注入,无需修改代码或重新编译即可实现热插拔(插 件)效果。配置如下:

{
    "DependencyInjectionSettings": {
    "Definitions": [
    {
    "Interface": "Mall3s.Application;Mall3s.Application.ITestService",
    "Service": "Mall3s.Application;Mall3s.Application.TestService", "RegisterType": "Transient",

    "Action": "Add",
    "Pattern": "SelfWithFirstInterface",
    "Named": "TestService",
    "Order": 1,
    "Proxy": "Mall3s.Application;Mall3s.Application.LogDispathProxy"
    }}
}

配置说明:

  • DependencyInjectionSettings :依赖注入配置根节点

    • Definitions :动态依赖注入配置节点,ExternalService 数组类型

      • ExternalService :配置单个依赖注入信息

        • Interface :配置依赖接口信息,格式:程序集名称;接口完整名称,如:Mall3s.Application;Mall3s.Application.ITestService

        • Service :配置接口实现信息,格式同上

        • RegisterType :配置依赖注入的对象生存期,取值:Transient ,Scoped ,Singleton

        • Action :注册行为,可选值:Add , TryAdd,参见[7.8-injection-特性配置]

        • Pattern :注册选项,参见[7.8-injection-特性配置]

        • Named :注册别名,参见[7.8-injection-特性配置]

        • order :注册排序,参见[7.8-injection-特性配置]

        • Proxy :配置代理拦截,格式:程序集名称;代理类完整名称,参见[7.8-injection-特性配置]

关于外部程序集 如果动态注入的对象是外部程序集,那么首先先注册外部程序集:

{
"AppSettings": {
"ExternalAssemblies":"外部程序集名称", "Taobao.Pay"// 支持多个
}
}

注册顺序和优先级 #

Mall3s 框架中,默认注册顺序是按照程序集扫描顺序进行注册,如果需要改变注册顺序,可通过[Injection(Order)] 特性指定,Order 值越大,则越在最后注册。

另外appsettings.json 配置的优先级最大,appsettings.json 配置的注册会覆盖之前所有注册。

Aop 注册拦截 #

关于动态API 和服务的区别 #

如果您的服务是动态API,那么请使用[动态API - AOP拦截],原因是动态API本质是控制器,所以采用Filter 方式。

AOP 是非常重要的思想和技术,也就是面向切面编程,可以让我们在不改动原来代码的情况下进行动态篡改业务代码。

在Mall3s 框架中,实现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 Mall3s.DependencyInjection;
using System;
using System.Reflection;
namespace Mall3s.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 var result = await taskT;
            Task\<T\>;
            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 方法被调用的时候就可以实现动态拦截了,比如这里写日志。

全局Aop拦截 #

Mall3s 框架也提供了全局拦截的方式,只需要将IDispatchProxy 修改为IGlobalDispatchProxy 即可。

using Mall3s;
using System.Reflection;
namespace Mall3s.Application
{
    public class LogDispatchProxy : AspectDispatchProxy, IGlobalDispatchProxy
    {
    // 	
    }
}

这样就会拦截所有的Service ,当然也可以通过给特定类贴[SuppressProxy] 跳过全局拦截操作。

拦截优先级 #

[SuppressProxy] > [Injection(Proxy = typeof(LogDispatchProxy))] > 全局拦截。

AOP 注入解析服务 #

Mall3s 框架未提供Proxy 构造函数注入功能,但是提供了Services 属性,如果需要解析服务则可以通过以下方式:

var someServices = Services.GetService<ISomeService>(); // 推荐方式
// 或
var someServices = App.GetService<ISomeService>();

AOP 的作用 #

这种面向切面的能力(动态拦截/代理)可以实现很多很多功能,如:

动态日志记录

动态修改参数

动态修改返回值

动态方法重定向

动态修改代码逻辑

动态实现异常监听

还可以做更多更多的事情。

上次更新: 3/10/2023, 5:03:49 PM