一直想写一个这样的程序:与其它的程序完全解耦,但可以动态的加载其它程序,并执行其中的特定方法,执行完后可以卸载,完全不影响该程序本身。最近无意间发现了 C# 中 AppDomain,再加上反射,感觉就是我所需要的。
应用程序域为安全性、可靠性、版本控制以及卸载程序集提供了隔离边界。 应用程序域通常由运行时宿主创建,运行时宿主负责在运行应用程序之前引导公共语言运行时。
应用程序域所提供的隔离具有以下优点:
(1)在一个应用程序中出现的错误不会影响其他应用程序。 因为类型安全的代码不会导致内存错误,所以使用应用程序域可以确保在一个域中运行的代码不会影响进程中的其他应用程序。
(2)能够在不停止整个进程的情况下停止单个应用程序。 使用应用程序域使您可以卸载在单个应用程序中运行的
注意:不能卸载单个程序集或类型。只能卸载整个域。
一切的根源,都是因为只有 Assembly.Load 方法,而没有 Assembly.Unload 方法,只能卸载其所在的 AppDomain。
操作为读取配置文件(为测试 AppDomain 中配置文件的读取情况),并使用 Newtonsoft.Json 将其序列化为 json(为测试 AppDomain 中加载程序中的第三方引用情况),在控制台输出。项目名为 ReadPrint, 将其编译为 exe 文件,并存放在 D:\AppDomainModules 中。
using Newtonsoft.Json; using System; using System.Configuration; namespace ReadPrint { class Program { static void Main(string[] args) { DoSomething(); } public static void DoSomething() { Person person = new Person { Account = ConfigurationManager.AppSettings["Account"], Name = ConfigurationManager.AppSettings["Name"], Age = int.Parse(ConfigurationManager.AppSettings["Age"]) }; Console.WriteLine(JsonConvert.SerializeObject(person)); Console.ReadLine(); } class Person { public string Account { get; set; } public string Name { get; set; } public int Age { get; set; } } } }
为了查看方便定义了 DoSomething 来执行相关方法。也可以直接写在 Main 方法中,调用时需要传入参数 args。因为最终测试 AppDomain 的程序也打算使用控制台应用,也使用控制台应用来写这个小程序。
主要包含 AssemblyLoader.cs 文件用于封装使用细节,和 Program.cs 主程序文件。
AssemblyLoader.cs
using System; using System.IO; using System.Reflection; namespace AppDomainTest { public class AssemblyDynamicLoader { private AppDomain appDomain; public readonly RemoteLoader remoteLoader; public AssemblyDynamicLoader() { AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationName = "ApplicationLoader"; setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); setup.CachePath = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; # 重点 setup.ShadowCopyDirectories = setup.ApplicationBase; setup.ConfigurationFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", "ReadPrint.exe.config"); //AppDomain.CurrentDomain.SetShadowCopyFiles(); this.appDomain = AppDomain.CreateDomain("ApplicationLoaderDomain", null, setup); String name = Assembly.GetExecutingAssembly().GetName().FullName; this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); # 重点 } public void Unload() { try { if (appDomain == null) return; AppDomain.Unload(this.appDomain); this.appDomain = null; } catch (CannotUnloadAppDomainException ex) { throw ex; } } } public class RemoteLoader : MarshalByRefObject { private Assembly _assembly; public void LoadAssembly(string assemblyFile) { try { _assembly = Assembly.LoadFrom(assemblyFile); } catch (Exception ex) { throw ex; } } public void ExecuteMothod(string typeName, string methodName) { if (_assembly == null) { return; } var type = _assembly.GetType(typeName); type.GetMethod(methodName).Invoke(Activator.CreateInstance(type), new object[] { }); } } }
其中类 RemoteLoader 为加载程序集的类,AssemblyDynamicLoader 类在此基础上封装了新建 AppDomain 的细节。
在 AssemblyDynamicLoader 的构造函数中,为了测试方便,硬编码了一些内容,如 程序集文件查找路径 PrivateBinPath 为当前程序执行目录下面的 Modules 目录,配置文件 ConfigurationFile 为 Modules 目录中的 ReadPrint.exe.config, 以及创建新 AppDomain 时的程序集名称。
AppDomainSetup 的属性 ShadowCopyFiles(似乎可以译为“卷影复制”) 代表是否锁定读取的程序集。如果设置为 true,则将程序集读取至内存,不锁定其文件,这也是热更新的前提;否则在程序执行期间这些程序集文件会被锁定,不能变化。
AppDomain 的方法 CreateInstanceAndUnwrap 意为在 AppDomain 的实例中创建指定类型的新实例,并返回。
在 RemoteLoader 的 ExecuteMethod 中,传入的参数硬编码为空。在实际使用时应当根据实际传入参数。
Program.cs
using System; using System.IO; namespace AppDomainTest { class Program { static void Main(string[] args) { string modulesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules"); DirectoryInfo di = new DirectoryInfo(modulesPath); if (!di.Exists) { di.Create(); } string remotePath = @"D:\AppDomainModules\"; string[] fileNames = new string[] { "ReadPrint.exe", "Newtonsoft.Json.dll", "ReadPrint.exe.config" }; foreach(var fileName in fileNames) { FileInfo fi = new FileInfo(Path.Combine(remotePath, fileName)); fi.CopyTo(Path.Combine(modulesPath, fileName), true); } AssemblyDynamicLoader adl = new AssemblyDynamicLoader(); adl.remoteLoader.LoadAssembly(Path.Combine(modulesPath, "ReadPrint.exe")); adl.remoteLoader.ExecuteMethod("ReadPrint.Program", "DoSomething"); adl.Unload(); } } }
在主程序文件中,创建 Modules 文件夹,拷贝程序文件、库文件和配置文件。程序运行结果:
可以看到成功调用了我们定义的 DoSomething 方法。
使用此方法,会首先在主程序的 AppDomain 中加载一遍程序集(和依赖),再移至我们创建的 AppDomain 中(特别注意,此时不会从我们新建的 AppDomain 的 PrivateBinPath 中搜索和加载)。
缺点有二,一是随着程序的运行,可能会加载大量的程序集,因此主程序的 AppDomain 也要加载大量程序集,而程序集无法单独卸载,只有在主程序停止后才会卸载,其间必然越积越多,极不优雅;二是无法自定目录,主程序加载程序集和依赖时只会在其指定的 PrivateBinPath 中搜索,因此其它模块所有需要的程序集文件都堆积在同一个目录中,条理不清。
验证
修改 AssemblyDynamicLoader.cs 中的代码,改为直接在构造函数里面执行程序加载,其它不变,并查看我们新建的 AppDomain 中已加载的程序集:
//String name = Assembly.GetExecutingAssembly().GetName().FullName; //this.remoteLoader = (RemoteLoader)this.appDomain.CreateInstanceAndUnwrap(name, typeof(RemoteLoader).FullName); Assembly assembly = this.appDomain.Load("ReadPrint"); Type t = assembly.GetType("ReadPrint.Program"); MethodInfo mi = t.GetMethod("DoSomething"); //mi.Invoke(Activator.CreateInstance(t), new object[] { }); var tmp = this.appDomain.GetAssemblies();
此处最为奇怪的是,尽管我们在上面指定了自己 AppDomain 的 PrivateBinPath 和 配置文件,执行时依然找的是主程序的 PrivateBinPath 和 配置文件,因此将执行的那一行代码注释。
修改 Program.cs 中的代码,改为仅调用 AssemblyDynamicLoader 的构造函数,其它不变,并查看主程序 AppDomain 中已加载的程序集:
AssemblyDynamicLoader adl = new AssemblyDynamicLoader(); //adl.remoteLoader.LoadAssembly(Path.Combine(modulesPath, "ReadPrint.exe")); //adl.remoteLoader.ExecuteMethod("ReadPrint.Program", "DoSomething"); //adl.Unload(); var tmp = AppDomain.CurrentDomain.GetAssemblies(); Console.ReadLine();
结果如图所示:
需要注意的是,RemoteLoader 类继承了 MarshalByRefObject,而继承此类的应用可以跨 AppDomain 使用。此处猜测虽然可以在主程序中创建新的 AppDomain,但新的 AppDomain 依然无法完全摆脱主程序。
我们不可能要求所有被调用的模块都继承此类,因此使用代理类 RemoteLoader。执行的过程为:创建新的 AppDomain;在其中新建代理类 RemoteLoader,代理类帮助我们加载不同的模块和依赖,并代替我们调用模块。CreateInstanceAndUnwrap 实际上就是在新建的 AppDomain 中创建并实例化代理类,此后所有的工作均在新的 AppDomain 中进行。
代码中使用了很多硬编码。实际中,应向主程序指出要调用的模块路径、依赖文件路径和配置文件路径,由主程序拷贝至临时目录,再使用 AssemblyDynamicLoader 创建新的 AppDomain 和执行。
感觉大部分时候查看文章都是为了解决一些问题,因此本文把使用方法放在了前面,把详细说明放在了后面,也算是一些优化了XD。
以上就是C# 关于AppDomain的一些总结的详细内容,更多关于C# AppDomain的资料请关注小牛知识库其它相关文章!
本文向大家介绍关于NoSQL之MongoDB的一些总结,包括了关于NoSQL之MongoDB的一些总结的使用技巧和注意事项,需要的朋友参考一下 NoSQL已经流行了很长一段时间,那么究竟是什么场景下你才更需要用到这些“新兴事物”,就比如MongoDB?下面是一些总结: 你期望一个更高的写负载 默认情况下,对比事务安全,MongoDB更关注高的插入速度。如果你需要加载大量低价值的业务数据,那么Mon
本文向大家介绍关于JavaScript数组去重的一些理解汇总,包括了关于JavaScript数组去重的一些理解汇总的使用技巧和注意事项,需要的朋友参考一下 前言 做前端开发几年,在项目中用到数组去重的机会倒不是很多,但是在面试的时候却经常被问到,个人理解,这道题真正考的是对JavaScript的基础的掌握,因为有很多种方式可以做到。这次就根据这道题,将相关的知识理解透彻。 一、ES6中的new S
您可以通过继承的方式来扩展基础 AppDomain 的行为,下面是 CatLib.ILRuntime.AppDomain 封装的一些方法。 获取 ILRuntime AppDomain 通过Domain属性您可以获取ILRuntime原始的AppDomain。 protected ILRuntimeDomain Domain { get; } 调用热更新主入口 在您的热更新代码都已经加载完成,
本文向大家介绍关于c#中单例模式的一些问题,包括了关于c#中单例模式的一些问题的使用技巧和注意事项,需要的朋友参考一下 本文主要介绍了关于单例模式的一些问题,想学习C#单例模式的同学们可以看一看,还是有些帮助 c#中的单例模式 单例模式是指在设计一个类时,保证在运行期间只有一个实例对象,因为过多相同的实例对象会占用内存空间。 ##举个例子 1.声明一个静态的Class1类的变量来引用唯一的对象。
这份文档由Bjarne Stroustrup进行编写并维护。任何建设性的意见,校正,引用和建议,都是欢迎的。目前,我正在努力让这份文档更加完善并进行一些参考的清理工作。 C++11是下一个国际标准组织ISO的C++标准。目前,已经有草案可供大家参考提出意见。提供意见。以前的(和目前的)标准通常被称为为C++98和C++03。C++98和C++03之间的差异很小并且太过技术化,不应当引起用户的关注。
本文向大家介绍关于C#中排序函数的总结,包括了关于C#中排序函数的总结的使用技巧和注意事项,需要的朋友参考一下 sort 函数对数组中的数据进行升序排序,(其中,sort函数有很多重载的形式,这里不再一一的说明) Reverse函数对数组中的数据进行降序排序, 如何把二个数组联系在一起进行排序操作呢? 例,在学生的信息中有学号和姓名,按学号输出学生的信息怎样实现??? 以上这篇关于C#中排序函数的