C# 通过 AppDomain 应用程序域实现程序集动态卸载或加载
AppDomain 表示应用程序域,它是一个应用程序在其中执行的独立环境。每个应用程序只有一个主应用程序域,但是一个应用程序可以创建多个子应用程序域。
因此可以通过 AppDomain 创建新的应用程序域,在新创建的子应用程序域中加载执行程序集并且在执行完毕后释放程序集资源,来实现系统在运行状态下,程序集的动态加载或卸载,从而达到系统运行中程序集热更新的目的。
所谓应用程序域,.Net引入的一个概念,指的是一种边界,它标识了代码的运行范围,在其中产生的任何行为,包括异常都不会影响到其他应用程序域,起到安全隔离的效果。也可以看成是一个轻量级的进程。
一个进程可以包含多个应用程序域,各个域之间相互独立。如下是一个.net进程的组成(图片来自网络)
以下为整个原理的实现代码
主应用程序入口:
using Kernel.ServiceAgent; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Remoting; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Kernel.App { class Program { static void Main(string[] args) { Console.WriteLine(""); using (ServiceManager<IObjcet> manager = new ServiceManager<IObjcet>()) { string result = manager.Proxy.Put("apprun one"); Console.WriteLine(result); Console.WriteLine(""); Console.WriteLine(" Thread AppDomain info "); Console.WriteLine(manager.CotrProxy.FriendlyName); Console.WriteLine(Thread.GetDomain().FriendlyName); Console.WriteLine(manager.CotrProxy.BaseDirectory); Console.WriteLine(manager.CotrProxy.ShadowCopyFiles); Console.WriteLine(""); } Console.ReadLine(); } } }
创建新的应用程序域并且在新的应用程序域中调用透明代理类:
using Kernel.Interface; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Security.Policy; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Kernel.ServiceAgent { public class ServiceManager<T> : IDisposable where T : class { private AppDomain ctorProxy = null; /// <summary> /// 应用程序运行域容器 /// </summary> public AppDomain CotrProxy { get { return ctorProxy; } } private T proxy = default(T); public T Proxy { get { if (proxy == null) { proxy = (T)InitProxy(AssemblyPlugs); } return proxy; } } private string assemblyPlugs; /// <summary> /// 外挂插件程序集目录路径 /// </summary> public string AssemblyPlugs { get { assemblyPlugs = ConfigHelper.GetVaule("PrivatePath"); if (assemblyPlugs.Equals("")){ assemblyPlugs = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); } if (!Directory.Exists(assemblyPlugs)) { Directory.CreateDirectory(assemblyPlugs); } return assemblyPlugs; } set { assemblyPlugs = value; } } public ServiceManager() { if (proxy == null) { proxy = (T)InitProxy(AssemblyPlugs); } } private T InitProxy(string assemblyPlugs) { try { //AppDomain.CurrentDomain.SetShadowCopyFiles(); //Get and display the friendly name of the default AppDomain. //string callingDomainName = Thread.GetDomain().FriendlyName; //Get and display the full name of the EXE assembly. //string exeAssembly = Assembly.GetEntryAssembly().FullName; //Console.WriteLine(exeAssembly); AppDomainSetup ads = new AppDomainSetup(); ads.ApplicationName = "Shadow"; //应用程序根目录 ads.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; //子目录(相对形式)在AppDomainSetup中加入外部程序集的所在目录,多个目录用分号间隔 ads.PrivateBinPath = assemblyPlugs; //设置缓存目录 ads.CachePath = ads.ApplicationBase;
//获取或设置指示影像复制是打开还是关闭 ads.ShadowCopyFiles = "true";
//获取或设置目录的名称,这些目录包含要影像复制的程序集 ads.ShadowCopyDirectories = ads.ApplicationBase; ads.DisallowBindingRedirects = false; ads.DisallowCodeDownload = true; ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; //Create evidence for the new application domain from evidence of Evidence adevidence = AppDomain.CurrentDomain.Evidence; // Create the second AppDomain. ctorProxy = AppDomain.CreateDomain("AD #2", adevidence, ads); //Type.GetType("Kernel.TypeLibrary.MarshalByRefType").Assembly.FullName string assemblyName = Assembly.GetExecutingAssembly().GetName().FullName; //string assemblyName = typeof(MarshalByRefType).Assembly.FullName // Create an instance of MarshalByRefObject in the second AppDomain. // A proxy to the object is returned. Console.WriteLine("CtorProxy:" + Thread.GetDomain().FriendlyName); //TransparentFactory factory = (IObjcet)ctorProxy.CreateInstance("Kernel.TypeLibrary",
"Kernel.TypeLibrary.TransparentFactory").Unwrap(); TransparentAgent factory = (TransparentAgent)ctorProxy.CreateInstanceAndUnwrap(assemblyName,
typeof(TransparentAgent).FullName); Type meetType = typeof(T); string typeName = AssemblyHelper.CategoryInfo(meetType); object[] args = new object[0]; string assemblyPath = ctorProxy.SetupInformation.PrivateBinPath; //IObjcet ilive = factory.Create(@"E:\Plug\Kernel.SimpleLibrary.dll", "Kernel.SimpleLibrary.PlugPut", args); T obj = factory.Create<T>(assemblyPath, typeName, args); return obj; } catch (System.Exception) { throw; } } /// <summary> /// 卸载应用程序域 /// </summary> public void Unload() { try { if (ctorProxy != null) { AppDomain.Unload(ctorProxy); ctorProxy = null; } } catch(Exception) { throw; } } public void Dispose() { this.Unload(); } } }
创建应用程序代理类:
using Kernel.Interface; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Kernel.ServiceAgent { public class TransparentAgent : MarshalByRefObject { private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance; public TransparentAgent() { } /// <summary> Factory method to create an instance of the type whose name is specified, /// using the named assembly file and the constructor that best matches the specified parameters. </summary> /// <param name="assemblyFile"> The name of a file that contains an assembly where the type named typeName is sought. </param> /// <param name="typeName"> The name of the preferred type. </param> /// <param name="constructArgs"> An array of arguments that match in number, order,
/// and type the parameters of the constructor to invoke, or null for default constructor. </param> /// <returns> The return value is the created object represented as IObjcet. </returns> public IObjcet Create(string assemblyFile, string typeName, object[] args) { return (IObjcet)Activator.CreateInstanceFrom(assemblyFile, typeName, false, bfi, null, args, null, null).Unwrap(); } public T Create<T>(string assemblyPath, string typeName, object[] args) { string assemblyFile = AssemblyHelper.LoadAssemblyFile(assemblyPath, typeName); return (T)Activator.CreateInstanceFrom(assemblyFile, typeName, false, bfi, null, args, null, null).Unwrap(); } } }
所有涉及到需要动态加载或释放的资源,都需要放在代理类中进行操作,只有在此代理类中进行托管的代码才是属于新建的应用程序域的操作。
using Kernel.Interface; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Kernel.ServiceAgent { public class AssemblyHelper { /// <summary> /// 获取泛型类中指定属性值 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static string CategoryInfo(Type meetType) { object[] attrList = meetType.GetCustomAttributes(typeof(CategoryInfoAttribute), false); if (attrList != null) { CategoryInfoAttribute categoryInfo = (CategoryInfoAttribute)attrList[0]; return categoryInfo.Category; } return ""; } public static string LoadAssemblyFile(string assemblyPlugs, string typeName) { string path = string.Empty; DirectoryInfo d = new DirectoryInfo(assemblyPlugs); foreach (FileInfo file in d.GetFiles("*.dll")) { Assembly assembly = Assembly.LoadFile(file.FullName); Type type = assembly.GetType(typeName, false); if (type != null) { path = file.FullName; } } return path; } } }
读取配置文件信息:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Configuration; namespace Kernel.ServiceAgent { public class ConfigHelper { public static string GetVaule(string configName) { string configVaule = ConfigurationManager.AppSettings[configName]; if (configVaule != null && configVaule != "") { return configVaule.ToString(); } return ""; } } }
配置文件相关配置信息:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> <appSettings> <add key="PrivatePath" value="E:\Plugins"/> </appSettings> </configuration>
创建接口信息:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Kernel.Interface { [CategoryInfo("Kernel.SimpleLibrary.PlugPut", "")] public interface IObjcet { void Put(); string Put(string plus); } }
创建接口自定义属性:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Kernel.Interface { /// <summary> /// 设置接口实现类自定义标注属性 /// </summary> /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] public class CategoryInfoAttribute : Attribute { public string Category { get; set; } public string Describtion { get; set; } /// <summary> /// 设置实现类自定义标注属性 /// </summary> /// <param name="category"></param> /// <param name="describtion"></param> public CategoryInfoAttribute(string category, string describtion) { this.Category = category; this.Describtion = describtion; } } }
创建继承至IObjcet接口带有具体操作的实现类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Kernel.Interface; namespace Kernel.SimpleLibrary { [Serializable] public class PlugPut : MarshalByRefObject, IObjcet { private string plugName = "my plugName value is default!"; public string PlugName { get { return plugName; } set { plugName = value; } } public PlugPut() { } public PlugPut(string plusName) { this.PlugName = plusName; } public void Put() { Console.WriteLine("Default plug value is:" + plugName); } public string Put(string plus) { Console.WriteLine("Put plus value is:" + plus); return ("-------------------- PlugPut result info is welcome -------------------------"); } } }
继承至IObjcet接口带有具体操作的实现类,就是属于需要动态替换更新的程序集,所以最好将其编译在一个单独的程序集中,插件目录在配置文件中可配置,示例中放置在E:\Plugins 目录下,示例中代码最后将生成 Kernel.SimpleLibrary.DLL ,最后将编译好的程序集放置在 E:\Plugins\Kernel.SimpleLibrary.DLL 路径下,程序运行后将加载此程序集,加载完毕运行完毕后并释放此程序集。
以下两句较为重要,最好设置一下,不设置的后果暂时没有尝试
//获取或设置指示影像复制是打开还是关闭
ads.ShadowCopyFiles = "true";
//获取或设置目录的名称,这些目录包含要影像复制的程序集
ads.ShadowCopyDirectories = ads.ApplicationBase;
当程序运行起来后,程序集加载之后会在设置的应用程序域缓存目录中复制一份程序集的副本,然后运行副本中的程序集,释放掉本身加载的程序集。以上示例中会在主程序目录下生成一个Shadow 目录,此目录下包含了程序集的副本文件。
小节:
如果在另一个AppDomain 中加载程序集,然后获取Type,最后在主AppDomain中使用CreateInstance中的Type重载创建对象,结果会是Type所属的程序集会被加入到当前AppDomain中,然后Type的实例会在当前AppDomain中创建。
只有继承至 MarshalByRefObject 的透明代理类才能够进行跨域操作。
所以需要在继承至 MarshalByRefObject 的透明代理类中进行相关操作然后返回给主应用程序域,只有在代理类中进行的代码操作才是属于新建的应用程序域。否则任何运行代理类以外的代码都是属于主应用程序域。
此章节只是讲解了程序集动态加载或卸载热插拔的实现方式,有关AppDomain 和 AppDomainSetup 具体信息可以参考MSDN上面的文档。