【.Net平台下插件開發】-MEF與MAF初步調研


背景


Team希望開發一個插件的平台去讓某搜索引擎變得更好。主要用於采集一些不滿意信息(DSAT)給Dev。這些信息會由不同的team提供不同的tool分析。有的提供僅僅是一個website,有的提供了api。有的提供了service。所以我們設想做一個插件的平台。讓那些team提供一些dll。我們只需要把這些dll放在我們的platform里。

由於對插件開發一無所知。所以重頭開始做調研。

 

為什么需要插件框架-擴展性問題

假設您的應用程序必須包含大量可能需要的較小組件,並負責創建和運行這些組件。解決這一問題的最簡單的方法是:將這些組件作為源代碼包括在您的應用程序中,然后通過代碼直接調用它們。 這種做法存在很多明顯的缺陷。 最重要的是,您無法在不修改源代碼的情況下添加新組件,這一限制在 Web 應用程序(舉例來說)中也許能夠接受,但在客戶端應用程序中行不通。 同樣存在問題的還有,您可能沒有對組件的源代碼的訪問權,因為這些組件可能是由第三方開發的,而出於相同的原因,您也不允許第三方訪問您的代碼。

一種稍微復雜的方法是:提供擴展點或接口,以允許應用程序與其組件相分離。 依據此模型,您可能會提供一個組件能夠實現的接口,並提供一個 API 以使該接口能夠與您的應用程序進行交互。 這一方法可解決需要源代碼訪問權的問題,但仍具有自己的難點。

由於應用程序缺乏自己發現組件的能力,因此仍必須明確告知應用程序哪些組件可用並應加載。 這通常是通過在一個配置文件中顯式注冊可用組件來實現的。 這意味着,確保組件正確無誤成為了一個日常維護問題,尤其是在執行更新操作的是最終用戶而非開發人員的情況下。

此外,各組件之間無法進行通信,除非是通過應用程序自身的嚴格定義的通道。 如果應用程序架構師未預計到需要某項通信,則通常是無法進行相應的通信的。

最后,組件開發人員不得不硬依賴於包含他們實現的接口的程序集。 這樣就很難在多個應用程序中使用同一個組件,另外,在為組件創建測試框架時也會造成問題。

 

如何解決?

我們可以使用MS提供的擴展性框架 MEF 或 MAF。

什么是MEF?

官方說法 Managed Extensibility FrameworkMEF)是.NET平台下的一個擴展性管理框架,它是一系列特性的集合,包括依賴注入(DI)等。MEF為開發人員提供了一個工具,讓我們可以輕松的對應用程序進行擴展並且對已有的代碼產生最小的影響,開發人員在開發過程中根據功能要求定義一些擴展點,之后擴展人員就可以使用這些擴展點與應用程序交互;同時MEF讓應用程序與擴展程序之間不產生直接的依賴,這樣也允許在多個具有同樣的擴展需求之間共享擴展程序。

關鍵詞: Parts,Catalogs,Composition container,Export ,Import,Discovery and Avoiding Discovery,Creation policies,Life Cycle and Disposing

 

關於上述的擴展性問題,MEF能給我們帶來什么?

有別於上邊種顯式注冊可用組件的做法,MEF 提供一種通過“組合”隱式發現組件的方法。

MEF 提供一種通過“組合”隱式發現組件的方法。 MEF 組件(稱為“部件-Part”)。部件以聲明方式同時指定其依賴項(稱為“導入-Import”)及其提供的功能(稱為“導出-Export”)。

MEF原理上很簡單,找出有共同接口的導入、導出。然后找到把導出的實例化,賦給導入。說到底MEF就是找到合適的類實例化,把它交給導入。

