乘风原创程序

  • 在.net core中实现字段和属性注入的示例代码
  • 2020/8/14 10:59:01
  • 简单来说,使用Ioc模式需要两个步骤,第一是把服务注册到容器中,第二是从容器中获取服务,我们一个一个讨论并演化。这里不会考虑使用如Autofac等第三方的容器来代替默认容器,只是提供一些简单实用的小方法用于简化应用层的开发。

    将服务注入到容器

    asp.netcore官方给出的在容器中注册服务方法是,要在Startup类的ConfigureServices方法中添加服务,如下所示:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc();
    
      services.AddSingleton(typeof(UserService));
      services.AddSingleton(typeof(MsgService));
      services.AddSingleton(typeof(OrderService));
    }
    
    

    AddMvc方法添加了mvc模块内部用到的一些服务,这个是封装好的,一句话就行了,其他第三方组件也都提供了类似的Add方法,把自己内部需要的服务都封装好注册进去了。但是我们应用开发人员使用的类,还是需要一个一个写进去的,大家最常见的三层架构中的数据访问层和业务逻辑层便是此类服务,上面代码中我加入了三个业务服务类。这显然不是长久之计,我想大家在开发中也会针对此问题做一些处理,这里说下我的,仅供参考吧。

    解决方法就是批量注册!说到批量,就需要一个东西来标识一批东西,然后用这一个东西来控制这一批东西。在.net程序的世界中,有两个可选的角色,一个是接口Interface,另一个是特性Attribute。

    如果使用接口作为标识来使用,限制就太死板了,一个标识的信息不是绝对的单一,是不推荐使用接口的,因为可能需要引入多个接口才能共同完成,所以我选择特性作为标识。特性相较与接口有什么特点呢?特性在运行时是类的实例,所以可以存储更多的信息。

    下面我们简单实现一个AppServiceAttribute:

    /// <summary>
    /// 标记服务
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public class AppServiceAttribute : Attribute
    {
    }
    

    这个特性类取名AppService有两个理由,一是指定是应用层的服务类,二是避免使用Service这样的通用命名和其他类库冲突。

    有了标识,就可以批量处理了,我们在一个新的类中给IServiceCollection提供一个扩展方法,用来批量添加标记有AppService特性的服务到容器中。

    public static class AppServiceExtensions
    {
      /// <summary>
      /// 注册应用程序域中所有有AppService特性的服务
      /// </summary>
      /// <param name="services"></param>
      public static void AddAppServices(this IServiceCollection services)
      {
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
          foreach (var type in assembly.GetTypes())
          {
            var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>();
    
            if (serviceAttribute != null)
            {
              services.AddSingleton(type);
            }
          }
        }
      }
    }
    
    

    我们遍历应用程序中所有程序集,然后嵌套遍历每个程序集中的所有类型,判断类型是否有AppService特性,如果有的话就添加到容器中,这里有点不自信哦,为什么呢,因为我是使用AddSingleton方法以单例模式将服务添加到容器中的,虽然三层中的数据访问层和业务逻辑层绝大部分都可以使用单例,但是我们希望更通用一些,大家都知道netcore自带的Ioc容器支持三种生命周期,所以我们修改AppServiceAttribute,添加一个Lifetime属性:

    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public class AppServiceAttribute : Attribute
    {
      /// <summary>
      /// 生命周期
      /// </summary>
      public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
    }
    

    Lifetime的默认值我们设置成ServiceLifetime.Singleton是比较合适的,因为大部分服务我们都希望使用单例注册,一个合理的默认设置可以节省使用者很多代码,新手可能还会乐于复制粘贴,但老同志肯定都深有体会。

    有了Lifetime这个信息,我们就可以改进AddAppServices方法了,在判断serviceAttribute不为null后,使用下面的代码替换services.AddSingleton(type):

      switch (serviceAttribute.Lifetime)
      {
        case ServiceLifetime.Singleton:
          services.AddSingleton(serviceType, type);
          break;
        case ServiceLifetime.Scoped:
          services.AddScoped(serviceType, type);
          break;
        case ServiceLifetime.Transient:
          services.AddTransient(serviceType, type);
          break;
        default:
          break;
      }
    

    现在我们可以注册不同生命周期的服务了,只是该控制是在类的定义中,按理说,服务对象注册到容器中的生命周期,是不应该在类的定义中确定的,因为一个类的定义是独立的,定义好之后,使用者可以用任何一种容器支持的生命周期来注册实例。但是此时这样的设计是比较合理的,因为我们要解决的是应用层服务的批量注册,这类服务一般在定义的时候就已经确定了使用方式,而且很多时候服务的开发者就是该服务的使用者!所以我们可以把这个当成合理的反范式设计。

    目前这样子,对于我来说,基本已经够用了,因为在应用层,我都是依赖实现编程的😀(哈哈,会不会很多人说咦......呢?)。设计模式说:“要依赖于抽象,不要依赖于具体”,这点我还没做到,我抽空检讨(呵呵,谁信呢!)。所以呢,我们的批量注入要支持那些优秀的同学。

    从上面的代码不难发现,如果定义接口IA和其实现A:IA,并在A上添加AppService特性是不行的:

      public interface IA { }
    
      [AppService]
      public class A : IA { }
    
    

    这个时候我们并不能依赖IA编程,因为我们注册的服务类是A,实现类是A,我们需要注册成服务类是IA,实现类是A才可:

    public class HomeController : Controller
    {
      private IA a;
      public HomeController(IA a)
      {
        this.a = a; //这里a是null,不能使用
      }
    }
    

    让我继续改进,在AppServiceAttribute中,我们加入服务类型的信息:

    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public class AppServiceAttribute : Attribute
    {
      /// <summary>
      /// 生命周期
      /// </summary>
      public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Singleton;
      /// <summary>
      /// 指定服务类型
      /// </summary>
      public Type ServiceType { get; set; }
      /// <summary>
      /// 是否可以从第一个接口获取服务类型
      /// </summary>
      public bool InterfaceServiceType { get; set; } = true;
    }
    

    我们从两个方面入手来解决服务类型的问题,一个是指定ServiceType,这个就毫无疑问了,在A的AppService中可以明确指定IA为其服务类:

    [AppService(ServiceType = typeof(IA))]
    public class A : IA { }
    

    另一个是从服务类自身所继承的接口中获取服务类形,这一点要在AddAppServices方法中体现了,再次改进AddAppServices方法,还是替换最开始services.AddSingleton(type)的位置:

      var serviceType = serviceAttribute.ServiceType;
      if (serviceType == null && serviceAttribute.InterfaceServiceType)
      {
        serviceType = type.GetInterfaces().FirstOrDefault();
      }
      if (serviceType == null)
      {
        serviceType = type;
      }
      switch (serviceAttribute.Lifetime)
      {
        case ServiceLifetime.Singleton:
          services.AddSingleton(serviceType, type);
          break;
        case ServiceLifetime.Scoped:
          services.AddScoped(serviceType, type);
          break;
        case ServiceLifetime.Transient:
          services.AddTransient(serviceType, type);
          break;
        default:
          break;
      }
    

    我们首先检查serviceAttribute.ServiceType,如果有值的话,它就是注册服务的类型,如果没有的话,看是否允许从接口中获取服务类型,如果允许,便尝试获取第一个作为服务类型,如果还没获取到,就把自身的类型作为服务类型。

    • 第一种情况不常见,特殊情况才会指定ServiceType,因为写起来麻烦;
    • 第二种情况适用于依赖抽象编程的同学,注意这里只取第一个接口的类型;
    • 第三种情况就是适用于像我这种有不良习惯的患者(依赖实现编程)!

    到此为止我们的服务注册已经讨论完了,下面看看如何获取。

    字段和属性注入

    这里我们说的获取,不是框架默认容器提供的构造器注入,而是要实现字段和属性注入,先看看构造器注入是什么样的:

    public class HomeController : Controller
    {
      UserService userService;
      OrderService orderService;
      MsgService msgService;
      OtherService otherService;
      OtherService2 otherService2;
    
      public HomeController(UserService userService, OrderService orderService, MsgService msgService, OtherService otherService, OtherService2 otherService2)
      {
        this.userService = userService;
        this.orderService = orderService;
        this.msgService = msgService;
        this.otherService = otherService;
        this.otherService2 = otherService2;
      }
    }
    
    

    如果引用的服务不再添加还好,如果编写边添加就太要命了,每次都要定义字段、在构造器方法签名中些添加参数、在构造器中赋值,便捷性和Spring的@autowired注解没法比,所以我们要虚心学习,创作更便捷的操作。
    首先我们再定义个特性,叫AutowiredAttribute,虽然也是个标识,但是由于这个特性是用在字段或者属性上,所以只能用特性Attribute,而不能使用接口Interface,到这里我们又发现一点,使用接口作为标识的话,只能用在类、接口和结构中,而不能用在他们的成员上,毕竟接口的主要作用是定义一组方法契约(即抽象)!

    [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
    public class AutowiredAttribute : Attribute
    {
    }
    

    这个特性里面什么也没有,主要是下面这个类,装配操作都在这里:

    /// <summary>
    /// 从容器装配service
    /// </summary>
    [AppService]
    public class AutowiredService
    {
      IServiceProvider serviceProvider;
      public AutowiredService(IServiceProvider serviceProvider)
      {
        this.serviceProvider = serviceProvider;
      }
      public void Autowired(object service)
      {
        var serviceType = service.GetType();
        //字段赋值
        foreach (FieldInfo field in serviceType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
          var autowiredAttr = field.GetCustomAttribute<AutowiredAttribute>();
          if (autowiredAttr != null)
          {
            field.SetValue(service, serviceProvider.GetService(field.FieldType));
          }
        }
        //属性赋值
        foreach (PropertyInfo property in serviceType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
          var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
          if (autowiredAttr != null)
          {
            property.SetValue(service, serviceProvider.GetService(property.PropertyType));
          }
        }
      }
    }
    

    我们刚刚写的[AppService]特性在这里已经用上了,并且这个类使用构造器注入了IServiceProvider。Autowired(object service)方法的参数是要装配的服务实例,首先获取服务类型,再使用反射查询有AutowiredAttribute特性的字段和属性,我们在构造器注入了serviceProvider,这里便可以使用serviceProvider的GetService方法从容器中获取对应类型的实例来给字段和属性赋值。 整个过程就是这样,简单明了。开始的时候我想使用静态类来编写AutowiredService,但是静态类没法注入IServiceProvider,解决方法也有,可以使用定位器模式全局保存IServiceProvider:

    /// <summary>
    /// 服务提供者定位器
    /// </summary>
    public static class ServiceLocator
    {
      public static IServiceProvider Instance { get; set; }
    }
    

    在Setup的Configure方法中赋值:

    ServiceLocator.Instance = app.ApplicationServices;
    

    这样在静态的AutowiredService中也就可以访问IServiceProvider了,但是使其自己也注册成服务能更好的和其他组件交互,java有了spring框架,大家都认可spring,一切都在容器中,一切都可注入,spring提供了统一的对象管理,非常好,我感觉netcore的将来也将会是这样。

    Autowired(object service)方法的实现虽然简单,但是使用了效率底下的反射,这个美中不足需要改进,以前可以使用晦涩难懂的EMIT来编写,现在有Expression,编写和阅读都简单了好多,并且效率也不比EMIT差,所以我们使用表达式+缓存来改进。Autowired方法要做的就是从容器中取出合适的对象,然后赋值给service要自动装配的字段和属性,据此我们先编写出委托的伪代码:

    (obj,serviceProvider)=>{
      ((TService)obj).aa=(TAAType)serviceProvider.GetService(aaFieldType);
      ((TService)obj).bb=(TBBType)serviceProvider.GetService(aaFieldType);
      ...
    }
    

    注意伪代码中的类型转换,Expression表达式在编译成委托时是非常严格的,所有转换都不能省。写表达式的时候我习惯先写伪代码,我希望大家也能养成这个习惯!有了伪代码我们可以开始改造AutowiredService类了:

      /// <summary>
      /// 从容器装配service
      /// </summary>
      [AppService]
      public class AutowiredService
      {
        IServiceProvider serviceProvider;
        public AutowiredService(IServiceProvider serviceProvider)
        {
          this.serviceProvider = serviceProvider;
        }
    
        Dictionary<Type, Action<object, IServiceProvider>> autowiredActions = new Dictionary<Type, Action<object, IServiceProvider>>();
    
        public void Autowired(object service)
        {
          Autowired(service, serviceProvider);
        }
        /// <summary>
        /// 装配属性和字段
        /// </summary>
        /// <param name="service"></param>
        /// <param name="serviceProvider"></param>
        public void Autowired(object service, IServiceProvider serviceProvider)
        {
          var serviceType = service.GetType();
          if (autowiredActions.TryGetValue(serviceType, out Action<object, IServiceProvider> act))
          {
            act(service, serviceProvider);
          }
          else
          {
            //参数
            var objParam = Expression.Parameter(typeof(object), "obj");
            var spParam = Expression.Parameter(typeof(IServiceProvider), "sp");
    
            var obj = Expression.Convert(objParam, serviceType);
            var GetService = typeof(IServiceProvider).GetMethod("GetService");
            List<Expression> setList = new List<Expression>();
    
            //字段赋值
            foreach (FieldInfo field in serviceType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
              var autowiredAttr = field.GetCustomAttribute<AutowiredAttribute>();
              if (autowiredAttr != null)
              {
                var fieldExp = Expression.Field(obj, field);
                var createService = Expression.Call(spParam, GetService, Expression.Constant(field.FieldType));
                var setExp = Expression.Assign(fieldExp, Expression.Convert(createService, field.FieldType));
                setList.Add(setExp);
              }
            }
            //属性赋值
            foreach (PropertyInfo property in serviceType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
              var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
              if (autowiredAttr != null)
              {
                var propExp = Expression.Property(obj, property);
                var createService = Expression.Call(spParam, GetService, Expression.Constant(property.PropertyType));
                var setExp = Expression.Assign(propExp, Expression.Convert(createService, property.PropertyType));
                setList.Add(setExp);
              }
            }
            var bodyExp = Expression.Block(setList);
            var setAction = Expression.Lambda<Action<object, IServiceProvider>>(bodyExp, objParam, spParam).Compile();
            autowiredActions[serviceType] = setAction;
            setAction(service, serviceProvider);
          }
        }
      }
    
    

    代码一下子多了不少,不过由于我们前面的铺垫,理解起来也不难,至此自动装配字段和属性的服务已经写好了,下面看看如何使用:

    编写服务类,并添加[AppService]特性

    [AppService]
    public class MyService
    {
      //functions
    }
    

    在Setup的ConfigureServices方法中注册应用服务

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddMvc();
      //注册应用服务
      services.AddAppServices();
    }
    

    在其他类中注入使用,比如Controller中

    public class HomeController : Controller
    {
      [Autowired]
      MyUserService myUserService;
    
      public HomeController(AutowiredService autowiredService)
      {
        autowiredService.Autowired(this);
      }
    }
    
    

    HomeController的构造函数是不是简洁了许多呢!而且再有新的服务要注入,只要定义字段(属性也可以,不过字段更方便)就可以了,注意:我们定义的字段不能是只读的,因为我们要在AutowiredService中设置。我们还用上面的例子,看一下它的威力吧!

    public class HomeController : Controller
    {
      [Autowired]
      UserService userService;
      [Autowired]
      OrderService orderService;
      [Autowired]
      MsgService msgService;
      [Autowired]
      OtherService otherService;
      [Autowired]
      OtherService2 otherService2;
    
      public HomeController(AutowiredService autowiredService)
      {
        autowiredService.Autowired(this);
      }
    }
    
    

    感谢您的观看!全文已经完了,我们没有使用第三方容器,也没有对自带的容器大肆修改和破坏,只是在服务类的构造器中选择性的调用了AutowiredService.Autowired(this)方法,为什么是选择性的呢,因为你还可以使用在构造器中注入的方式,甚至混用,一切都好,都不会错乱。

    nuget安装:

    PM> Install-Package Autowired.Core

    git源码:

    [Autowired.Core] https://gitee.com/loogn/Autowired.Core

    更新:

    • 支持多个AppServiceAttribute,
    • 支持服务唯一标识,通过Identifier指定服务实现