背景php
Team但願開發一個插件的平臺去讓某搜索引擎變得更好。主要用於採集一些不滿意信息(DSAT)給Dev。這些信息會由不一樣的team提供不一樣的tool分析。有的提供僅僅是一個website,有的提供了api。有的提供了service。因此咱們設想作一個插件的平臺。讓那些team提供一些dll。咱們只須要把這些dll放在咱們的platform裏。html
因爲對插件開發一無所知。因此重頭開始作調研。web
爲何須要插件框架-擴展性問題編程
假設您的應用程序必須包含大量可能須要的較小組件,並負責建立和運行這些組件。解決這一問題的最簡單的方法是:將這些組件做爲源代碼包括在您的應用程序中,而後經過代碼直接調用它們。 這種作法存在不少明顯的缺陷。 最重要的是,您沒法在不修改源代碼的狀況下添加新組件,這一限制在 Web 應用程序(舉例來講)中也許可以接受,但在客戶端應用程序中行不通。 一樣存在問題的還有,您可能沒有對組件的源代碼的訪問權,由於這些組件多是由第三方開發的,而出於相同的緣由,您也不容許第三方訪問您的代碼。api
一種稍微複雜的方法是:提供擴展點或接口,以容許應用程序與其組件相分離。 依據此模型,您可能會提供一個組件可以實現的接口,並提供一個 API 以使該接口可以與您的應用程序進行交互。 這一方法可解決須要源代碼訪問權的問題,但仍具備本身的難點。安全
因爲應用程序缺少本身發現組件的能力,所以仍必須明確告知應用程序哪些組件可用並應加載。 這一般是經過在一個配置文件中顯式註冊可用組件來實現的。 這意味着,確保組件正確無誤成爲了一個平常維護問題,尤爲是在執行更新操做的是最終用戶而非開發人員的狀況下。架構
此外,各組件之間沒法進行通訊,除非是經過應用程序自身的嚴格定義的通道。 若是應用程序架構師未預計到須要某項通訊,則一般是沒法進行相應的通訊的。app
最後,組件開發人員不得不硬依賴於包含他們實現的接口的程序集。 這樣就很難在多個應用程序中使用同一個組件,另外,在爲組件建立測試框架時也會形成問題。框架
如何解決?模塊化
咱們可使用MS提供的擴展性框架 MEF 或 MAF。
什麼是MEF?
官方說法: Managed Extensibility Framework(MEF)是.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
PS:
我看了一些前輩的博客,主要都是應用在WPF,sliverlight中的。因此寫了這篇文章。
在完成此次調研以後,team成員提出問題:MEF和咱們本身實現反射有什麼不一樣。初步以爲使用反射是能夠實現的。可是若是本身寫框架的話會有額外的成本,也不會是穩定版。
我會在此後作一些調查。提供一些對比。對於MEF的使用我也在摸索。但願能夠和你們一塊兒學習,探討。