关于依赖注入
控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。应用控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用,传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转。
控制反转还有一个名字叫做依赖注入(Dependency Injection),简称DI。相对而言对于.Net开发者来说听到得更多的是依赖注入这个名词。实现依赖注入的框架有很多,如Spring.Net,Unity,Ninject等等,基本上萝卜白菜各有所爱,当然了更多的还是根本不尿依赖注入。
Unity(Unity Application Block)是微软patterns & practices组用C#实现的轻量级,可扩展的依赖注入容器,支持构造函数注入、属性注入、方法调用注入,开发者使用Unity可以很轻松的建立松散耦合的应用程序。关于Unity的常规使用方法可以参考 【ASP.Net MVC3 】使用Unity 实现依赖注入 ,本文主要探讨下接口到实现类的自动映射注册实现方案。
Unity容器接口和实现类的自动注册
众所周知我们使用依赖注入时要么使用配置文件建立接口和实现类的映射关系,要么使用代码直接注册依赖关系。这两种方法都有一个弊端那就是当你新添加了一个接口和接口的实现后必须去修改配置文件或者代码去注册新的映射关系,当你改了接口、类名后还得再次去修改映射关系。实际应用中更多的是使用配置文件来实现映射关系的注册,这对于Java来说就是天经地义的事,不过放到.Net来说完全不是那么回事,繁杂的配置文件,无尽的接口映射,重复的乏味的代码简直不可忍受,于是乎激发了实现全自动映射的念想。
原理:所谓的依赖注入无非就是根据配置文件或代码完成接口和实现类的映射关系,然后使用反射实现的接口实例化而已。
目的:使用Unity实现ASP.Net MVC3的依赖注入,实现松耦合设计,但不使用配置文件,将开发人员从繁琐的配置中解脱出来,将关注点放在业务领域设计实现上
思路:扫描应用程序域的的所有程序集,在Unity容器中完成实现了接口或继承了抽象类的Class完成接口到Class的映射关系注册
首先建立一个WebApp Mvc3项目,建立LazyRabbit.Domain和LazyRabbit.Infrastructure.Dependency类库,使用NuGet添加对向WebApp和LazyRabbit.Infrastructure.Dependency添加Unity引用,NuGet使用请参考http://www.cnblogs.com/lzrabbit/archive/2012/04/30/2476255.html
Install-Package Unity
完成后输出类似如下结果:
每个程序包的所有者将相应程序包授权给您。Microsoft 不负责也不会授予对第三方程序包的任何许可。有些程序包可能包含受其他许可证控制的依赖项。请访问程序包源(源) URL 以确定所有依赖项。 程序包管理器控制台主机版本 2.0.30619.9119 键入“get-help NuGet”以查看所有可用的 NuGet 命令。 PM> Install-Package Unity 正在尝试解析依赖项“CommonServiceLocator (≥ 1.0)”。 已成功安装“CommonServiceLocator 1.0”。 您正在从 Microsoft patterns & practices 下载 Unity,有关此程序包的许可协议在 http://www.opensource.org/licenses/ms-pl 上提供。请检查此程序包是否有其他依赖项,这些依赖项可能带有各自的许可协议。您若使用程序包及依赖项,即构成您接受其许可协议。如果您不接受这些许可协议,请从您的设备中删除相关组件。 已成功安装“Unity 2.1.505.0”。 已成功将“CommonServiceLocator 1.0”添加到 WebApp。 已成功将“Unity 2.1.505.0”添加到 WebApp。
继续向LazyRabbit.Infrastructure.Dependency项目添加System.Web.Mvc程序集引用,完成准备工作后来看我们的自动依赖注入实现核心类
在LazyRabbit.Infrastructure.Dependency项目添加DependencyContext类,完整代码如下
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Web; using System.Web.Mvc; using Microsoft.Practices.Unity; using System.Text; namespace LazyRabbit.Infrastructure.Dependency { /// <summary> /// Unity依赖注入辅助类 /// </summary> public sealed class DependencyContext { private static IUnityContainer _UnityContainer = new UnityContainer(); /// <summary> /// 获取依赖注入容器(单例模式) /// </summary> public static IUnityContainer Current { get { return _UnityContainer; } } private DependencyContext() { } /// <summary> /// 获取指定目录及其子目录的所有DLL文件路径集合 /// </summary> /// <param name="assemblyDirectory"></param> /// <returns></returns> private static List<string> GetAssemblyFiles() { string assemblyDirectory = AppDomain.CurrentDomain.BaseDirectory; if (HttpContext.Current != null) { assemblyDirectory = Path.Combine(assemblyDirectory, "Bin"); } string[] notFiles = new string[] { "EntityFramework.dll", "ICSharpCode.SharpZipLib.dll", "Ionic.Zip.dll", "log4net.dll", "Microsoft.ApplicationBlocks.Data.dll", "Microsoft.Practices.ServiceLocation.dll", "Microsoft.Practices.Unity.Configuration.dll", "Microsoft.Practices.Unity.dll", "Newtonsoft.Json.dll" }; //获取DLL文件 List<string> assemblyFiles = Directory.GetFiles(assemblyDirectory, "*.dll").Select(path => Path.GetFileName(path)).ToList(); //EXE可执行文件 assemblyFiles.AddRange(Directory.GetFiles(assemblyDirectory, "*.exe").Select(path => Path.GetFileName(path))); assemblyFiles = assemblyFiles.Where(f => !notFiles.Contains(f)).ToList(); return assemblyFiles; } /// <summary> /// 从当前应用程序域获取已加载的程序集 /// </summary> /// <param name="assemblyFiles"></param> /// <returns></returns> public static List<Assembly> LoadAssembly() { List<string> assemblyFiles = GetAssemblyFiles(); //加载程序集不能使用Assembly.LoadFile()方法,该方法会导致DLL文件占用无法释放,改为文件流加载方式 //return assemblyFiles.Select(assemblyFile => Assembly.Load(File.ReadAllBytes(assemblyFile))).ToList(); //放弃使用Assembly.Load方法加载程序集 //Assembly.Load方法返回的程序集和当前应用程序域运行的程序集是相互独立 //当使用Load方法加载程序集Assembly1并加载类型T1时,然后从应用程序中的程序集Assembly1加载一个类型T1, //尽管这两个类型看上去是完全相同的而且也确实是完全相同的,但这两个T1类型却不相同,因为这两个都叫T1的类型分属两个不同的Assembly //只有一种情况下才会相同即你使用的这个类型有一个接口,并且这个接口定义在其它Assembly中 //因此为了程序的兼容性这里采取从当前应用程序域获取程序集 return AppDomain.CurrentDomain.GetAssemblies().Where(assembly => assemblyFiles.Contains(assembly.ManifestModule.ScopeName)).ToList(); } /// <summary> /// 从程序集加载所有类(不包含接口、抽象类) /// </summary> /// <param name="assemblys"></param> /// <returns></returns> private static List<Type> GetClassTypes(List<Assembly> assemblys) { List<Type> types = new List<Type>(); assemblys.ForEach(assembly => { try { types.AddRange(assembly.GetTypes().Where(t => t.IsClass && !t.IsInterface && !t.IsAbstract)); } catch (ReflectionTypeLoadException ex) { //处理类型加载异常,一般为缺少引用的程序集导致 } }); return types; } /// <summary> /// 获取类型的所有集成、实现的接口抽象类 /// </summary> /// <param name="classType"></param> /// <returns></returns> public static List<Type> GetBaseTypes(Type classType) { HashSet<string> ignoreInterface = new HashSet<string> { typeof(ISingleton).ToString(), typeof(IWeakReference).ToString() }; List<Type> baseTypes = classType.GetInterfaces().Where(t => !IsSystemNamespace(t.Namespace) && !ignoreInterface.Contains(t.FullName)).ToList(); GetAbstructTypes(classType, baseTypes); return baseTypes; } /// <summary> /// 获取类型所有的抽象基类 /// </summary> /// <param name="classType"></param> /// <param name="abstructTypes"></param> public static void GetAbstructTypes(Type classType, List<Type> abstructTypes) { Type baseType = classType.BaseType; if (baseType != typeof(object) && baseType.IsAbstract && !IsSystemNamespace(baseType.Namespace)) { abstructTypes.Add(baseType); GetAbstructTypes(baseType, abstructTypes); } } /// <summary> /// 判断接口或抽象类是否为系统的命名空间 /// </summary> /// <param name="ns"></param> /// <returns></returns> private static bool IsSystemNamespace(string ns) { //常用系统命名空间 HashSet<string> sysNamespace = new HashSet<string> { "Microsoft.Xml", "System", "System.Collections", "System.ComponentModel", "System.Configuration", "System.Data", "System.IO", "System.Runtime", "System.ServiceModel", "System.Text", "System.Web", "System.Xml" }; return sysNamespace.Contains(string.Join(".", ns.Split('.').Take(2))); } /// <summary> /// 从指定的类型集合中过滤出从指定类或接口派生的类 /// </summary> /// <typeparam name="T">基类或接口</typeparam> /// <param name="classTypes">类型集合</param> /// <returns></returns> private static List<Type> GetDerivedClass<T>(List<Type> classTypes) where T : class { return classTypes.AsParallel().Where(t => t.GetInterface(typeof(T).ToString()) != null).ToList(); } /// <summary> /// 注册 /// </summary> /// <param name="types"></param> /// <param name="lifetimeManager"></param> private static void RegisterType<T>(List<Type> types) where T : LifetimeManager, new() { types.AsParallel().ForAll(classType => { List<Type> baseTypes = GetBaseTypes(classType).ToList(); foreach (Type baseType in baseTypes) { Current.RegisterType(baseType, classType, new T()); Current.RegisterType(baseType, classType, classType.FullName, new T()); } }); } /// <summary> /// 初始化依赖注入 /// 注册所有实现了ISingleton和IWeakReference接口的类型到IUnityContainer容器 /// </summary> private static void Init() { //默认的情况下使用TransientLifetimeManager管理对象的生命周期,它不会在container中保存对象的引用,简而言之每当调用Resolve或ResolveAll方法时都会实例化一个新的对象 //ContainerControlledLifetimeManager 单例模式,每次调用Resolve或ResolveAll方法都会调用同一个对象的引用 //ExternallyControlledLifetimeManager 弱引用 List<Assembly> assemblys = LoadAssembly(); List<Type> classTypes = GetClassTypes(assemblys); //所有接口默认注册为弱引用 RegisterType<ExternallyControlledLifetimeManager>(classTypes); //先注册单例,再注册若引用,确保若同时实现了ISingleton和IWeakReference接口,则注册为弱引用 //实现ISingleton接口的类型集合注册为单例模式 List<Type> singletonTypeList = GetDerivedClass<ISingleton>(classTypes); //注册单例 RegisterType<ContainerControlledLifetimeManager>(singletonTypeList); //实现IWeakReference接口的类型集合注册为弱引用 List<Type> weakReferenceTypeList = GetDerivedClass<IWeakReference>(classTypes); //注册弱引用 RegisterType<ExternallyControlledLifetimeManager>(weakReferenceTypeList); } /// <summary> /// 注册依赖注入 /// </summary> public static void RegisterDependency() { Init(); DependencyResolver.SetResolver(new UnityDependencyResolver(Current)); } } }
具体说明下DependencyContext类
1.获取bin目录下的所有DLL文件,过滤掉不需要实现依赖注入的DLL
因为是自动扫描所有程序集,所以无法分辨哪些是我们自己的程序集,因此可以显示的排除不需要的程序集,减少不必要的加载,以提高程序性能,这里我们只保留程序集的名称,因为我们从当前应用程序域获取程序集,所以我们只需要获取要进行自动类型注册的程序集名称完成程序集过滤即可。
private static List<string> GetAssemblyFiles() { string assemblyDirectory = AppDomain.CurrentDomain.BaseDirectory; if (HttpContext.Current != null) { assemblyDirectory = Path.Combine(assemblyDirectory, "Bin"); } string[] notFiles = new string[] { "EntityFramework.dll", "ICSharpCode.SharpZipLib.dll", "Ionic.Zip.dll", "log4net.dll", "Microsoft.ApplicationBlocks.Data.dll", "Microsoft.Practices.ServiceLocation.dll", "Microsoft.Practices.Unity.Configuration.dll", "Microsoft.Practices.Unity.dll", "Newtonsoft.Json.dll" }; //获取DLL文件 List<string> assemblyFiles = Directory.GetFiles(assemblyDirectory, "*.dll").Select(path => Path.GetFileName(path)).ToList(); //EXE可执行文件 assemblyFiles.AddRange(Directory.GetFiles(assemblyDirectory, "*.exe").Select(path => Path.GetFileName(path))); assemblyFiles = assemblyFiles.Where(f => !notFiles.Contains(f)).ToList(); return assemblyFiles; }
2.获取当前应用程序域加载的程序集,根据上面找到的程序集名称筛选我们要进行自动映射的程序集。
这里解释下为什么要从应用程序域获取程序集,而不是根据DLL文件路径使用Assembly.Load方法加载。首先一般我们需要自动完成映射的程序集肯定是程序需要用到的,因此在应用程序域加载的程序集中肯定能找到;其次也是最重要的原因:使用Assembly.Load加载的程序集和应用程序域的程序集类型来自不同程序集,会导致相同的类型无法识别,我在注释中也进行了说明。
public static List<Assembly> LoadAssembly() { List<string> assemblyFiles = GetAssemblyFiles(); //加载程序集不能使用Assembly.LoadFile()方法,该方法会导致DLL文件占用无法释放,改为文件流加载方式 //return assemblyFiles.Select(assemblyFile => Assembly.Load(File.ReadAllBytes(assemblyFile))).ToList(); //放弃使用Assembly.Load方法加载程序集 //Assembly.Load方法返回的程序集和当前应用程序域运行的程序集是相互独立 //当使用Load方法加载程序集Assembly1并加载类型T1时,然后从应用程序中的程序集Assembly1加载一个类型T1, //尽管这两个类型看上去是完全相同的而且也确实是完全相同的,但这两个T1类型却不相同,因为这两个都叫T1的类型分属两个不同的Assembly //只有一种情况下才会相同即你使用的这个类型有一个接口,并且这个接口定义在其它Assembly中 //因此为了程序的兼容性这里采取从当前应用程序域获取程序集 return AppDomain.CurrentDomain.GetAssemblies().Where(assembly => assemblyFiles.Contains(assembly.ManifestModule.ScopeName)).ToList(); }
3.从程序集获取所有类(不包含接口、抽象类),并为类的接口和抽象类注册映射关系
从程序集中获取所有的类,然后根据类实现的接口和集成的抽象类,完成接口、抽象类到实现类的类型映射,GetBaseTypes方法是根据类型获取其所有的接口和抽象类,Current.RegisterType(baseType, classType, new T()),完成接口和实现类的映射,本例中默认将对象生命生命周期设置为弱引用,关于生命周期可以根据自己的需要设置。Current.RegisterType(baseType, classType, classType.FullName, new T())此方法又为每个接口注册了一个带Name的映射关系,之所以为每个映射关系都注册两个映射主要是为了兼容有些接口、抽象类有多个实现类,若只是用没有Name的方法注册,最后只能保留一个映射,而使用实现类的类名作为Name可以在需要时使用根据Name完成接口的解析。保留无Name的映射是为了使用方便,毕竟绝大多数接口都只有一个实现类,我们就不用在使用时额外加上Name了。这种机制带来方便的同时也会有些性能的损失,看个人喜好和实际需要调整。
private static void RegisterType<T>(List<Type> types) where T : LifetimeManager, new() { types.AsParallel().ForAll(classType => { List<Type> baseTypes = GetBaseTypes(classType); foreach (Type baseType in baseTypes) { Current.RegisterType(baseType, classType, new T()); Current.RegisterType(baseType, classType, classType.FullName, new T()); } }); }
4.依赖注入的初始化调用
这里额外对ISingleton,IWeakReference接口进行了注册,主要是为了保持灵活,默认注册为弱引用,需要注册为单例的可以继承ISingletone接口,需要显示若引用的继承IWeakReference,这也就不会受默认生命周期改变影响了。
/// <summary> /// 初始化依赖注入 /// 注册所有实现了ISingleton和IWeakReference接口的类型到IUnityContainer容器 /// </summary> private static void Init() { //默认的情况下使用TransientLifetimeManager管理对象的生命周期,它不会在container中保存对象的引用,简而言之每当调用Resolve或ResolveAll方法时都会实例化一个新的对象 //ContainerControlledLifetimeManager 单例模式,每次调用Resolve或ResolveAll方法都会调用同一个对象的引用 //ExternallyControlledLifetimeManager 弱引用 List<Assembly> assemblys = LoadAssembly(); List<Type> classTypes = GetClassTypes(assemblys); //所有接口默认注册为弱引用 RegisterType<ExternallyControlledLifetimeManager>(classTypes); //先注册单例,再注册若引用,确保若同时实现了ISingleton和IWeakReference接口,则注册为弱引用 //实现ISingleton接口的类型集合注册为单例模式 List<Type> singletonTypeList = GetDerivedClass<ISingleton>(classTypes); //注册单例 RegisterType<ContainerControlledLifetimeManager>(singletonTypeList); //实现IWeakReference接口的类型集合注册为弱引用 List<Type> weakReferenceTypeList = GetDerivedClass<IWeakReference>(classTypes); //注册弱引用 RegisterType<ExternallyControlledLifetimeManager>(weakReferenceTypeList); }
到这里核心的的接口映射注册就完成了,是不是很简单,呵呵,下面来完成和MVC的关联
Unity和MVC3的关联
在LazyRabbit.Infrastructure.Dependency建立UnityDependencyResolver类实现System.Web.Mvc.IDependencyResolver接口的GetService和GetServices方法,具体代码如下,很简单不再解释
using System; using System.Collections.Generic; using System.Web.Mvc; using Microsoft.Practices.Unity; namespace LazyRabbit.Infrastructure.Dependency { public class UnityDependencyResolver : IDependencyResolver { public IUnityContainer _UnityContainer; /// <summary> /// /// </summary> /// <param name="unityContainer">依赖注入容器</param> public UnityDependencyResolver(IUnityContainer unityContainer) { _UnityContainer = unityContainer; } #region IDependencyResolver 成员 public object GetService(Type serviceType) { try { return _UnityContainer.Resolve(serviceType); } catch { /// 按微软的要求,此方法,在没有解析到任何对象的情况下,必须返回 null,必须这么做!!!! return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return _UnityContainer.ResolveAll(serviceType); } catch { /// 按微软的要求,此方法,在没有解析到任何对象的情况下,必须返回空集合,必须这么做!!!! return new List<object>(); } } #endregion } }
现在我所需要的编码已经全部完成了,只差最后一步,在Application_Start中添加代码LazyRabbit.Infrastructure.Dependency.DependencyContext.RegisterDependency()完成依赖注入的初始化以及和MVC的关联
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); LazyRabbit.Infrastructure.Dependency.DependencyContext.RegisterDependency(); }
RegisterDependency()方法代码
public static void RegisterDependency() { Init(); DependencyResolver.SetResolver(new UnityDependencyResolver(Current)); }
具体应用依赖注入
至此全自动注册依赖关系已经搞定,我们在WebApp建立IApplication接口和实现类Application,在LazyRabbit.Domain中建立IUserService接口和实现类UserService (LazyRabbit.Domain纯粹是为了掩饰着玩建立的,呵呵),完成后我们在HomeController进行调用
public class HomeController : Controller { /// <summary> /// 这个不指定Name /// </summary> [Dependency] public IApplication Application { get; set; } /// <summary> /// 这个用Name指定实现类 /// </summary> [Dependency("LazyRabbit.Domain.UserService")] public IUserService UserService { get; set; } // // GET: /Home/ public ActionResult Index() { ViewBag.SiteInfo = Application.GetSiteInfo(); ViewBag.CurrentUser = UserService.GetCurrentUser(); return View(); } }
微软Unity下载: http://www.microsoft.com/en-us/download/details.aspx?id=17866
Unity官网 http://unity.codeplex.com/
源码下载:UnityDemo.rar
本例特点在于实现思路简单,代码通俗易懂,复用性强,可以把LazyRabbit.Infrastructure.Dependency类库直接拿到项目使用,只需在Application_Start中调用下依赖注入初始化方法即可。这种设计方案可以大幅降低我们使用依赖注入的开发的工作量,设计上有足够的灵活性,不需要对现有代码做任何修改,即可轻松实现依赖注入,完全不用操心接口的注册问题,只要定义了接口和实现类就能自动完成注册。