Jusfr 原創(chuàng),文章所用代碼已給出,轉(zhuǎn)載請(qǐng)注明來自博客園。
開始之前還是得說:插件機(jī)制老生常談,但一下子到某工廠或 MAF 管線我相信不少園友吃不消。授人以魚不如授人以漁,個(gè)人覺得思考過程的引導(dǎo)和干貨一樣重要,不然大家直接看 MSDN 或者 API 文檔好了。
“CLR不提供缷載單獨(dú)程序集的能力。如果CLR允許這樣做,那么一旦線程從某個(gè)方法返回至已缷載的一個(gè)程序集中的代碼,應(yīng)用程序就會(huì)崩潰。健壯性和安全性是CLR最優(yōu)先考慮的目標(biāo),如果允許應(yīng)用程序以這樣的一種方式崩潰,就和它的設(shè)計(jì)初衷背道而馳了。缷載應(yīng)用程序集必須缷載包含它的整個(gè) AppDoamin 。” ———— 出自《CLR via C#》519頁(yè)。
想要達(dá)到插件化目的,必須手動(dòng)創(chuàng)建 AppDomain 作為插件容器和邊界,在需要時(shí)卸載 AppDomain 以達(dá)到卸載插件的目的。這里不得不提及 MEF 和 MAF。MEF 使用 Import 與 Export 進(jìn)行類型發(fā)現(xiàn)和元數(shù)據(jù)查找,還維護(hù)了組件生命周期,但與插件機(jī)制并無關(guān)聯(lián),多數(shù)情況下把它歸納到注入工具比較合適;MAF 極為強(qiáng)大但仍然是上述原理的運(yùn)用,過于厚重關(guān)注有限。
.Net 下插件限制已經(jīng)在文章開始的時(shí)候進(jìn)行了描述,機(jī)制就是自定義 AppDomain 的創(chuàng)建與缷載,實(shí)現(xiàn)并不復(fù)雜,貼一段 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() 調(diào)用后刪掉該程序集,它滿足動(dòng)態(tài)缷載的要求。
但是這個(gè) Demo 程序?qū)嵲谑怯刑鄦栴}:
1)如果 IPlugin 是空的標(biāo)記接口,那么宿主無法調(diào)用實(shí)現(xiàn)類的業(yè)務(wù)邏輯;如果 IPlugin 是非空的業(yè)務(wù)接口,那么類庫(kù)職責(zé)與應(yīng)用職混淆在了一起? 2)接口實(shí)現(xiàn)類和關(guān)聯(lián)類型必須使用 [Serializable] 標(biāo)記或者從 MarshalByRefObject 派生,由于生產(chǎn)環(huán)境存在相當(dāng)多的數(shù)據(jù)類型及引用,可能需要把業(yè)務(wù)上的數(shù)據(jù)結(jié)構(gòu)改個(gè)遍,甚至不能實(shí)現(xiàn); 3)插件的隔離性沒有體現(xiàn)出來,不同插件可能有不同的數(shù)據(jù)庫(kù)連接和獨(dú)立的第三方類庫(kù)引用,程序發(fā)布成為難題;
前文列舉的問題就是我們要解決的問題:
1)可運(yùn)行時(shí)加載/缷載,基本原理在 Demo 中得到了體現(xiàn),但是實(shí)現(xiàn)得非常丑陋,管理 AppDomain 是核心的底層邏輯,不應(yīng)該出現(xiàn)在啟動(dòng)過程中; 2)劃清類庫(kù)開發(fā)與應(yīng)用開發(fā)邊界,我期望創(chuàng)建出可重復(fù)使用的插件機(jī)制而不要混入一大坨業(yè)務(wù)邏輯; 3)保證隔離性,插件需要擁有獨(dú)立配置文件、各自升級(jí)的能力;
我們先進(jìn)入下一節(jié)作些準(zhǔn)備工作;
.Net 進(jìn)程總是會(huì)創(chuàng)建默認(rèn) AppDomain,由于插件化需要額外的 AppDomain,難免出現(xiàn)跨 AppDomain 邊界訪問對(duì)象的問題,比如宿主調(diào)用插件、為插件傳遞參數(shù)、獲取插件的計(jì)算結(jié)果等等,我們知道有兩種方法可以使用:標(biāo)記 [Serializable] 以按值封送、從 MarshalByRefObject 派生以按引用封送。
舉例,我們定義某接口包含了推送消息的方法 bool Push(Message message) ,如果期望在自定義 AppDomain 中創(chuàng)建實(shí)現(xiàn)類,那么該實(shí)現(xiàn)類需要標(biāo)記 [Serializable] 以按值封送或從 MarshalByRefObject 派生以按引用封送;額外地,按引用封送時(shí),被依賴的 Message 對(duì)象也需要滿足跨邊界訪問要求。
那么按值封送時(shí)類型 Message 不用特殊處理 ?確實(shí)如此,簡(jiǎn)單解釋下,為封送方式的選擇作出解釋。
使用過 System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 的同學(xué)應(yīng)該和 "SerializationException: Type 'xxoo' in Assembly 'ooxx' is not marked as serializable." 打過交道。按值封送是一個(gè)序列化和反序列化的過程,看起來我們?cè)谧远x AppDomain 中進(jìn)行了類型實(shí)例化并拿到引用,實(shí)際上發(fā)生了更多事情:原始實(shí)例被序列化為字節(jié)數(shù)組傳回調(diào)用邏輯所在 AppDomain,然后字節(jié)數(shù)組反序列化,該類型所在和相關(guān)的程序集被視需求加載,最后得到了是對(duì)原始對(duì)象的精確拷貝及該拷貝的引用,而原始類型實(shí)例會(huì)在垃圾回收中被銷毀。
按值封送的類型實(shí)例化過程中,相關(guān)程序集已在調(diào)用方 AppDomain 完成加載即我們已經(jīng)擁有 Message 類型信息,調(diào)用 Push() 方法時(shí)不會(huì)存在跨 AppDomain 邊界訪問對(duì)象的問題,故 Message 對(duì)象無須處理。
按引用封送拿到的是類型實(shí)例的代理,我們通過它與原始對(duì)象打交道。基于上述描述和可缷載的插件化要求,我們應(yīng)該選擇按引用封送。
接著關(guān)注下性能問題,以下是基本測(cè)試。
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 相對(duì)其他實(shí)現(xiàn)比較特殊,它是一個(gè)裝飾器模式;調(diào)用測(cè)試如下,PerformanceRecorder 是我寫的測(cè)試類,它內(nèi)部包含一個(gè) Stopwatch,接收整型數(shù)及一個(gè)委托列表,返回每個(gè)委托執(zhí)行聲明次數(shù)所需要的時(shí)間等結(jié)果;
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 }可以看到結(jié)果:標(biāo)記 [Serializable] 的 MarshalByRefValuePlugin,由于實(shí)例調(diào)用并不會(huì)發(fā)生跨 AppDomain 邊界的對(duì)象訪問,無論是直接創(chuàng)建還是使用自定義 AppDomain 創(chuàng)建都沒有顯著的性能差異;而繼承自 MarshalByRefObject 的 MarshalByRefTypePlugin,在默認(rèn) AppDomain 中調(diào)用時(shí)性能十分接近,一旦在自定義 AppDomain 中創(chuàng)建、在默認(rèn) AppDomain 中訪問時(shí),性能直跌谷底。
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 實(shí)例工作,因?yàn)?IPlugin 調(diào)用發(fā)生在自定義 AppDomain 內(nèi)部,這里沒有跨 AppDomain 邊界的對(duì)象訪問! 雖然相比直接調(diào)用存在不小性能差距,但相比直接引用 IPlugin 在自定義 AppDomain 中的實(shí)例還是高效太多,有所啟示嗎?
X++ 就是業(yè)務(wù)邏輯,通過調(diào)用 MarshalByRefTypePluginProxy.Proceed() 間接調(diào)用業(yè)務(wù)邏輯,我們得到了性能收益,同時(shí)因?yàn)椴辉賹?duì) IPlugin 的實(shí)現(xiàn)有封送要求,我們做到了對(duì)業(yè)務(wù)邏輯沒有入侵。
一方面接口可以有相當(dāng)多的實(shí)現(xiàn),而去操作每個(gè)實(shí)例過于細(xì)粒度;另一方面實(shí)踐中我們常常以項(xiàng)目即 Visual Studio 里的 Project 定義業(yè)務(wù),所以我選擇使用項(xiàng)目編譯結(jié)果作為插件邊界。使用文件夾分隔能很方便地保證物理隔離,同時(shí)配合 AppDomainSetup 初始化 AppDomain 能做到配置文件和第三方類庫(kù)引用獨(dú)立!
另一方面前文提到的 MarshalByRefTypePluginProxy 相對(duì)直接的插件調(diào)用有一定的性能優(yōu)勢(shì),我們可以將其與自定義 AppDomain 關(guān)聯(lián)、充當(dāng)宿主與插件的橋梁,達(dá)到調(diào)用業(yè)務(wù)邏輯、插件管理的目的。

核心類型為 IPluginCatalog 與 IPluginCatalogProxy。前者并供應(yīng)用開發(fā)人員擴(kuò)展以操作業(yè)務(wù)邏輯,后者聚合前者,通過路徑管理自定義 AppDomain 和 IPluginCatalog 實(shí)例;IPluginResolver 承擔(dān)默認(rèn)的類型發(fā)現(xiàn)職責(zé)。

IPluginCatalog 與相關(guān)實(shí)現(xiàn):IPluginCatalog 僅定義了插件目錄,泛型 IPluginCatalog<out T> 定義了插件類型查找方法,PluginCatalog<T> 繼承自 MarshalByRefObject 作為默認(rèn)實(shí)現(xiàn),F(xiàn)indPlugins() 被標(biāo)記為虛方法,應(yīng)用開發(fā)人員可以很方便地重寫,而 InitializeLifetimeService() 方法返回 null 以避免原始對(duì)象被垃圾回收。
IPluginCatalogProxy 與相關(guān)實(shí)現(xiàn): IPluginCatalogProxy 定義了泛型的 Construct<T, P>() 方法和約束,T 被要求從 IPluginCatalog<P> 定義。PluginCatalogProxy.Construct() 方法調(diào)用前會(huì)檢查內(nèi)部字典以創(chuàng)建或獲取自定義 AppDomain,接著在該 AppDomain 上創(chuàng)建類型為 T 的 IPluginCatalog<P> 實(shí)例;Release() 方法執(zhí)行 AppDomain 的查找和卸載邏輯,用戶擴(kuò)展的 IPluginCatalog 實(shí)例還可以定義資源清理工作,例如停止計(jì)數(shù)器、釋放數(shù)據(jù)庫(kù)連接。
注意:本例中的IPluginCatalog 實(shí)現(xiàn)及類型的實(shí)例均調(diào)用了的使用了 AppDomain.CreateInstanceAndUnwrap(string assemblyName, string typeName) 重載,該方法將調(diào)用目標(biāo)類型的無參構(gòu)造函數(shù),其他重載更強(qiáng)大也很復(fù)雜,請(qǐng)自行查看。
邏輯不過百來行,就不打包了。

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 類型發(fā)現(xiàn)相關(guān) 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
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注