創建一個部件時,MEF 組合引擎會使其導入與其他部件提供的內容相符合。由於 MEF 部件以聲明方式指定其功能,因此在運行時可發現這些部件。這意味着,應用程序無需硬編碼的引用或脆弱的配置文件即可利用相關部件。 通過 MEF,應用程序可以通過部件的元數據來發現並檢查部件,而不用實例化部件,或者甚至不用加載部件的程序集。 因此,沒有必要仔細指定應何時以及如何加載擴展。 

使用 MEF 編寫的可擴展應用程序會聲明一個可由擴展組件填充的導入,而且還可能會聲明導出,以便向擴展公開應用程序服務。 每個擴展組件都會聲明一個導出,而且還可能會聲明導入。 通過這種方式,擴展組件本身是自動可擴展的。

如何聲明一個部件-導入與導出

導出”是部件向容器中的其他部件提供的一個值,而“導入”是部件向要通過可用導出滿足的容器提出的要求。 在特性化編程模型中,導入和導出是由修飾類或成員使用 Import 和Export 特性聲明的。 Export 特性可修飾類、字段、屬性或方法,而 Import 特性可修飾字段、屬性或構造函數參數。為了使導入與導出匹配,導入和導出必須具有相同的協定。

假設有一個類MyClass,它聲明了可以導入插件的類型是IMyAddin。

 

public class MyClass
{
[Import]
public IMyAddin MyAddin { get; set; }
}

這里有一個類,它聲明為導出。類型同樣為IMyAddin。

[Export(typeof(IMyAddin))]
public class MyLogger : IMyAddin { }
這樣我們使用MyAddin屬性的時候就可以獲得到MyLogger的實例。

如何導入多個部件?

一般的 ImportAttribute 特性由一個且只由一個 ExportAttribute 填充。 如果有多個導出可用,則組合引擎將生成錯誤。若要創建一個可由任意數量的導出填充的導入,可以使用 ImportManyAttribute 特性。

將以下 operations 屬性添加到 MySimpleCalculator 類中:

[ImportMany]
IEnumerable<IMyAddin> MyAddins;

 

導入和導出的繼承

如果某個類繼承自部件,則該類也可能會成為部件。 導入始終由子類繼承。 因此,部件的子類將始終為部件,並具有與其父類相同的導入。通過使用 Export 特性的聲明的導出不會由子類繼承。 但是,部件可通過使用 InheritedExport 特性繼承自身。 部件的子類將繼承並提供相同的導出,其中包括協定名稱和協定類型。 與 Export 特性不同,InheritedExport 只能在類級別(而不是成員級別)應用。 因此,成員級別導出永遠不能被繼承。

下面四個類演示了導入和導出繼承的原則。 NumTwo 繼承自 NumOne,因此 NumTwo 將導入 IMyData。 普通導出不會被繼承,因此 NumTwo 將不會導出任何內容。 NumFour 繼承自NumThree。 由於 NumThree 使用了 InheritedExport,因此 NumFour 具有一個協定類型為 NumThree 的導出。 成員級別導出從不會被繼承,因此不會導出 IMyData。

[Export]
public class NumOne
{
[Import]
public IMyData MyData { get; set; }
}

public class NumTwo : NumOne
{
//導入會被繼承,所以NumTwo會有導入屬性 IMyData

//原始的導出不能被繼承,所以NumTwo不會有任何導出。所以它不會被目錄發現
}

[InheritedExport]
public class NumThree
{
[Export]
Public IMyData MyData { get; set; }

//這個部件提供兩個導出,一個是NumThree,一個是IMyData類型的MyData
}

public class NumFour : NumThree
{
//因為NumThree使用了InheritedExport特性,這個部件有一個導出NumThree。

//成員級別的導出永遠不會被繼承,所以IMydata永遠不是導出
}

 

發現部件
 
