如何从程序集中加载及卸载插件

来源:互联网 发布:知乎 阿子 编辑:程序博客网 时间:2024/04/26 13:33
 

在软件开发的领域中,插件技术一直是一项非常实用的技术。许多优秀的软件产品都提供了通过加载插件来扩展、丰富产品本身功能的能力。而像Firefox、Eclipse之类的软件,更是将插件的功能发挥到了极致。顺便做点广告的是,我们的Mussel框架便是一套基于插件扩充应用功能的框架:),在这里我们来分析一下.Net中常用的插件技术。

 

我们先从最简单的加载方式来讲起,我们知道在Assembly类中,提供了一个Loadxxxx的方法,这些方法可以让我们从磁盘文件或是字节流中加载程序集到当前的AppDomain中,例如:

 

            Assembly.Load("Addin1.dll");
            Assembly.LoadFile("Addin1.dll");
            Assembly.LoadFrom("Addin1.dll");
            AppDomain.CurrentDomain.Load("Addin1.dll");

 

上面的方式都可以将磁盘上的文件“Addin1.dll” 加载到当前的AppDomain中,然后我们很自然的可以联想到通过AppDomain.CurrentDomain.CreateInstance 或是 Activator.CreateInstance 之类的方法便可以构建出插件具体的实例,一切似乎都非常顺利。但是如果我们的插件服务是一个可热插拔的,那么我们还需要考虑的是——我们如何更插件的程序集?Assembly是没有提供UnLoad操作的,这就意味着我们只可以将文件或是字节流加载成当前AppDomain中的Assembly,却无法卸载这个Assembly,这明显不符合“可热插拔的”的要求。因此对于一些更换频率较高的插件是不适合采用这种方式的——除非软件的使用者可以接受频繁的重启软件。

 

基于Assembly没有UnLoad操作的缺陷,我们必须考虑通过其它的方式来实现来修复这个问题。通过浏览.Net类库的一些声明我们可以看到,在AppDomain中是有UnLoad操作的。这似乎是一个较好的解决途径——我们可以创建一个新的AppDomain,将一些包含插件的程序集文件及相关的一些引用加入到这个新创建的AppDomain中,那么便可以实现插件的卸载了。当然,要想实现这一构想,我们必须对.Net的AppDomain有一个全面的认识。

 

在介绍AppDomain之前我们希望大家对Remoting有一定的了解。因为.Net同进程中的不同的AppDomain之间的通信,其实是基于Remoting的,采用的是Icp Channel。关于Remoting的具体内容就不在本文的讨论范围之类,我们只需要知道:如果我们通过AppDomain.CreateDomain(string)方法创建出的一个新的AppDomain,其实只是一个Remoting的Proxy对象。我们知道在Remoting操作中,如果我们需要调用服务端的方法,服务相关的对象必须是从MarshalByRefObject这个类型继承的;同时,如果需要在Remoting的客户端与服务端之间传递信息,这个信息本身的对象必须是可以序列化的。

 

基于上面的理由,我们的插件实现类必须符合两个规范:

  1. 必须继承自MarshalByRefObject。
  2. 插件中传递的参数类型及返回值必须是可序列化的。

 

根据上面的一些知识,我们就不难实现一个可卸载的插件加载模式:

  1. 建立一个新的AppDomain: AppDomain.CreateDomain()。
  2. 利用的AppDomain的实例,采用 CreateInstanceFromAndUnwrap() 方法在新的AppDomain中构建一个指定的类型,并返回相应的Proxy。
  3. 根据获取的Proxy就可调用插件了。
  4. 卸载时,完成一些资源清理后可以直接对新建出的AppDomain进行UnLoad

今天先讲理论,在下一章中,我们会演示相应的代码,敬请关注。



继续我们上一章的讲解。现在我们用一个具体的程序示例来演示我们插件的加载及卸载过程,我们先回顾一下上一章中我们总结出来的一些思路:
 

  1. 建立一个新的AppDomain: AppDomain.CreateDomain()。
  2. 利用的AppDomain的实例,采用 CreateInstanceFromAndUnwrap() 方法在新的AppDomain中构建一个指定的类型,并返回相应的Proxy。
  3. 根据获取的Proxy就可调用插件了。
  4. 卸载时,完成一些资源清理后可以直接对新建出的AppDomain进行UnLoad


我们先来准备一下Visual Studio中的解决方案:

 
 

在上图的解决方案中,我们新建了三个工程,来看看他们具体的职责:

  1. Unit9.Contract:包含插件的调用接口
  2. Unit9.Implement:包含插件具体的实现
  3. Unit9.App:主程序的代码,动态加载插件,并依据插件的调用接口对插件进行调用。

