• ASP.NET MVC模块化开发——动态挂载外部项目


    最近在开发一个MVC框架,开发过程中考虑到以后开发依托于框架的项目,为了框架的维护更新升级,代码肯定要和具体的业务工程分割开来,所以需要解决业务工程挂载在框架工程的问题,MVC与传统的ASP.NET不同,WebForm项目只需要挂在虚拟目录拷贝dll就可以访问,但是MVC不可能去引用工程项目的dll重新编译,从而产生了开发一个动态挂在MVC项目功能的想法,MVC项目挂载主要有几个问题,接下来进行详细的分析与完成解决方案

    一般动态加载dll的方法是使用Assembly.LoadFIle的方法来调用,但是会存在如下问题:

    1.如果MVC项目中存在依赖注入,框架层面无法将外部dll的类放入IOC容器

    通过 BuildManager.AddReferencedAssembly方法在MVC项目启动前,动态将外部代码添加到项目的编译体系中,需要配合PreApplicationStartMethod注解使用,示例:

    声明一个类,然后进行注解标记,指定MVC启动前方法

    //使用PreApplicationStartMethod注解的作用是在mvc应用启动之前执行操作
    [assembly: PreApplicationStartMethod(typeof(FastExecutor.Base.Util.PluginUtil), "PreInitialize")]
    namespace FastExecutor.Base.Util
    {
        public class PluginUtil
        {
            public static void PreInitialize()
            {
               
            }
        }
    }

    2.外部加载的dll中的Controller无法被识别

    通过自定义的ControllerFactory重写GetControllerType方法进行识别

     public class FastControllerFactory : DefaultControllerFactory
        {
    
            protected override Type GetControllerType(RequestContext requestContext, string controllerName)
            {
                Type ControllerType = PluginUtil.GetControllerType(controllerName + "Controller");
                if (ControllerType == null)
                {
                    ControllerType = base.GetControllerType(requestContext, controllerName);
                }
                return ControllerType;
            }
        }

    在Global.asax文件中进行ControllerFactory的替换

    ControllerBuilder.Current.SetControllerFactory(new FastControllerFactory());

    ControllerTypeDic是遍历外部dll获取到的所有Controller,这里需要考虑到Controller通过RoutePrefix注解自定义Controller前缀的情况

                    IEnumerable<Assembly> assemblies = GetProjectAssemblies();
                    foreach (var assembly in assemblies)
                    {
                        foreach (var type in assembly.GetTypes())
                        {
                            if (type.GetInterface(typeof(IController).Name) != null && type.Name.Contains("Controller") && type.IsClass && !type.IsAbstract)
                            {
                                string Name = type.Name;
                                //如果有自定义的路由注解
                                if (type.IsDefined(typeof(System.Web.Mvc.RoutePrefixAttribute), false))
                                {
                                    var rounteattribute = type.GetCustomAttributes(typeof(System.Web.Mvc.RoutePrefixAttribute), false).FirstOrDefault();
                                    Name = ((System.Web.Mvc.RoutePrefixAttribute)rounteattribute).Prefix + "Controller";
                                }
                                if (!ControllerTypeDic.ContainsKey(Name))
                                {
                                    ControllerTypeDic.Add(Name, type);
                                }
                            }
                        }
                        BuildManager.AddReferencedAssembly(assembly);
                    }

    3.加载dll后如果要更新业务代码,dll会被锁定,无法替换,需要重启应用

    解决办法是通过AppDomain对业务项目dll独立加载,更新时进行卸载

    1)创建一个RemoteLoader一个可穿越边界的类,作为加载dll的一个包装

     public class RemoteLoader : MarshalByRefObject
        {
            private Assembly assembly;
    
            public Assembly LoadAssembly(string fullName)
            {
                assembly = Assembly.LoadFile(fullName);
                return assembly;
            }
    
            public string FullName
            {
                get { return assembly.FullName; }
            }
    
        }

    2)创建LocalLoader作为AppDomian创建与卸载的载体

    public class LocalLoader
        {
            private AppDomain appDomain;
            private RemoteLoader remoteLoader;
            private DirectoryInfo MainFolder;
            public LocalLoader()
            {
    
                AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
              
                setup.ShadowCopyDirectories = setup.ApplicationBase;
                appDomain = AppDomain.CreateDomain("PluginDomain", null, setup);
    
                string name = Assembly.GetExecutingAssembly().GetName().FullName;
                remoteLoader = (RemoteLoader)appDomain.CreateInstanceAndUnwrap(
                    name,
                    typeof(RemoteLoader).FullName);
            }
    
            public Assembly LoadAssembly(string fullName)
            {
                return remoteLoader.LoadAssembly(fullName);
            }
    
            public void Unload()
            {
                AppDomain.Unload(appDomain);
                appDomain = null;
            }
    
            public string FullName
            {
                get
                {
                    return remoteLoader.FullName;
                }
            }
        }

    这里需要说明的,AppDomainSetup配置文件请使用AppDomain.CurrentDomain.SetupInformation也就是使用框架的作用于配置信息,因为业务代码会引用到很多框架的dll,如果独立创建配置信息,会有找不到相关dll的错误,同时这里也需要配置web.confg文件指定额外的dll搜索目录,因为业务工程代码也会有很多层多个dll相互引用,不指定目录也会存在找不到依赖dll的错误

    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
          <!--插件加载目录-->
          <probing privatePath="PluginTemp" />
        </assemblyBinding>
      </runtime>

    3)创建业务代码文件夹Plugin与临时dll文件夹PluginTemp

    为什么要创建临时文件夹呢,因为我们需要在PluginTemp真正的加载dll,然后监听Plugin文件夹的文件变化,有变化时进行AppDomain卸载这个操作,将Plugin中的dll拷贝到PluginTemp文件夹中,再重新加载dll

    监听Plugin文件夹:

    private static readonly FileSystemWatcher _FileSystemWatcher = new FileSystemWatcher();
      public static void StartWatch()
            {
                _FileSystemWatcher.Path = HostingEnvironment.MapPath("~/Plugin");
                _FileSystemWatcher.Filter = "*.dll";
                _FileSystemWatcher.Changed += _fileSystemWatcher_Changed;
    
                _FileSystemWatcher.IncludeSubdirectories = true;
                _FileSystemWatcher.NotifyFilter =
                    NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.DirectoryName | NotifyFilters.FileName | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size;
                _FileSystemWatcher.EnableRaisingEvents = true;
            }
            private static void _fileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
            {
                DllList.Clear();
                Initialize(false);
                InjectUtil.InjectProject();
            }

    拷贝dll:

     if (PluginLoader == null)
                {
                    PluginLoader = new LocalLoader();
                }
                else
                {
                    PluginLoader.Unload();
                    PluginLoader = new LocalLoader();
                }
    
                TempPluginFolder.Attributes = FileAttributes.Normal & FileAttributes.Directory;
                PluginFolder.Attributes = FileAttributes.Normal & FileAttributes.Directory;
                //清理临时文件。
                foreach (var file in TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
                {
                    try
                    {
                        File.SetAttributes(file.FullName, FileAttributes.Normal);
                        file.Delete();
                    }
                    catch (Exception)
                    {
                        //这里虽然能成功删除,但是会报没有权限的异常,就不catch了
                    }
    
                }
                //复制插件进临时文件夹。
                foreach (var plugin in PluginFolder.GetFiles("*.dll", SearchOption.AllDirectories))
                {
                    try
                    {
                        string CopyFilePath = Path.Combine(TempPluginFolder.FullName, plugin.Name);
                        File.Copy(plugin.FullName, CopyFilePath, true);
                        File.SetAttributes(CopyFilePath, FileAttributes.Normal);
                    }
                    catch (Exception)
                    {
                        //这里虽然能成功删除,但是会报没有权限的异常,就不catch了
                    }
                }

    注:这里有个问题一直没解决,就是删除文件拷贝文件的时候,AppDomain已经卸载,但是始终提示无权限错误,但是操作又是成功的,暂时还未解决,如果大家有解决方法可以一起交流下

    加载dll:

      public static IEnumerable<Assembly> GetProjectAssemblies()
            {
                if (DllList.Count==0)
                {
                    IEnumerable<Assembly> assemblies = TempPluginFolder.GetFiles("*.dll", SearchOption.AllDirectories).Select(x => PluginLoader.LoadAssembly(x.FullName));
                    foreach (Assembly item in assemblies)
                    {
                        DllList.Add(item);
                    }
                }
                return DllList;
            }

    4.业务代码的cshtml页面如何加入到框架中被访问

    在MVC工程中,cshtml也是需要被编译的,我们可以通过RazorBuildProvider将外部编译的页面动态加载进去

     public static void InitializeView()
            {
                IEnumerable<Assembly> assemblies = GetProjectAssemblies();
                foreach (var assembly in assemblies)
                {
                    RazorBuildProvider.CodeGenerationStarted += (object sender, EventArgs e) =>
                   {
                       RazorBuildProvider provider = (RazorBuildProvider)sender;
                       provider.AssemblyBuilder.AddAssemblyReference(assembly);
                   };
                }
    
            }

    RazorBuildProvider方法啊只是在路由层面将cshtml加入到框架中,我们还需要将业务工程View中模块的页面挂载虚拟目录到框架中,如图所示

    5.框架启动后,更新业务dll带来的相关问题

    在启动的项目中我们更新dll,我们希望达到的效果是和更新框架bin目录文件的dll一样,程序会重启,这样就会再次调用被PreApplicationStartMethod注解标注的方法,不需要在代码中做额外处理判断是首次加载还是更新加载,同时也做不到动态的将外部dll加入到MVC编译dll体系中,也只能启动前加载,查了很多资料,重新加载项目可以通过代码控制IIS回收程序池达到效果,但是因为各种繁琐的权限配置问题而放弃,我最后的解决方法是比较歪门邪道的方法,更新web.config文件的修改日期,因为iis会监控配置文件,更新了会重启引用,大家如果有更好的简单的方法,可以评论回复我呦

    //这里通过修改webconfig文件的时间达到重启应用,加载项目dll的目的!
    File.SetLastWriteTime(HostingEnvironment.MapPath("~/Web.config"), System.DateTime.Now);

    博客园没找到资源上传地址,传到码云上了,放个地址:https://gitee.com/grassprogramming/FastExecutor/attach_files

  • 相关阅读:
    2016第19周三
    2016第19周二
    Android JNI 获取应用程序签名
    HDU 3830 Checkers
    hadoop记录topk
    Codeforces 4A-Watermelon(意甲冠军)
    经验38--新闻内容处理
    Java8高中并发
    ACM-简单的主题Ignatius and the Princess II——hdu1027
    二叉搜索树
  • 原文地址:https://www.cnblogs.com/yanpeng19940119/p/12104799.html
Copyright © 2020-2023  润新知