MEF提供三種方式發現部件
AssemblyCatalog 在當前程序集發現部件。
DirectoryCatalog 在指定的目錄發現部件。
DeploymentCatalog 在指定的XAP文件中發現部件(用於silverlight)
當通過不同方式發現部件的時候,還可以使用AggregateCatalog來把這些部件聚合到一起。
var catalog = new AggregateCatalog();
//把從Program所在程序集中發現的部件添加到目錄中
catalog.Catalogs.Add(new AssemblyCatalog(typeof(Program).Assembly));
//把從指定path發現的部件添加到目錄中
catalog.Catalogs.Add(new DirectoryCatalog("C:\\Users\\v-rizhou\\SimpleCalculator\\Extensions"));
上邊的代碼分別從從程序集和指定路徑讀取目錄信息
 
        
如何組合部件?

在加載完部件之后,要把它們放到一個CompositionContainer容器中。

var container = new CompositionContainer(catalog)
通過調用容器的ComposeParts()方法可以把容器中的部件組合到一起。
container.ComposeParts(this);


如何避免被發現?

在某些情況下,您可能需要防止部件作為目錄的一部分被發現。 例如,部件可能是應從中繼承(而不是使用)的基類。 可通過兩種方式來實現此目的。 首先,可以對部件類使用abstract 關鍵字。 盡管抽象類能夠向派生自抽象類的類提供繼承的導出,但抽象類從不提供導出。如果無法使類成為抽象類,您可以使用 PartNotDiscoverable 特性來修飾它。 用此特性修飾的部件將不會包括在任何目錄中。如下程序中,只有DataOne可以被發現。

[Export]
public class DataOne
{
//This part will be discovered
//as normal by the catalog.
}

[Export]
public abstract class DataTwo
{
//This part will not be discovered
//by the catalog.
}

[PartNotDiscoverable]
[Export]
public class DataThree
{
//This part will also not be discovered
//by the catalog.
}


元數據和元數據視圖

導出可提供有關自身的附加信息(稱為元數據)。 元數據可用於將導出的對象的屬性傳遞到導入部件。 導入部件可以使用此數據來決定要使用哪些導出,或收集有關導出的信息而不必構造導出。 因此,導入必須為延遲導入才能使用元數據

為了使用元數據,您通常會聲明一個稱為元數據視圖的接口,該接口聲明什么元數據將可用。 元數據視圖接口必須只有屬性,並且這些屬性必須具有 get 訪問器。 下面的接口是一個示例元數據視圖

public interface IPluginMetadata
{
string Name { get; }

[DefaultValue(1)]
int Version { get; }
}

通常,在元數據視圖中命名的所有屬性都是必需的,並且不會將未提供這些屬性的任何導出視為匹配。 DefaultValue 特性指定屬性是可選的。 如果未包括屬性,則將為其分配指定為 DefaultValue 的參數的默認值。 下面是用元數據修飾的兩個不同的類。 這兩個類都將與前面的元數據視圖匹配。

[Export(typeof(IPlugin)),
ExportMetadata("Name", "Logger"),
ExportMetadata("Version", 4)]
public class Logger : IPlugin
{
}

[Export(typeof(IPlugin)),
ExportMetadata("Name", "Disk Writer")]
//Version is not required because of the DefaultValue
public class DWriter : IPlugin
{
}

元數據是通過使用 ExportMetadata 特性在 Export 特性之后表示的。 每一段元數據都由一個名稱/值對組成。 元數據的名稱部分必須與元數據視圖中相應屬性的名稱匹配,並且值將分配給該屬性

導入程序負責指定將使用的元數據視圖(如果有)。 包含元數據的導入將聲明為延遲導入,其元數據接口作為 Lazy<T,T> 的第二個類型參數。 下面的類導入前面的部件以及元數據

public class Addin
{
[Import]
public Lazy<IPlugin, IPluginMetadata> plugin;
}

在許多情況下,您需要將元數據與 ImportMany 特性結合,以便分析各個可用的導入並選擇僅實例化一個導入,或者篩選集合以匹配特定條件。 下面的類僅實例化具有 Name值“Logger”的 IPlugin 對象。

public class User
{
[ImportMany]
public IEnumerable<Lazy<IPlugin, IPluginMetadata>> plugins;

public IPlugin InstantiateLogger ()
{
IPlugin logger = null;

foreach (Lazy<IPlugin, IPluginMetadata> plugin in plugins)
{
if (plugin.Metadata.Name = "Logger") logger = plugin.Value;
}
return logger;
}
}


自定義導出特性

可以對基本導出特性 Export 和 InheritedExport 進行擴展,以包括元數據作為特性屬性。 在將類似的元數據應用於多個部件或創建元數據特性的繼承樹時,此方法十分有用。

自定義特性可以指定協定類型、協定名稱或任何其他元數據。 為了定義自定義特性,必須使用 MetadataAttribute 特性來修飾繼承自 ExportAttribute(或InheritedExportAttribute)的類。 下面的類定義一個自定義特性。

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
public class MyAttribute : ExportAttribute
{
public MyAttribute(string myMetadata)
: base(typeof(IMyAddin))
{
MyMetadata = myMetadata;
}

public string MyMetadata { get; private set; }
}

下面兩個聲明等效:

[Export(typeof(IMyAddin), 
ExportMetadata("MyMetadata", "theData")]
public MyAddin myAddin { get; set; }


[MyAttribute("theData")]
public MyAddin myAddin { get; set; }


創建策略

當部件指定執行導入和組合時,組合容器將嘗試查找匹配的導出。 如果它將導入與導出成功匹配,則導入成員將設置為導出的對象的實例。 導出部件的創建策略控制此實例來源於何處。導入和導出都可從值 Shared、NonShared 或 Any 中指定部件的創建策略。 導入和導出的默認值均為 Any。

例如:

[Import(RequiredCreationPolicy = CreationPolicy.Shared)]


生命周期和釋放

由於部件承載於組合容器中,因此其生命周期可能比普通對象更復雜。需要在關閉時執行工作的部件和需要釋放資源的部件應照常為 .NET Framework 對象實現 IDisposable。 但是,由於容器創建並維護對部件的引用,因此只有擁有部件的容器才應對其調用 Dispose 方法。 容器本身實現 IDisposable,並且作為 Dispose 中其清理的一部分,它將對擁有的所有部件調用 Dispose。 因此,當不再需要組合容器及其擁有的任何部件時,您應始終釋放該組合容器。

對於生存期很長的組合容器,創建策略為“非共享”的部件的內存消耗可能會成為問題。 這些非共享部件可以多次創建,並且在容器本身被釋放之前將不會得到釋放。 為了應對這種情況,容器提供了 ReleaseExport 方法。 如果對非共享導出調用此方法,將會從組合容器中移除該導出並將其釋放。 僅由移除的導出使用的部件以及樹中更深層的諸如此類部件將也會被移除並得到釋放。 通過這種方式,不必釋放組合窗口本身即可回收資源。


微軟的另一個插件管理框架-MAF (Managed Add-in Framework 

 

微軟提供的另一個可擴展性選項是托管在框架(MAF)。這是在System.AddIn命名空間。NET 3.5中引入。

這個框架插件可以配置為運行在他們自己的應用程序域。它最大的特點就是它可以防止您的應用程序崩潰的第三方插件。

外接程序模型:

外接程序模型包含一系列的段,這些段組成負責外接程序和宿主之間所有通信的外接程序管線(也稱為通信管線)。 管線是在外接程序與外接程序宿主之間交換數據的段的對稱通信模型。 在宿主和外接程序之間開發這些管線段可以提供必需的抽象層,用於支持外接程序的版本管理和隔離。

 
        

為了使 .NET Framework 發現管線段並激活外接程序,必須將管線段放在指定的目錄中。 需要使用指定的目錄名,但它們不區分大小寫。 唯一沒有指定的名稱是管線根目錄的名稱(提供給發現方法)以及包含外接程序的子目錄的名稱。 所有指定的段名稱必須是管線根目錄下位於同一級別的子目錄。

 
        

向后兼容性:

假設我們有了一個新的宿主版本2.  為了使外接程序的版本 1 能夠與新宿主和協定一起工作,管線包含了用於版本 1 的外接程序視圖和外接程序端適配器,該適配器可以將數據從舊外接程序視圖轉換為新協定。 下面的插圖顯示了兩個外接程序如何與同一宿主一起工作。

 

 

總結

MEF與MAF(Managed Addin Framework)最大不同在於:前者關注使用非常簡單的方式來支持具有很強靈活性的可擴展支持,后者關注具有物理隔離、安全、多版本支持的插件平台架構。MAF是這兩個框架中較為可靠的框架。該框架允許從應用程序中分離出插件,從而它們只依賴於您定義的接口。如果希望處理不同的版本,MAF提供了很受歡迎的靈活性——例如,如果需要修改接口,但是為了向后兼容需要繼續支持舊插件。MAF還允許應用程序將插件加載到一個獨立的應用程序域中,從而插件的崩潰是無害的,不會影響主應用程序。所有這些特性意味着如果有一個開發團隊開發一個應用程序,並且另一個(或幾個)團隊開發插件,MAF可以工作得很好。MAF還特別適合於支持第三方插件。

但是為了得到MAF功能需要付出代價。MAF是一個復雜的框架,並且即使是對於簡單的應用程序,設置插件管道也很繁瑣。這正是MEF的出發點。MEF是一個輕量級的選擇,其目的是使得實現可擴展性就像是將相關的程序集復制到同一個文件夾中那樣容易。但是MEF相對於MAF有一個不同的基本原則。MAF是一個嚴格的、接口驅動的模型,而MEF是一個自由使用系統,允許根據部件集合構建應用程序。每個部件導出功能,並且所有部件都可以導入其他任何部件的功能。該系統為開發人員提供了更大的靈活性,並且對於設計可組合的應用程序(composable applications)(由單個開發團隊開發但是需要以不同方式組裝的模塊化程序,為單獨的發布提供不同的功能實現)工作得特別好。

示例代碼

SimpleCalculator 是使用MEF的簡單示例,我們可以通過指定的path獲得ExtendedOperations中的插件。

Calc1Contract和Calc2Contract則是使用MAF實現Calculator的兩個不同版本。體現MAF的向后兼容性。點我下載


參考資料

插件開發預覽:http://msdn.microsoft.com/zh-cn/library/bb384200.aspx#addin_model

http://www.cnblogs.com/lc329857895/archive/2009/07/22/1528640.html 博客園相關文章

msdn blog 官方 http://blogs.msdn.com/b/clraddins/

http://tech.ddvip.com/2008-10/122499543784074.html

管線開發:http://msdn.microsoft.com/zh-cn/library/bb384201.aspx 

MEF開發指南:http://www.cnblogs.com/beniao/archive/2010/08/11/1797537.html

http://blog.endjin.com/2010/10/component-discovery-and-composition-part-1b-fundamentals-mef/ discovery

MAF與MEF之間選擇

http://www.cnblogs.com/niceWk/archive/2010/07/23/1783394.html

https://www.zxproxy.com/browse.php?u=6f00ccbf82bOi8vZW1jcGFkZGVuLndvcmRwcmVzcy5jb20vMjAwOC8xMi8wNy9tYW5hZ2VkLWV4dGVuc2liaWxpdHktZnJhbWV3b3JrLWFuZC1vdGhlcnMv&b=6

PS:
我看了一些前輩的博客,主要都是應用在WPF,sliverlight中的。所以寫了這篇文章。
在完成這次調研之后,team成員提出問題:MEF和我們自己實現反射有什么不同。初步覺得使用反射是可以實現的。但是如果自己寫框架的話會有額外的成本,也不會是穩定版。
我會在此后做一些調查。提供一些對比。對於MEF的使用我也在摸索。希望可以和大家一起學習,探討。
 
        







 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM