Ninject是一个快如闪电、超轻量级的基于.Net平台的依赖注入框架。它能够帮助你把应用程序分离成一个个松耦合、高内聚的模块,然后用一种灵活的方式组装起来。通过使用Ninject配套你的软件架构,那么代码将会变得更加容易编写、重用性强、易于测试和修改。
MVC4 配合 Ninject 3 更是如虎添翼。
1.问题场景
在 MVC 的开发中,我们通常会使用到后台的数据,比如说需要获取一个后台的信息。通常会定义一个访问信息的接口,然后,有一个类实现了这个接口。
public interface IMessageProvider { string GetMessage(); } public class NinjectMessageProvider : IMessageProvider { public string GetMessage() { return "This message was provided by Ninject"; } }
在控制器中,经常会出现这样的代码。
public class HomeController : Controller { private IMessageProvider MessageProvider { set; get; } public HomeController() { this.MessageProvider = new NinjectMessageProvider(); } public ActionResult Index() { string message = this.MessageProvider.GetMessage(); return View( (object) message); } ...... }
这里使用了构造函数来创建提供器对象实例。问题是,在网站中,我们会出现大量的 Controller,那么,在每个 Controller 中我们都需要写这样的代码,进一步讲,如果我们以后需要创建的提供器对象类型不再是NinjectMessageProvider 类型了,就会导致大量的修改。
使用 NInject 可以让这些问题迎刃而解。
2. 获取 NInject
第一种方式是常用的引用程序集方式,首先到官方网站下载,注意有多种版本。
解压之后,你会得到一个名为 Ninject.dll 的程序集,将它引用到你的项目中。
还有一种方式,是使用 Visual Studio 支持的 NuGet,使用这个工具可以直接帮你从网上下载相应的程序集并引用到项目中。
3. 简单使用
首先,打开控制器的代码文件,在前面使用 using 引用 Ninject 命名空间,这里使用了扩展方法。
using Ninject;
然后,将构造函数修改为如下所示:
public HomeController() { Ninject.IKernel ninjectKernel = new Ninject.StandardKernel(); ninjectKernel.Bind<IMessageProvider>() .To<NinjectMessageProvider>(); this.MessageProvider = ninjectKernel.Get<IMessageProvider>(); }
这里的 Ninject.IKernel 是 Ninject 的核心。
4. 使用依赖注入
在每个控制器中,都这样创建对象,还不如原来方便,好在 MVC 提供了依赖注入的入口,我们将现在的构造函数修改一下,支持构造函数注入。
public HomeController(IMessageProvider provider) { this.MessageProvider = provider; }
然后,创建一个 NinjectDependencyResolver,实现 MVC 中提供的注入接口 IDependencyResolver,如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using Ninject; using MvcApplication1.Controllers; namespace MvcApplication1 { public class NinjectDependencyResolver :System.Web.Mvc.IDependencyResolver { private Ninject.IKernel kernel; public NinjectDependencyResolver() { this.kernel = new Ninject.StandardKernel(); this.AddBindings(); } private void AddBindings() { this.kernel.Bind<IMessageProvider>() .To<NinjectMessageProvider>(); } public object GetService(Type serviceType) { return this.kernel.TryGet(serviceType); } public IEnumerable<object> GetServices(Type serviceType) { return this.kernel.GetAll(serviceType); } } }
不要忘记第三步,注册这个容器。在 Global.asax 中,添加如下代码。
System.Web.Mvc.DependencyResolver.SetResolver( new MvcApplication1.NinjectDependencyResolver() ); AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes);
重新运行程序,你会发现控制器已经能够直接获取消息提供器对象了。
在 MVC 获取控制器对象的时候,会发现需要为构造函数传递一个实现接口 IMessageProvider 的对象实例,Ninject 发现已经注册的类型为 NinjectMessageProvider,那么,Ninject 就会自动帮助我们创建一个对象实例,再将这个对象实例传递到控制器中。
5. 使用属性注入
如果控制器中没有使用构造函数注入,而是使用了属性注入呢?完全没有问题,你只需要标注一个特性。注意:这个属性的作用域必须是 public 的。
[Ninject.Inject] public IMessageProvider MessageProvider { set; get; }
什么?你的这个成员是 private 的?没有问题,设置一下,让 Ninject 也可以注入非公共的成员就可以了。
public NinjectDependencyResolver() { this.kernel = new Ninject.StandardKernel(); this.kernel.Settings.InjectNonPublic = true; this.AddBindings(); }
这样,下面的成员也可以注入。
[Ninject.Inject] private IMessageProvider MessageProvider { set; get; }
===========================================================================================================
Ninject是一款.Net平台下的开源依赖注入框架.按照官方说法,它快如闪电、超级轻量,且充分利用了.Net的最新语法,使用Lambda表达式代替Xml文件完成类型绑定.Ninject结构精巧,功能强大,越来越受到各类开发者的关注,其开源社区非常活跃,众多开发者为它开发了各种各样的扩展应用.其中有一款名叫Ninject.Web.Common,是所有将Ninject应用于Web项目的基框架,而Ninject.MVC3则是将Ninject应用于Asp.Net Mvc中的框架.这两者是本文分析的主角.
书写本文时,Ninject的版本号为3.0.1,Ninject.Web.Common的版本号为3.0.0.3.,Ninject.MVC3的版本号为3.0.0.6.
OnePerRequestHttpModule是与对象生命周期有关的类,表示在同一次请求内同一类型会解析出相同的实例.这个是Web程序特有的生命周期,能在部分应用中节约资源,提高性能.此类实现了IHttpModule接口,如下:
public void Init(HttpApplication application) { application.EndRequest += (o, e) => this.DeactivateInstancesForCurrentHttpRequest(); } public void DeactivateInstancesForCurrentHttpRequest() { if (this.ReleaseScopeAtRequestEnd) { var context = HttpContext.Current; this.MapKernels(kernel => kernel.Components.Get<ICache>().Clear(context)); } }
可以看到,在WebApplication生命周期的最后,其会将所有在请求过程中生成的且缓存于HttpContext.Current的对象手动清空.还可以看到,其实这个类只涉及到OnePerRequest生命周期的部分实现,即清空部分.对象的分配部分则内置于Ninject框架中.
IBootstrapper接口的实现类为Bootstrapper,如下所示:
private static IKernel kernelInstance; public void Initialize(Func<IKernel> createKernelCallback) { kernelInstance = createKernelCallback(); kernelInstance.Components.GetAll<INinjectHttpApplicationPlugin>().Map(c => c.Start()); kernelInstance.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); kernelInstance.Inject(this); } public void ShutDown() { if (kernelInstance != null) { kernelInstance.Components.GetAll<INinjectHttpApplicationPlugin>().Map(c => c.Stop()); kernelInstance.Dispose(); kernelInstance = null; } } public void InitializeHttpApplication(HttpApplication httpApplication) { kernelInstance.Inject(httpApplication); }
Bootstrapper的中文意思是启动加载器,即管理程序启动时所需资源.IOC核心最终是保存在此对象的静态字段kernelInstance中.Initialize方法接受一个Func<IKernel>类型的委托,即通过它获取IOC核心.然后从中获取所有实现了INinjectHttpApplicationPlugin接口的类并立刻调用此接口Start方法.可以从名字猜出,这是一个简易的插件系统,在系统启动前首先启动所有插件.第二句先放一下.第三句则是将自己重新注入.只有由从IOC中获取的对象才能被注入,而Inject方法专门用于不是由IOC生成的对象,可以让其标记了[Inject]的且为空的属性重新获得对象注入.可以看到在默认情况下这一句不会产生实际效果.
InitializeHttpApplication则是将HttpApplication对象重注入.HttpApplication对象是由Asp.Net自动生成的.如果在实际使用的子类中进行了属性注入,则需要通过重注入的方式为属性获取合适的值.
ShutDown方法在系统关闭时被调用,用于插件的逐个卸载.
分析完这两个类我们再来分析Ninject.Web.Common的核心类:NinjectHttpApplication
private readonly OnePerRequestHttpModule onePerRequestHttpModule; private readonly IBootstrapper bootstrapper; protected NinjectHttpApplication() { this.onePerRequestHttpModule = new OnePerRequestHttpModule(); this.onePerRequestHttpModule.Init(this); this.bootstrapper = new Bootstrapper(); } public void Application_Start() { lock (this) { this.bootstrapper.Initialize(this.CreateKernel); this.onePerRequestHttpModule.ReleaseScopeAtRequestEnd = this.bootstrapper.Kernel.Settings.Get("ReleaseScopeAtRequestEnd", true); this.OnApplicationStarted(); } } public void Application_End() { this.bootstrapper.ShutDown(); this.OnApplicationStopped(); } protected abstract IKernel CreateKernel(); protected virtual void OnApplicationStarted() { } protected virtual void OnApplicationStopped() { } public override void Init() { base.Init(); this.bootstrapper.InitializeHttpApplication(this); }在此对象构造函数中,构造了上面所述的两个对象,并手动调用了OnePerRequestHttpModule对象的Init方法完成了此IHttpModule的挂载.然后在Application_Start()方法中调用了IBootstrapper对象的Initialize方法完成了插件的挂载.并设置了OnePerRequestHttpModule对象是否开启资源清理.最后调用OnApplicationStarted方法.Application_End()则完成插件的卸载和OnApplicationStopped()方法.
可以看到,此对象改变了传统HttpApplication对象的一般处理过程.按照其设计意图,应用程序开启时执行的方法由默认的Application_Start方法转移动OnApplicationStarted(),而应用程序终止时执行的方法则由Application_End方法转移到OnApplicationStopped方法中.
此对象实际是一个抽象对象,包含唯一一个抽象方法CreateKernel,方法最终将被用户重写,用于提供IOC核心.
在Init方法中调用Bootstrapper对象的InitializeHttpApplication方法完成对自身的重注入.Init方法与Application_Start方法不同.Init方法将在每个HttpApplication对象构造完成并加载了所有IHttpModule之后被调用,而Application_Start方法则是在应用程序启动时被调用,在整个应用程序中Init方法会被调用多次,而Application_Start方法只会被调用一次.如果HttpApplication对象池有剩余对象,则会取出一个来处理请求,这时不会触发Init方法,否则则会建立新的HttpApplication对象来处理新的请求,这时则会调用Init方法.
至此,Ninject.Web.Common的主体工程已分析完毕.但其仍提供了两个额外的HttoModule.这也是最容易让人引起困惑的地方.
NinjectHttpModule类提供了一种功能,即通过类型绑定而不是Web.Config来配置IHttpModule,如下:
private IList<IHttpModule> httpModules; public void Init(HttpApplication context) { this.httpModules = new Bootstrapper().Kernel.GetAll<IHttpModule>().ToList(); foreach (var httpModule in this.httpModules) { httpModule.Init(context); } } public void Dispose() { foreach (var httpModule in this.httpModules) { httpModule.Dispose(); } this.httpModules.Clear(); }
代码比较简单,即在Init处获取所有已绑定的实现了IHttpModule接口的对象并循环调用它们的Init方法,在Dispose方法处循环调用它们各自的Dispose方法.
HttpApplicationInitializationHttpModule对象如下:
private readonly Func<IKernel> lazyKernel; public HttpApplicationInitializationHttpModule(Func<IKernel> lazyKernel) { this.lazyKernel = lazyKernel; } public void Init(HttpApplication context) { this.lazyKernel().Inject(context); }这个更简单,就是将HttpApplication对象重新注入,只不过实现为了IHttpModule接口.还记得Bootstrapper对象的Initialize方法吗?在那里将IHttpModule与HttpApplicationInitializationHttpModule进行了绑定.
在Ninject.Web.Common官网上展示了使用这个框架的两种方式,一种是继承,一种是动态注入.现在问题来了:
1.如何使用HttpApplicationInitializationHttpModule对象?可以看到,其没有无参构造函数,如果直接配置于Web.Config,Asp.Net则会因为无法构造此对象而报错.此对象需配合NinjectHttpModule对象使用.
2.如果使用继承方式使用,同时在Web.Config中配置了NinjectHttpModule,则HttpApplication对象将被注入两次.一次在Bootstrapper对象的InitializeHttpApplication方法中,一次在HttpApplicationInitializationHttpModule对象的Init方法中.一开始我百思不得其解,后来看到一篇官方的反馈我才恍然大悟.原来HttpApplicationInitializationHttpModule并不应该用于使用继承方式中,而是用于动态注入方式中的.
3.同上,如果使用继承方式使用,同时在Web.Config中配置了NinjectHttpModule,则程序会报错,因为是HttpApplicationInitializationHttpModule对象是由IOC核心构造的,其需要知道Func<IKernel>类型的实际类型.
最后,我们来看一看官方推荐的动态注入使用方式
[assembly: WebActivator.PreApplicationStartMethod(typeof(WebApplication1.App_Start.NinjectWebCommon), "Start")] [assembly: WebActivator.ApplicationShutdownMethodAttribute(typeof(WebApplication1.App_Start.NinjectWebCommon), "Stop")] public static class NinjectWebCommon { private static readonly Bootstrapper bootstrapper = new Bootstrapper(); /// <summary> /// Starts the application /// </summary> public static void Start() { DynamicModuleUtility.RegisterModule(typeof(OnePerRequestHttpModule)); DynamicModuleUtility.RegisterModule(typeof(NinjectHttpModule)); bootstrapper.Initialize(CreateKernel); } public static void Stop() { bootstrapper.ShutDown(); } private static IKernel CreateKernel() { var kernel = new StandardKernel(); kernel.Bind<Func<IKernel>>().ToMethod(ctx => () => new Bootstrapper().Kernel); kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); RegisterServices(kernel); return kernel; } private static void RegisterServices(IKernel kernel) { } }它充分使用Asp.Net 4.0的新技术,并使用了WebActivator框架,在Web程序启动前动态调用Start函数,在关闭前调用Stop函数.在Start函数中,动态注册了OnePerRequestHttpModule和NinjectHttpModule.而对于CreateKernel函数,首先新建了IOC核心,然后绑定了Func<IKernel>类型与IHttpModule类型.可以看到,由于在Bootstrapper对象的Initialize方法里已绑定过一次,这里重复绑定了.然后调用RegisterServices方法获取应用程序的各类具体类型绑定,最后将IOC核心返回以保存至Bootstrapper的静态变量kernelInstance中.
Ninject.MVC3的代码则比较简单
MvcModule类继承自GlobalKernelRegistrationModule<OnePerRequestHttpModule>类.GlobalKernelRegistrationModule<T>类继承自NinjectModule类.完成了各种类型绑定.GlobalKernelRegistrationModule<T>类与GlobalKernelRegistration类配合使用的,实现了一种反向绑定.一般的正向绑定,即某一个IOC核心绑定了多个接口到类的映射,而反向绑定则是记录某个接口在哪些IOC核心里进行了绑定.这个功能我暂时没有看到在哪里进行了使用,不过应该是有所目的的.
MvcModule类定义如下:
public override void Load() { base.Load(); this.Kernel.Components.Add<INinjectHttpApplicationPlugin, NinjectMvcHttpApplicationPlugin>(); this.Kernel.Bind<IDependencyResolver>().To<NinjectDependencyResolver>(); this.Kernel.Bind<IFilterProvider>().To<NinjectFilterAttributeFilterProvider>(); this.Kernel.Bind<IFilterProvider>().To<NinjectFilterProvider>(); this.Kernel.Bind<RouteCollection>().ToConstant(RouteTable.Routes); this.Kernel.Bind<HttpContext>().ToMethod(ctx => HttpContext.Current).InTransientScope(); this.Kernel.Bind<HttpContextBase>().ToMethod(ctx => new HttpContextWrapper(HttpContext.Current)).InTransientScope(); this.Kernel.Bind<ModelValidatorProvider>().To<NinjectDataAnnotationsModelValidatorProvider>(); //this.Kernel.Bind<IModelBinderProvider>().To<NinjectModelBinderProvider>(); //this.Kernel.Bind<IModelBinder>().To<NinjectModelBinder>(); }那这个Load函数是何时执行的呢?这就要谈到Ninject的加载策略了.核心对象StandardKernel与其基类KernelBase的构造函数如下
public class StandardKernel : KernelBase { public StandardKernel(params INinjectModule[] modules) : base(modules) { } } public abstract class KernelBase : BindingRoot, IKernel { protected KernelBase() : this(new ComponentContainer(), new NinjectSettings(), new INinjectModule[0]) {} protected KernelBase(IComponentContainer components, INinjectSettings settings, params INinjectModule[] modules) { if (this.Settings.LoadExtensions) { this.Load(this.Settings.ExtensionSearchPatterns); } } }可以看到,在KernelBase的构造函数中,有一个LoadExtensions的判断,默认在NinjectSettings类中,意思是加载本地扩展
public class NinjectSettings : INinjectSettings { public bool LoadExtensions { get { return Get("LoadExtensions", true); } set { Set("LoadExtensions", value); } } public string[] ExtensionSearchPatterns { get { return Get("ExtensionSearchPatterns", new [] { "Ninject.Extensions.*.dll", "Ninject.Web*.dll" }); } set { Set("ExtensionSearchPatterns", value); } } }
可以猜测,默认会去Bin目录下搜索所有以Ninject.Extensions或Ninject.Web开头的程序集.
具体的加载过程则比较简单
public void Load(IEnumerable<string> filePatterns) { var moduleLoader = this.Components.Get<IModuleLoader>(); moduleLoader.LoadModules(filePatterns); }加载管理器接口IModuleLoader的实现类为ModuleLoader
public void LoadModules(IEnumerable<string> patterns) { var plugins = Kernel.Components.GetAll<IModuleLoaderPlugin>(); var fileGroups = patterns .SelectMany(pattern => GetFilesMatchingPattern(pattern)) .GroupBy(filename => Path.GetExtension(filename).ToLowerInvariant()); foreach (var fileGroup in fileGroups) { string extension = fileGroup.Key; IModuleLoaderPlugin plugin = plugins.Where(p => p.SupportedExtensions.Contains(extension)).FirstOrDefault(); if (plugin != null) plugin.LoadModules(fileGroup); } }第一句获取所有的加载分析器.程序自带了加载以dll为扩展名的文件型加载分析器,实现类为CompiledModuleLoaderPlugin.第二句获取传入的程序集全路径.第三句则是循环文件,为每一个文件获取一个加载分析器并进行操作.
public class CompiledModuleLoaderPlugin : NinjectComponent, IModuleLoaderPlugin { public void LoadModules(IEnumerable<string> filenames) { var assembliesWithModules = this.assemblyNameRetriever.GetAssemblyNames(filenames, asm => asm.HasNinjectModules()); this.Kernel.Load(assembliesWithModules.Select(asm => Assembly.Load(asm))); } }这里将实际功能委托给AssemblyNameRetriever类完成.
public class AssemblyNameRetriever : NinjectComponent, IAssemblyNameRetriever { public IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames, Predicate<Assembly> filter) { var assemblyCheckerType = typeof(AssemblyChecker); var temporaryDomain = CreateTemporaryAppDomain(); try { var checker = (AssemblyChecker)temporaryDomain.CreateInstanceAndUnwrap( assemblyCheckerType.Assembly.FullName, assemblyCheckerType.FullName ?? string.Empty); return checker.GetAssemblyNames(filenames.ToArray(), filter); } } }这里创建了一个临时应用程序域,然后在其中创建AssemblyChecker对象,最终通过它来完成最后的任务
private class AssemblyChecker : MarshalByRefObject { public IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames, Predicate<Assembly> filter) { var result = new List<AssemblyName>(); foreach (var filename in filenames) { Assembly assembly = Assembly.Load(filename); if (filter(assembly)) { result.Add(assembly.GetName(false)); } } return result; } }这里在临时应用程序域中创建程序集后,使用了一个判断来决定是否在此程序集中搜索,是由之前传入的,定义在ExtensionsForAssembly类中.
internal static class ExtensionsForAssembly { public static bool HasNinjectModules(this Assembly assembly) { return assembly.GetExportedTypes().Any(IsLoadableModule); } public static IEnumerable<INinjectModule> GetNinjectModules(this Assembly assembly) { return assembly.GetExportedTypes() .Where(IsLoadableModule) .Select(type => Activator.CreateInstance(type) as INinjectModule); } private static bool IsLoadableModule(Type type) { return typeof(INinjectModule).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface && type.GetConstructor(Type.EmptyTypes) != null; } }可以看到,这里就是说,在某一程序集中是否有实现了INinjectModule接口的类,且不是接口,不是抽象类,且还拥有无参构建函数.
获取到程序集名后,由KernelBase完全加载
public void Load(IEnumerable<Assembly> assemblies) { this.Load(assemblies.SelectMany(asm => asm.GetNinjectModules())); } public void Load(IEnumerable<INinjectModule> m) { Ensure.ArgumentNotNull(m, "modules"); m = m.ToList(); foreach (INinjectModule module in m) { module.OnLoad(this); this.modules.Add(module.Name, module); } }
NinjectDependencyResolver类则实现了接口IDependencyResolver,其本质是一个配置器模式,即让Asp.Net MVC从IOC核心的获取资源.定义如下:
private readonly IResolutionRoot resolutionRoot; public object GetService(Type serviceType) { var request = this.resolutionRoot.CreateRequest(serviceType, null, new Parameter[0], true, true); return this.resolutionRoot.Resolve(request).SingleOrDefault(); } public IEnumerable<object> GetServices(Type serviceType) { return this.resolutionRoot.GetAll(serviceType).ToList(); }NinjectMvcHttpApplicationPlugin类则是利用Ninject.Web.Common的插件机制,完成IOC与Asp.Net MVC的对接,其关键代码如下:
public void Start() { ModelValidatorProviders.Providers.Remove(ModelValidatorProviders.Providers.OfType<DataAnnotationsModelValidatorProvider>().Single()); DependencyResolver.SetResolver(this.CreateDependencyResolver()); RemoveDefaultAttributeFilterProvider(); } protected IDependencyResolver CreateDependencyResolver() { return this.kernel.Get<IDependencyResolver>(); }在CreateDependencyResolver方法中从IOC核心中获取IOC实例,在KernelBase对象的构造函数中完成了IKernel接口与IKernel实例的隐式绑定.如下
this.Bind<IKernel>().ToConstant(this).InTransientScope(); this.Bind<IResolutionRoot>().ToConstant(this).InTransientScope();回到NinjectMvcHttpApplicationPlugin类,最后在其Start方法的第二行将获取到的IOC实例作为参数传入DependencyResolver对象的SetResolver方法,完成与Asp.Net MVC的对接.