never下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")); }
这里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(); }
当前easyioc中有 DefaultLifetimeScopeTracker,ThreadLifetimeScopeTracker,WebLifetimeScopeTracker三个作用域跟踪者。
- 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
可以看到,该对象始终都会开启范围,由于参数ILifetimeScope parent始终是系统ILifetimeScope第一个实例,每一次BeginLifetimeScope得到的对象都是新的一个ILifetimeScope实例。
- 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(); }
使用了System.Threading.ThreadLocal<T>去管理当前ILifetimeScope,跟名字一样,用在线程管理的场景,但是异步线程会有切换问题,可以看看AsyncLocal<T>的来源。
- 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(); }
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; } }); }
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); }
所有的注册规则要遵守:
- T必定是可实例化的(即便在回调注入中,自己返回的T也是要自己构造出来),IT可以是接口,也可以是对象
- 多次注册相同的实例,是合理的,并不会出现前浪被后浪拍死,只是后面会引发Resolve的优先级问题。
- 每个注册规则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 { ..... }
- 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; }
在这里我们加上key和style表示一些额外的信息,实际完全可以用该Id去对比。
- ICloneable 接口,用来克隆该规则,目前用于生成代理用到 + 泛型规则,生成代理和IProxyRegisterRule配合使用,主要思想是生成的代理实现了被代理对象的功能,使用了装饰者设计模式,代理类注入了被代理的对象,此时代理类也被当生成新的注册规则;注入是泛型Repository<T>,等下要Resolve的是Repostory<int>,这样Repostory<int>从Repository<T>规则克隆出来。
- IRegisterRuleDescriptor 描述规则属性,必定包含了4个核心对象:T,IT,key,lifestyle;还带有其他属性,比如Parameters属性,表示这个规则匹配构造参数可指定特定参数。
- 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); }
- 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); }
可以扩展一下:在webapi请求过程中,对每个方法调用进行监督其性能,可以使用该特性注入性能监督拦截器
规则构建者RegisterRuleBuilder
对每一条使用到的规则,去进行实例化的构建;RegisterRuleBuilder该对象分析规则的构造函数,找到适当的构造方法(含参数),使用emit去调用该构造而去实例目标对象(指的是规则里面的T目标),将构造好的方法缓存起来放到RegisterRule的Builder与OptionalBuilder这2个属性
- 生命周期的相容,通常来说,单例可以注入任何周期中,作用域只能注入到作用域+短暂中,短暂只能注入到短暂;而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; }
- 其他工具的生命周期的相容性,对于autofac,netcore的provider,似乎对上面的相容性没有那么大的限制,因此在easyioc中使用Resolveoptional则可以不用遵守上述相容规则:就是处理过程中优先遵守规则,出现问题至少使用一个规则,这样可以保证Resolve可正常得到对象。
- 构造者会检查循环引用,一旦发现有死循环引用,则抛异常
/*选择策略*/ 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]); } }
代码中RuleMatchUsingNegativeSort是检查规则的相容性。
- ResolveAll<T>去构建数组对象的,永不返回null,至少返回new T[0]
- Resolve过程中如果构造方法参数Generic<int>是泛型Generic<T>注入的,则找到Generic<T>规则后重新构造一个Generic<int>的规则(该新规则被缓存到T目标对象规则里面的数组里面,可以看看ReigsterRule的实现)
- 系统默认注入了数组和字典的注册规则,考虑到我们只注入了<T,T>(key,style),如果我们Resolve<IEnumerable<T>>,系统没有数组的注入规则,则放方法直接抛异常。。
- 系统注入多个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; } }
- ServiceRegister 对象,注册规则
- ServiceLocator 对象,Resolve对象
- ServiceActivator 一些没有注入的对象,可以使用规则去构造一个(生成规则过程会特别一点,跟启动顺序有关系),跟Activator.CreateInstance差不多相同的方式。
- TypeFinder 类型发现者,协助查询特定类。
/// <summary> /// IoC容器 /// </summary> public class EasyContainer : Never.IoC.IContainer, Never.IoC.IContainerStartup, IValuableOption<UnableRegisterRule>
- IContainerStartup接口定义容器的启动行为:初始化,启动中。初始化Init(),里面可以注入规则,触发OnIniting事件。启动中Startup(),也可以注入规则,触发OnStarting事件。两者有什么区别?还记得netcore下的startup启动代码吗,UseEasyIoC的两个回调方法分别是对OnIniting和OnStarting两个事件的一个委托绑定,两者的区别就是里面说的“ioc分开2种启动方法:第一与最后,主要原因如下:(1)服务启动有先后顺序,不同的系统组件所注册的顺序不同的,但有些组件要求在所有环境下都只有第一或最后启动(2)由于使用环境自动注册这种设计下,一些组件要手动注册会带自己的规则就会被自动注册覆盖”。第2个原因可以解释为:当你注入的对象有可能被当成一种组件而系统自动化注入了,但实际上我们又想手动加一些参数注入,所以我们可以在Startup方法调用就可以了。
- 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; .... }
而且相对接近使用者层的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); }
还记得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个扫描者:
- ScopedAutoInjectingEnvironmentProvider + ScopedAutoInjectingAttribute 对带有ScopedAutoInjectingAttribute特性的对象,如果Env跟ScopedAutoInjectingEnvironmentProvider相匹配,那么这个对象就被注入为作用域周期。
- SingletonAutoInjectingEnvironmentProvider + SingletonAutoInjectingAttribute 对带有SingletonAutoInjectingAttribute 特性的对象,如果Env跟SingletonAutoInjectingEnvironmentProvider相匹配,那么这个对象就被注入为单例周期。
- 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; }
跟踪到里面的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; } }
实际上无论是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对象就可以了
文章导航: