never下ioc 生命周期 注册规则 容器定义 环境的自动注入 其他IoC的结合使用方案

当前分单例,作用域(范围),短暂。单例是整个服务中只有一个实例,短暂则是每一次得到的都是新的实例,作用域就是在该一套行动中内得到的是同一个实例,该行动中指的是什么?我们看看demo下的startup里面一个方法

                using (var sc = x.ServiceLocator.BeginLifetimeScope())
                {
                    var serv = sc.Resolve<IUserService>();
                    sc.Resolve<IVCodeService>();
                    sc.Resolve<IUserService>();
                    sc.Resolve<IUserProxyService>();
                    sc.Resolve<Controllers.LoginController>();
                    var logger = sc.Resolve<ILoggerBuilder>().Build(typeof(Startup));
                    logger.Info("startup at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                }
View Code

这里using块代码就是我们使用了一个作用域的例子,所以作用域应该是指一件事的整个过程(这件事里面拆分了几个子事件,每个子事件又可以是一个作用域)。

在web模式中,从beginreqeust到endrequest,我们都可以认为从开始到结束的一种作用域,这就是web的周期。autofac对周期的描述:IoC之AutoFac(三)——生命周期

easyioc中用了ILifetimeScopeTracker接口让使用者去管理作用域周期,比如想在web实现的begin+end周期,ILifetimeScope StartScope(ILifetimeScope parent)方法中返回的对象使用HttpContext.Item去管理就可以了。

每一次使用都要开启一个作用域,将要释放资源的对象(实现了IDisposable 接口)放到ILifetimeScope的上下文的释放队列中,用于等下被调用方法释放。单例不会进入ILifetimeScope的释放队列中,而短暂 + 作用域的就有可能被加入到队列中(有可能对象没有实现IDisposable接口)。

    /// <summary>
    /// 组件生命范围定义跟踪者
    /// </summary>
    public interface ILifetimeScopeTracker
    {
        /// <summary>
        /// 开始一个范围
        /// </summary>
        /// <param name="parent"></param>
        /// <returns></returns>
        ILifetimeScope StartScope(ILifetimeScope parent);

        /// <summary>
        /// 清空所有范围
        /// </summary>
        void CleanScope();
    }
View Code

当前easyioc中有 DefaultLifetimeScopeTracker,ThreadLifetimeScopeTracker,WebLifetimeScopeTracker三个作用域跟踪者。

  1. DefaultLifetimeScopeTracker
    #region ILifetimeScopeTracker
    
            /// <summary>
            /// 开始一个范围
            /// </summary>
            /// <param name="parent"></param>
            /// <returns></returns>
            public virtual ILifetimeScope StartScope(ILifetimeScope parent)
            {
                return parent == null ? parent : parent.BeginLifetimeScope();
            }
    
            /// <summary>
            /// 结束所有范围
            /// </summary>
            public virtual void CleanScope()
            {
            }
    
            #endregion ILifetimeScopeTracker
    View Code

    可以看到,该对象始终都会开启范围,由于参数ILifetimeScope parent始终是系统ILifetimeScope第一个实例,每一次BeginLifetimeScope得到的对象都是新的一个ILifetimeScope实例。

  2. ThreadLifetimeScopeTracker
    private readonly System.Threading.ThreadLocal<ILifetimeScope> threadLocal = null;
       
         public override ILifetimeScope StartScope(ILifetimeScope parent)
            {
                if (this.threadLocal.IsValueCreated)
                    return this.threadLocal.Value;
    
                return this.threadLocal.Value = base.StartScope(parent);
            }
    
            public override void CleanScope()
            {
                if (this.threadLocal.IsValueCreated && this.threadLocal.Value != null)
                {
                    this.threadLocal.Value.Dispose();
                    this.threadLocal.Value = null;
                }
    
                base.CleanScope();
            }
    View Code

    使用了System.Threading.ThreadLocal<T>去管理当前ILifetimeScope,跟名字一样,用在线程管理的场景,但是异步线程会有切换问题,可以看看AsyncLocal<T>的来源。

  3. WebLifetimeScopeTracker
    public override ILifetimeScope StartScope(ILifetimeScope parent)
            {
                return new HttpThreadCache().Get("BeginLifetimeScope", () => base.StartScope(parent));
            }
    
            public override void CleanScope()
            {
                var cache = new HttpThreadCache();
                var scope = cache.Get<ILifetimeScope>("BeginLifetimeScope");
                if (scope != null)
                    scope.Dispose();
    
                cache.Remove("BeginLifetimeScope");
                base.CleanScope();
            }
    View Code
    static HttpThreadCache()
            {
                asyncLocak = new AsyncLocal<IDictionary>();
                init = new Func<IDictionary>(() =>
                {
    #if NET461
                    if (HttpContext.Current == null)
                        goto _do;
    
                    if (HttpContext.Current.Items.Contains(key))
                        return System.Web.HttpContext.Current.Items[key] as Hashtable;
    
                    var result = new Hashtable();
                    HttpContext.Current.Items[key] = result;
    
                    return result;
    #else
                    goto _do;
    #endif
                _do:
                    {
                        if (asyncLocak.Value == null)
                            asyncLocak.Value = new Hashtable();
    
                        return asyncLocak.Value;
                    }
                });
            }
    View Code

    web周期的跟踪者,HttpThreadCached对象就是在framework中使用了上面说到的HttpContent.Item去管理,非framework则使用了System.Thread.AsyncLocal<T>去管理。framework下相对ThreadLifetimeScopeTracker无非就是将周期拉长而已

注册规则

ioc.RegisterType<T,IT>(string key,lifestyle style) 像这样的方法注入了IT接口T实现的一个规则,key可以为空。在easyioc中还可以注入回调方法去构造对象

        /// <summary>
        /// 注册对象实例映射关系
        /// </summary>
        /// <typeparam name="TService">服务类型</typeparam>
        /// <param name="mission">回调生成</param>
        /// <param name="key">key</param>
        /// <param name="lifeStyle">生命周期</param>
        /// <returns></returns>
        public void RegisterCallBack<TService>(string key, ComponentLifeStyle lifeStyle, Func<ILifetimeScope, TService> mission)
        {
            if (this.option.Value.Unabled)
                throw new InvalidException("the builder is builded,can not update rules");

            var rule = new RegisterRuleCollector(1);
            rule.RegisterCallBack(key, lifeStyle, mission);
            register.Update(rule);
        }
View Code

所有的注册规则要遵守:

  1. T必定是可实例化的(即便在回调注入中,自己返回的T也是要自己构造出来),IT可以是接口,也可以是对象
  2. 多次注册相同的实例,是合理的,并不会出现前浪被后浪拍死,只是后面会引发Resolve的优先级问题。
  3. 每个注册规则RegisterRule都有唯一标识,该标识内部自动生成。

注册规则对象RegisterRule

该对象的定义比较复杂,实际上你可以理解这里是保存了4个核心对象:T,IT,key,lifestyle。我们上面ioc.RegisterType<T,IT>(string key,lifestyle style)方法用到的对象就是这个RegisterRule对象了。

    /// <summary>
    /// 注册规则
    /// </summary>
    public class RegisterRule : IEquatable<RegisterRule>, ICloneable, IDisposable, IRegisterRule, IParameterRegisterRule, IProxyRegisterRule, IRegisterRuleDescriptor
    {
          .....
    }
  1. IEquatable<RegisterRule>接口 实现两个规则相等性,每个规则有唯一Id,故里面必定使用上该Id去区分
    private string ConcatCachedKey()
            {
                switch (this.lifeStyle)
                {
                    case ComponentLifeStyle.Singleton:
                        {
                            return string.Concat("s", this.key, "_", increment);
                        }
                    case ComponentLifeStyle.Transient:
                        {
                            return string.Concat("t", this.key, "_", increment);
                        }
                    case ComponentLifeStyle.Scoped:
                        {
                            return string.Concat("l", this.key, "_", increment);
                        }
                }
    
                return this.serviceType.FullName;
            }
    View Code

    在这里我们加上key和style表示一些额外的信息,实际完全可以用该Id去对比。

  2. ICloneable 接口,用来克隆该规则,目前用于生成代理用到 + 泛型规则,生成代理和IProxyRegisterRule配合使用,主要思想是生成的代理实现了被代理对象的功能,使用了装饰者设计模式,代理类注入了被代理的对象,此时代理类也被当生成新的注册规则;注入是泛型Repository<T>,等下要Resolve的是Repostory<int>,这样Repostory<int>从Repository<T>规则克隆出来。
  3. IRegisterRuleDescriptor 描述规则属性,必定包含了4个核心对象:T,IT,key,lifestyle;还带有其他属性,比如Parameters属性,表示这个规则匹配构造参数可指定特定参数。
  4. IParameterRegisterRule 参数注册规则,用于规则指定使用某个规则注入(系统注入多个IA接口,比如AA,BA,那么该方法可以指定注入AA,否则系统会找到BA)
    /// <summary>
        /// 参数注册规则
        /// </summary>
        public interface IObviousProxyRegisterRule
        {
            /// <summary>
            /// 构造函数参数
            /// </summary>
            /// <typeparam name="TService">服务类型</typeparam>
            /// <param name="key">注册key</param>
            /// <returns></returns>
            IObviousProxyRegisterRule WithParameter<TService>(string key);
        }
    View Code
  5. IProxyRegisterRule 代理注册规则,可以注入多个拦截器
            IProxyRegisterRule WithInterceptor<TInterceptor>(string key) where TInterceptor : Never.Aop.IInterceptor;

    拦截器定义如下:

    /// <summary>
        /// 拦截接口
        /// </summary>
        public interface IInterceptor
        {
            /// <summary>
            /// 在对方法进行调用前
            /// </summary>
            /// <param name="invocation">调用信息</param>
            void PreProceed(IInvocation invocation);
    
            /// <summary>
            /// 对方法进行调用后
            /// </summary>
            /// <param name="invocation">调用信息</param>
            void PostProceed(IInvocation invocation);
        }
    View Code

    可以扩展一下:在webapi请求过程中,对每个方法调用进行监督其性能,可以使用该特性注入性能监督拦截器

规则构建者RegisterRuleBuilder

对每一条使用到的规则,去进行实例化的构建;RegisterRuleBuilder该对象分析规则的构造函数,找到适当的构造方法(含参数),使用emit去调用该构造而去实例目标对象(指的是规则里面的T目标),将构造好的方法缓存起来放到RegisterRule的Builder与OptionalBuilder这2个属性

  1. 生命周期的相容,通常来说,单例可以注入任何周期中,作用域只能注入到作用域+短暂中,短暂只能注入到短暂;而easyioc遵守该规则
            /// <summary>
            /// 是否相容的周期
            /// </summary>
            /// <param name="current">当前周期</param>
            /// <param name="target">目标周期</param>
            /// <returns></returns>
            public static string Compatible(this RegisterRule target, RegisterRule current)
            {
                switch (current.LifeStyle)
                {
                    /*单例可以注入到任何实例中,其构造只能是单例对象*/
                    case ComponentLifeStyle.Singleton:
                        {
                            return string.Empty;
                        }
                    /*短暂只能注入到短暂,其构造可接受任何实例对象*/
                    case ComponentLifeStyle.Transient:
                        {
                            if (target.LifeStyle != ComponentLifeStyle.Transient)
                                return string.Format("构建当前对象{0}为{1},期望对象{2}为短暂,不能相容",
                                    target.ServiceType.FullName,
                                    target.LifeStyle == ComponentLifeStyle.Scoped ? "作用域" : "单例",
                                    current.ServiceType.FullName);
    
                            return string.Empty;
                        }
                    /*作用域其构造不能接受短暂,可接受有作用域和单例*/
                    case ComponentLifeStyle.Scoped:
                        {
                            if (target.LifeStyle == ComponentLifeStyle.Singleton)
                                return string.Format("构建当前对象{0}为单例,期望对象{1}为作用域,不能相容",
                                    target.ServiceType.FullName,
                                    current.ServiceType.FullName);
    
                            return string.Empty;
                        }
                }
    
                return string.Empty;
            }
    View Code
    代码说明:target参数指的是目标对象,curren指得是目标对象构造方法里面的参数。为什么要遵守该规则?2个例子:(1)比如单例注入短暂的参数,很明显短暂有可能只能依赖HttpContent,但是在单例中,在非Web执行环境中,这个短暂的实例就会有HttpContent为空的错误。(2)短暂参数被设计为构造的时候开启事务 + 被disponse的时候释放,被注入到单例对象后这个事务一直开户并且造成不disponse的后果。
  2. 其他工具的生命周期的相容性,对于autofac,netcore的provider,似乎对上面的相容性没有那么大的限制,因此在easyioc中使用Resolveoptional则可以不用遵守上述相容规则:就是处理过程中优先遵守规则,出现问题至少使用一个规则,这样可以保证Resolve可正常得到对象。
  3. 构造者会检查循环引用,一旦发现有死循环引用,则抛异常
                /*选择策略*/
                if (level > 0)
                {
                    /*递归检查*/
                    foreach (var re in recursion)
                    {
                        if (re.ImplementationType == rule.ImplementationType)
                        {
                            throw new ArgumentOutOfRangeException(string.Format("{0}和{1}类型形成递归调用", re.ImplementationType.FullName, rule.ImplementationType.FullName));
                        }
                    }
    
                    if (recursion[recursion.Count - 1] != null)
                    {
                        RuleMatchUsingNegativeSort(rule, recursion[recursion.Count - 1]);
                    }
                }
    View Code

    代码中RuleMatchUsingNegativeSort是检查规则的相容性。

  4. ResolveAll<T>去构建数组对象的,永不返回null,至少返回new T[0]
  5. Resolve过程中如果构造方法参数Generic<int>是泛型Generic<T>注入的,则找到Generic<T>规则后重新构造一个Generic<int>的规则(该新规则被缓存到T目标对象规则里面的数组里面,可以看看ReigsterRule的实现)
  6. 系统默认注入了数组和字典的注册规则,考虑到我们只注入了<T,T>(key,style),如果我们Resolve<IEnumerable<T>>,系统没有数组的注入规则,则放方法直接抛异常。。
  7. 系统注入多个IA接口,比如AA,BA。当要Resolve<IA>的时候,先将BA,AA都查询出来到某个集合,再按策略去使用BA还是AA,策略当前是:先是key是否相等,再是相容性(如果不是ResolveOptional方法的话),然后是加入时序:从尾到首,因此BA的概率会比AA的概率大。

容器定义

实际上叫容器是要在不同场景的叫法,比如我们的注册规则也要有个集合,保存着所有的规则,我们也叫容器,而相对于整个系统来说,注入Register,构建Resolve等所有组件组合起来,这也是容器(easyContainer的面貌)

    /// <summary>
    /// IoC容器接口
    /// </summary>
    public interface IContainer
    {
        /// <summary>
        /// 服务注册器
        /// </summary>
        IServiceRegister ServiceRegister { get; }

        /// <summary>
        /// 服务定位器
        /// </summary>
        IServiceLocator ServiceLocator { get; }

        /// <summary>
        /// 服务创建器
        /// </summary>
        IServiceActivator ServiceActivator { get; }

        /// <summary>
        /// 类型发现者
        /// </summary>
        ITypeFinder TypeFinder { get; }
    }
View Code
  1. ServiceRegister 对象,注册规则
  2. ServiceLocator 对象,Resolve对象
  3. ServiceActivator 一些没有注入的对象,可以使用规则去构造一个(生成规则过程会特别一点,跟启动顺序有关系),跟Activator.CreateInstance差不多相同的方式。
  4. TypeFinder 类型发现者,协助查询特定类。
    /// <summary>
    /// IoC容器
    /// </summary>
    public class EasyContainer : Never.IoC.IContainer, Never.IoC.IContainerStartup, IValuableOption<UnableRegisterRule>
  1. IContainerStartup接口定义容器的启动行为:初始化,启动中。初始化Init(),里面可以注入规则,触发OnIniting事件。启动中Startup(),也可以注入规则,触发OnStarting事件。两者有什么区别?还记得netcore下的startup启动代码吗UseEasyIoC的两个回调方法分别是对OnIniting和OnStarting两个事件的一个委托绑定,两者的区别就是里面说的“ioc分开2种启动方法:第一与最后,主要原因如下:(1)服务启动有先后顺序,不同的系统组件所注册的顺序不同的,但有些组件要求在所有环境下都只有第一或最后启动(2)由于使用环境自动注册这种设计下,一些组件要手动注册会带自己的规则就会被自动注册覆盖”。第2个原因可以解释为:当你注入的对象有可能被当成一种组件而系统自动化注入了,但实际上我们又想手动加一些参数注入,所以我们可以在Startup方法调用就可以了。
  2. IValuableOption<UnableRegisterRule> 该接口是用来限制注册规则的注入时机。想想一个场景下,我们在时刻A的时候Resolve<IA>用的是AA规则,实际上我们一直期望后面用到IA的都是AA规则就好了,此时时刻B如果再注入了一个BA,我们期望一直使用AA就出现麻烦,按注入规则后再要使用到Resolve<IA>是会找到BA规则的,当然有人说BA带个key注入也是个解决办法。但是为了保护规则不被破坏,我们就要设定一旦系统组件已经初始化后(Startup调用方法)就不再接受注入规则。按这个定义我们应该在注入规则的容器中应该会有这样的判断,追随代码可以看到RegisterRuleContainer里面的Update方法
            /// <summary>
            /// 更新容器规则
            /// </summary>
            /// <param name="collector"></param>
            public void Update(RegisterRuleCollector collector)
            {
                if (option != null && option.Value.Unabled)
                    return;
    ....
    }
    View Code

    而且相对接近使用者层的ServiceRegister对象,则是直接抛异常

            public void RegisterType(Type implementationType, Type serviceType, string key, ComponentLifeStyle lifeStyle)
            {
                if (this.option.Value.Unabled)
                    throw new InvalidException("the builder is builded,can not update rules");
    
                var rule = new RegisterRuleCollector(1);
                rule.RegisterType(implementationType, serviceType, key, lifeStyle);
                register.Update(rule);
            }
    View Code

    还记得Autofac里面ContainerBuilder的Update方法?官方目前已经被标识为废弃方法,大伙可以讨论一下为什么会这样。

环境的自动注入

可能懒惰的原因,我们不用每一次都手动注入<AA,IA>,<BA,IA>这种规则,所以我们可以定义扫描程序集去找到AA,BA后注入。举个栗子,程序C我不想BA注入,程序D又想只用BA,程序E两者都可以,因此不同环境下扫描程序集后想要注入AA和BA也要有策略。

假设<AA,IA>规则是单例 + 运行环境是"programc",<BA,IA>规则是作用域 + 运行环境是"programd",程序C的运行环境是“programc",规则扫描者是扫描单例的,而程序D运行环境是”programd",规则扫描者是扫描线程的,程序E的运行环境是""(可认为*,可以匹配所有环境),规则扫描者是扫描线程的+扫描单例的。在这三种环境中,可以得出,程序C环境匹配 + 单例扫描者扫描到AA,可以注入<AA,IA>,单例扫描者扫描不到BA这个类型(为什么描述不到?一个是环境,一个是单例只匹配单例,不匹配作用域+短暂),所以不会注入<BA,IA>,程序E则可以注入<BA,IA>,<AA,IA>,而程序D本身环境不是”grogramc",直接环境不匹配<BA,IA>。系统默认实现了3个扫描者:

  1. ScopedAutoInjectingEnvironmentProvider + ScopedAutoInjectingAttribute  对带有ScopedAutoInjectingAttribute特性的对象,如果Env跟ScopedAutoInjectingEnvironmentProvider相匹配,那么这个对象就被注入为作用域周期。
  2. SingletonAutoInjectingEnvironmentProvider +  SingletonAutoInjectingAttribute 对带有SingletonAutoInjectingAttribute 特性的对象,如果Env跟SingletonAutoInjectingEnvironmentProvider相匹配,那么这个对象就被注入为单例周期。
  3. TransientAutoInjectingEnvironmentProvider +  TransientAutoInjectingAttribute 对带有TransientAutoInjectingAttribute 特性的对象,如果Env跟TransientAutoInjectingEnvironmentProvider相匹配,那么这个对象就被注入为短暂周期。

在代码中我们发现IAutoInjectingEnvironmentRuleCollector定义,这个接口的作用是什么?里面方法Register参数中的collector是什么对象?

当前我们有好几个IoC工具,第一种工具都有自己的实现方法,特别是其Container的核心设计,这个核心有些的方法我们想用的话,构架就要将其暴露出去,只不过构架要抽象出来方便做适配,因此IAutoInjectingEnvironmentRuleCollector接口可以让不同的工具做适配工具而已。

什么时候会调用这个自动注入的接口方法?

void Call(AutoInjectingGroupInfo[] groups, IContainerStartupEventArgs eventArgs)

我们先去看看扩展方法Never.StartupExtension.UseAutoInjectingAttributeUsingIoC方法,第二个参数接受的是IAutoInjectingEnvironmentProvider[] providers,一个数组,说明我们环境可以有多个扫描规则者

        /// <summary>
        /// 在sampleioc中自动使用属性发现注入
        /// </summary>
        /// <param name="startup"></param>
        /// <param name="providers"></param>
        /// <returns></returns>
        public static ApplicationStartup UseAutoInjectingAttributeUsingIoC(this ApplicationStartup startup, IAutoInjectingEnvironmentProvider[] providers)
        {
            startup.RegisterStartService(new AutoInjectingStartupService(providers));
            return startup;
        }
View Code

跟踪到里面的AutoInjectingStartupService类型,我们发现环境自动注入是使用了IContainerStartup接口的OnStarting事件,IContainerStartup接口则是定义了IContainer的启动过程,OnStarting事件必定是Container里面调用的,我们也发现IContainerStartupEventArgs对象的属性Collector被设定为object类型,跟我们上面说的IAutoInjectingEnvironmentRuleCollector接口方法Register参数的collector一样的设计。

    /// <summary>
    /// 容器初始化过程事件
    /// </summary>
    public class IContainerStartupEventArgs : EventArgs
    {
        /// <summary>
        /// 类型发现者
        /// </summary>
        public ITypeFinder TypeFinder { get; }

        /// <summary>
        /// 程序集
        /// </summary>
        public IEnumerable<Assembly> Assemblies { get; }

        /// <summary>
        /// app
        /// </summary>
        public object Collector { get; }
    }
View Code

实际上无论是OnIniting事件还是OnStarting事件,我们会将Collector对象设计为每种IoC技术方案的规则容器,比如Autofac的是Autofac.ContainerBuilder类型,StructureMap的是StructureMap.Container类型,都只是让使用者可以直接使用Autofac.ContainerBuilder或StructureMap.Container的友好特性而已,当然前提你要知道你当前使用的是Autofac,还是StructureMap或者是EasyIoC。

其他IoC的结合使用方案

如果我先使用autofac来替换easyioc怎么办?先去github下载never的扩展信息

我们可以打开Never.IoC.Autofac项目代码发现,实际上也是实现了上面说到的IContainer,IServiceLocator,IServiceActivator,IServiceRegister,ILifetimeScope5个核心接口,然后在Startup对象中ApplicationStartup实例使用.UseAutofac()方法就可以了。

而环境的自动注入解决方案:实现IAutoInjectingEnvironmentRuleCollector接口,传入到TransientAutoInjectingEnvironmentProvider构造就可以了,当前组件要自己实现哦,看着Never下面的AutoInjectingEnvironmentCollector对象就可以了

文章导航:

  1. never框架
  2. sqlcient 一套容易上手性能又不错的sqlhelper
  3. easySql使用xml管理带事务的orm