Jusfr 原创,文章所用代码已给出,转载请注明来自博客园。
开始之前还是得说:插件机制老生常谈,但一下子到某工厂或 MAF 管线我相信不少园友吃不消。授人以鱼不如授人以渔,个人觉得思考过程的引导和干货一样重要,不然大家直接看 MSDN 或者 API 文档好了。
“CLR不提供缷载单独程序集的能力。如果CLR允许这样做,那么一旦线程从某个方法返回至已缷载的一个程序集中的代码,应用程序就会崩溃。健壮性和安全性是CLR最优先考虑的目标,如果允许应用程序以这样的一种方式崩溃,就和它的设计初衷背道而驰了。缷载应用程序集必须缷载包含它的整个 AppDoamin 。” ———— 出自《CLR via C#》519页。
想要达到插件化目的,必须手动创建 AppDomain 作为插件容器和边界,在需要时卸载 AppDomain 以达到卸载插件的目的。这里不得不提及 MEF 和 MAF。MEF 使用 Import 与 Export 进行类型发现和元数据查找,还维护了组件生命周期,但与插件机制并无关联,多数情况下把它归纳到注入工具比较合适;MAF 极为强大但仍然是上述原理的运用,过于厚重关注有限。
.Net 下插件限制已经在文章开始的时候进行了描述,机制就是自定义 AppDomain 的创建与缷载,实现并不复杂,贴一段 Demo:
1 static void Main(string[] args) {2 var pluginDomain = AppDomain.CreateDomain("ad#1");3 var pluginType = typeof(Plugin); // Other ways4 var pluginInstance = (iplugin)pluginDomain.CreateInstanceAndUnwrap(pluginType.Assembly.FullName, pluginType.FullName);5 6 // Do stuff with pluginInstance7 AppDomain.Unload(pluginDomain);8 }
我们可以通过反射拿到定义在其他程序集中的 pluginType ,并在 AppDomain.Unload() 调用后删掉该程序集,它满足动态缷载的要求。
但是这个 Demo 程序实在是有太多问题:
1)如果 IPlugin 是空的标记接口,那么宿主无法调用实现类的业务逻辑;如果 IPlugin 是非空的业务接口,那么类库职责与应用职混淆在了一起? 2)接口实现类和关联类型必须使用 [Serializable] 标记或者从 MarshalByRefObject 派生,由于生产环境存在相当多的数据类型及引用,可能需要把业务上的数据结构改个遍,甚至不能实现; 3)插件的隔离性没有体现出来,不同插件可能有不同的数据库连接和独立的第三方类库引用,程序发布成为难题;
前文列举的问题就是我们要解决的问题:
1)可运行时加载/缷载,基本原理在 Demo 中得到了体现,但是实现得非常丑陋,管理 AppDomain 是核心的底层逻辑,不应该出现在启动过程中; 2)划清类库开发与应用开发边界,我期望创建出可重复使用的插件机制而不要混入一大坨业务逻辑; 3)保证隔离性,插件需要拥有独立配置文件、各自升级的能力;
我们先进入下一节作些准备工作;
.Net 进程总是会创建默认 AppDomain,由于插件化需要额外的 AppDomain,难免出现跨 AppDomain 边界访问对象的问题,比如宿主调用插件、为插件传递参数、获取插件的计算结果等等,我们知道有两种方法可以使用:标记 [Serializable] 以按值封送、从 MarshalByRefObject 派生以按引用封送。
举例,我们定义某接口包含了推送消息的方法 bool Push(Message message) ,如果期望在自定义 AppDomain 中创建实现类,那么该实现类需要标记 [Serializable] 以按值封送或从 MarshalByRefObject 派生以按引用封送;额外地,按引用封送时,被依赖的 Message 对象也需要满足跨边界访问要求。
那么按值封送时类型 Message 不用特殊处理 ?确实如此,简单解释下,为封送方式的选择作出解释。
使用过 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 的同学应该和 "SerializationException: Type 'xxoo' in Assembly 'ooxx' is not marked as serializable." 打过交道。按值封送是一个序列化和反序列化的过程,看起来我们在自定义 AppDomain 中进行了类型实例化并拿到引用,实际上发生了更多事情:原始实例被序列化为字节数组传回调用逻辑所在 AppDomain,然后字节数组反序列化,该类型所在和相关的程序集被视需求加载,最后得到了是对原始对象的精确拷贝及该拷贝的引用,而原始类型实例会在垃圾回收中被销毁。
按值封送的类型实例化过程中,相关程序集已在调用方 AppDomain 完成加载即我们已经拥有 Message 类型信息,调用 Push() 方法时不会存在跨 AppDomain 边界访问对象的问题,故 Message 对象无须处理。
按引用封送拿到的是类型实例的代理,我们通过它与原始对象打交道。基于上述描述和可缷载的插件化要求,我们应该选择按引用封送。
接着关注下性能问题,以下是基本测试。
1 public interface IPlugin { 2 Int32 X { get; set; } 3 } 4 5 public class Plugin : IPlugin { 6 public Int32 X { get; set; } 7 } 8 9 [Serializable]10 public class MarshalByRefValuePlugin : IPlugin {11 public Int32 X { get; set; }12 }13 14 public class MarshalByRefTypePlugin : MarshalByRefObject, IPlugin {15 public Int32 X { get; set; }16 }17 18 public class MarshalByRefTypePluginPRoxy : MarshalByRefObject {19 private readonly IPlugin h = new Plugin();20 21 public void Proceed() {22 h.X++;23 }24 }
MarshalByRefTypePluginProxy 相对其他实现比较特殊,它是一个装饰器模式;调用测试如下,PerformanceRecorder 是我写的测试类,它内部包含一个 Stopwatch,接收整型数及一个委托列表,返回每个委托执行声明次数所需要的时间等结果;
1 static void Main(string[] args) { 2 var h1 = new Plugin(); 3 var h2 = new MarshalByRefValuePlugin(); 4 var h3 = new MarshalByRefTypePlugin(); 5 6 AppDomain ad = AppDomain.CreateDomain("ad#2"); 7 var t1 = typeof(MarshalByRefTypePlugin); 8 var h4 = (IPlugin)ad.CreateInstanceAndUnwrap(t1.Assembly.FullName, t1.FullName); 9 var t2 = typeof(MarshalByRefValuePlugin);10 var h5 = (IPlugin)ad.CreateInstanceAndUnwrap(t2.Assembly.FullName, t2.FullName);11 12 var t3 = typeof(MarshalByRefTypePluginProxy);13 var py = (MarshalByRefTypePluginProxy)ad.CreateInstanceAndUnwrap(t3.Assembly.FullName, t3.FullName);14 15 var records = PerformanceRecorder.Invoke(100000,16 () => h1.X++, () => h3.X++, () => h2.X++, () => h4.X++, () => h5.X++, py.Proceed);17 18 foreach (var r in records) {19 Console.WriteLine("{0} {1,4} {2}",20 r.RunningTime, r.CollectionCount, r.TotalMemory);21 }22 }
可以看到结果:标记 [Serializable] 的 MarshalByRefValuePlugin,由于实例调用并不会发生跨 AppDomain 边界的对象访问,无论是直接创建还是使用自定义 AppDomain 创建都没有显著的性能差异;而继承自 MarshalByRefObject 的 MarshalByRefTypePlugin,在默认 AppDomain 中调用时性能十分接近,一旦在自定义 AppDomain 中创建、在默认 AppDomain 中访问时,性能直跌谷底。
00:00:00.0016055 3 6300400:00:00.0020829 6 6798800:00:00.0019477 9 6798800:00:01.7473949 146 7164800:00:00.0020485 149 7164800:00:00.0770707 152 71648Press any key to continue . . .
采取装饰器模式的 MarshalByRefTypePluginProxy 很有意思,它依赖 IPlugin 实例工作,因为 IPlugin 调用发生在自定义 AppDomain 内部,这里没有跨 AppDomain 边界的对象访问! 虽然相比直接调用存在不小性能差距,但相比直接引用 IPlugin 在自定义 AppDomain 中的实例还是高效太多,有所启示吗?
X++ 就是业务逻辑,通过调用 MarshalByRefTypePluginProxy.Proceed() 间接调用业务逻辑,我们得到了性能收益,同时因为不再对 IPlugin 的实现有封送要求,我们做到了对业务逻辑没有入侵。
一方面接口可以有相当多的实现,而去操作每个实例过于细粒度;另一方面实践中我们常常以项目即 Visual Studio 里的 Project 定义业务,所以我选择使用项目编译结果作为插件边界。使用文件夹分隔能很方便地保证物理隔离,同时配合 AppDomainSetup 初始化 AppDomain 能做到配置文件和第三方类库引用独立!
另一方面前文提到的 MarshalByRefTypePluginProxy 相对直接的插件调用有一定的性能优势,我们可以将其与自定义 AppDomain 关联、充当宿主与插件的桥梁,达到调用业务逻辑、插件管理的目的。
核心类型为 IPluginCatalog 与 IPluginCatalogProxy。前者并供应用开发人员扩展以操作业务逻辑,后者聚合前者,通过路径管理自定义 AppDomain 和 IPluginCatalog 实例;IPluginResolver 承担默认的类型发现职责。
IPluginCatalog 与相关实现:IPluginCatalog 仅定义了插件目录,泛型 IPluginCatalog<out T> 定义了插件类型查找方法,PluginCatalog<T> 继承自 MarshalByRefObject 作为默认实现,FindPlugins() 被标记为虚方法,应用开发人员可以很方便地重写,而 InitializeLifetimeService() 方法返回 null 以避免原始对象被垃圾回收。
IPluginCatalogProxy 与相关实现: IPluginCatalogProxy 定义了泛型的 Construct<T, P>() 方法和约束,T 被要求从 IPluginCatalog<P> 定义。PluginCatalogProxy.Construct() 方法调用前会检查内部字典以创建或获取自定义 AppDomain,接着在该 AppDomain 上创建类型为 T 的 IPluginCatalog<P> 实例;Release() 方法执行 AppDomain 的查找和卸载逻辑,用户扩展的 IPluginCatalog 实例还可以定义资源清理工作,例如停止计数器、释放数据库连接。
注意:本例中的IPluginCatalog 实现及类型的实例均调用了的使用了 AppDomain.CreateInstanceAndUnwrap(string assemblyName, string typeName) 重载,该方法将调用目标类型的无参构造函数,其他重载更强大也很复杂,请自行查看。
逻辑不过百来行,就不打包了。
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel.Composition.Hosting; 4 using System.IO; 5 using System.Reflection; 6 using System.Linq; 7 using System.Text; 8 using System.Threading.Tasks; 9 10 namespace ChuyeEventBus.Plugin { 11 #region 类型发现相关 12 public interface IPluginResolver { 13 IEnumerable<T> FindAll<T>(String pluginFolder); 14 } 15 16 public class MefPluginResolver : IPluginResolver { 17 public IEnumerable<T> FindAll<T>(String pluginFolder) { 18 var catalog = new AggregateCatalog(); 19 catalog.Catalogs.Add(new DirectoryCatalog(pluginFolder)); 20 var container = new CompositionContainer(catalog); 21
新闻热点
疑难解答