为什么要做这样的区分呢?因为对于主程序而言,它可以在运行时动态的加载多个插件,但主程序并不需要知道插件内部的具体实现细节。主程序只需要知道被调用插件相应的接口即可完成调用,因此我们有必要将调用规范及插件的具体实现分开。

 

插件的调用接口代码
  1. namespace Unit9   
  2. {   
  3.     public interface IAddin   
  4.     {   
  5.         string Run(string paramString);   
  6.     }   
  7. }  

 

 

定义出来上面的调用接口,我们接下来做插件功能的具体实现。在上章的讲述中,我们总结出了插件的实现必须符合两个规范:

  1. 必须继承自MarshalByRefObject。
  2. 插件中传递的参数类型及返回值必须是可序列化的。

因此依此来做具体的实现:

 

插件的实现代码
  1. using System;   
  2.   
  3. namespace Unit9   
  4. {   
  5.     public class AddinOne:MarshalByRefObject,IAddin   
  6.     {   
  7.         public string Run(string paramString)   
  8.         {   
  9.             const string resultString =   
  10.                 "Current AppDomain:{0},Param String:{1}!";   
  11.   
  12.             return string.Format(   
  13.                 resultString,   
  14.                 AppDomain.CurrentDomain.FriendlyName,   
  15.                 paramString);   
  16.         }   
  17.     }   
  18. }  

 

 

在上面的代码中,我们输出了一个AppDomain的信息。如前面所讲的,我们需要在一个新的AppDomain中加载插件的实现,因此这个AppDomain应该与系统默认的AppDomain应该是不相同的。接下来我们来看看主程序的代码:

 

C#代码
  1. using System;   
  2. using System.IO;   
  3. using System.Runtime.Remoting.Lifetime;   
  4.   
  5. namespace Unit9   
  6. {   
  7.     class Program   
  8.     {   
  9.         /// <summary>   
  10.         /// 构建一个AppDomainSetup实例   
  11.         /// 用于启用卷影复制并设置基本路径   
  12.         /// </summary>   
  13.         public static AppDomainSetup CreateAppDomainSetup()   
  14.         {   
  15.             AppDomainSetup setup = new AppDomainSetup();   
  16.             setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;   
  17.             setup.ShadowCopyFiles = "true";   
  18.             return setup;   
  19.         }   
  20.   
  21.   
  22.         /// <summary>   
  23.         /// 从当前目录下的指定的程序集文件中加载指定的类型   
  24.         /// </summary>   
  25.         public static object CreateAndUnwrap(AppDomain appDomain, string assemblyFile, string typeName)   
  26.         {   
  27.             string fullName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyFile);   
  28.             return appDomain.CreateInstanceFromAndUnwrap(fullName, typeName);   
  29.         }   
  30.   
  31.            
  32.         static void Main()   
  33.         {   
  34.             Console.WriteLine("Current AppDomain:{0}",    
  35.                 AppDomain.CurrentDomain.FriendlyName);   
  36.   
  37.   
  38.             AppDomainSetup setup = CreateAppDomainSetup();   
  39.   
  40.             //建立准备加载插件的AppDomain   
  41.             AppDomain secAppDomain = AppDomain.CreateDomain("SecAppDomain"null, setup);   
  42.   
  43.   
  44.             //忽略新建立的AppDomain里面的调用租约管理   
  45.             secAppDomain.DoCallBack(delegate  
  46.             {   
  47.                 LifetimeServices.LeaseTime = TimeSpan.Zero;   
  48.             });   
  49.   
  50.             IAddin addinOne = (IAddin) CreateAndUnwrap(   
  51.                                            secAppDomain, "Unit9.Implement.dll""Unit9.AddinOne");   
  52.   
  53.             Console.WriteLine(addinOne.Run("Test"));   
  54.   
  55.   
  56.             //卸载装入插件的AppDomain   
  57.             AppDomain.Unload(secAppDomain);   
  58.   
  59.             //由于插件所在的AppDmain已被卸载,所以以下的执行会出现异常   
  60.             try  
  61.             {   
  62.                 Console.WriteLine(addinOne.Run("Test"));   
  63.             }   
  64.             catch(Exception ex)   
  65.             {   
  66.                 Console.WriteLine("发生调用异常:"+ex.Message);   
  67.             }   
  68.                
  69.             Console.ReadLine();   
  70.         }   
  71.     }   
  72. }   

如果大家对于AppDomain及Remoting有一定了解的话,上面的代码不难理解。我们建立了一个新的AppDomain,并开启了这个AppDomain的卷影复制功能,卷影复制功能其实是不一定需要的,只是这样做的话可以让程序运行时不至少锁住相关的程序集文件。接下来,我们取消了对新建立的AppDomain的租约管理,因为我们的插件是一个长效的服务。再接着,我们从指定的插件实现文件中加载了指定的服务,如果在这真正的插件架构现中,这个过程可以进行配置化的。我们看看输出的结果: