在netcore中實現字段和屬性注入

原文: 在netcore中實現字段和屬性注入

簡單來講,使用Ioc模式須要兩個步驟,第一是把服務註冊到容器中,第二是從容器中獲取服務,咱們一個一個討論並演化。這裏不會考慮使用如Autofac等第三方的容器來代替默認容器,只是提供一些簡單實用的小方法用於簡化應用層的開發。html

將服務注入到容器

asp.netcore官方給出的在容器中註冊服務方法是,要在Startup類的ConfigureServices方法中添加服務,以下所示:java

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddSingleton(typeof(UserService));
    services.AddSingleton(typeof(MsgService));
    services.AddSingleton(typeof(OrderService));
}

AddMvc方法添加了mvc模塊內部用到的一些服務,這個是封裝好的,一句話就好了,其餘第三方組件也都提供了相似的Add方法,把本身內部須要的服務都封裝好註冊進去了。可是咱們應用開發人員使用的類,仍是須要一個一個寫進去的,你們最多見的三層架構中的數據訪問層和業務邏輯層即是此類服務,上面代碼中我加入了三個業務服務類。這顯然不是長久之計,我想你們在開發中也會針對此問題作一些處理,這裏說下個人,僅供參考吧。git

解決方法就是批量註冊!說到批量,就須要一個東西來標識一批東西,而後用這一個東西來控制這一批東西。在.net程序的世界中,有兩個可選的角色,一個是接口Interface,另外一個是特性Attribute。spring

若是使用接口做爲標識來使用,限制就太死板了,一個標識的信息不是絕對的單一,是不推薦使用接口的,由於可能須要引入多個接口才能共同完成,因此我選擇特性做爲標識。特性相較與接口有什麼特色呢?特性在運行時是類的實例,因此能夠存儲更多的信息。編程

下面咱們簡單實現一個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屬性:mvc

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

Lifetime的默認值咱們設置成ServiceLifetime.Singleton是比較合適的,由於大部分服務咱們都但願使用單例註冊,一個合理的默認設置能夠節省使用者不少代碼,新手可能還會樂於複製粘貼,但老同志確定都深有體會。app

有了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);
            }
        }
    }

代碼一會兒多了很多,不過因爲咱們前面的鋪墊,理解起來也不難,至此自動裝配字段和屬性的服務已經寫好了,下面看看如何使用:

  1. 編寫服務類,並添加[AppService]特性

    [AppService]
    public class MyService
    {
        //functions
    }
  2. 在Setup的ConfigureServices方法中註冊應用服務

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        //註冊應用服務
        services.AddAppServices();
    }
  3. 在其餘類中注入使用,好比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
相關文章
相關標籤/搜索