【.Net平台下插件开发】-MEF与MAF初步调研
背景
Team希望开发一个插件的平台去让某搜索引擎变得更好。主要用于采集一些不满意信息(DSAT)给Dev。这些信息会由不同的team提供不同的tool分析。有的提供仅仅是一个website,有的提供了api。有的提供了service。所以我们设想做一个插件的平台。让那些team提供一些dll。我们只需要把这些dll放在我们的platform里。
由于对插件开发一无所知。所以重头开始做调研。
为什么需要插件框架-扩展性问题
假设您的应用程序必须包含大量可能需要的较小组件,并负责创建和运行这些组件。解决这一问题的最简单的方法是:将这些组件作为源代码包括在您的应用程序中,然后通过代码直接调用它们。 这种做法存在很多明显的缺陷。 最重要的是,您无法在不修改源代码的情况下添加新组件,这一限制在 Web 应用程序(举例来说)中也许能够接受,但在客户端应用程序中行不通。 同样存在问题的还有,您可能没有对组件的源代码的访问权,因为这些组件可能是由第三方开发的,而出于相同的原因,您也不允许第三方访问您的代码。
一种稍微复杂的方法是:提供扩展点或接口,以允许应用程序与其组件相分离。 依据此模型,您可能会提供一个组件能够实现的接口,并提供一个 API 以使该接口能够与您的应用程序进行交互。 这一方法可解决需要源代码访问权的问题,但仍具有自己的难点。
由于应用程序缺乏自己发现组件的能力,因此仍必须明确告知应用程序哪些组件可用并应加载。 这通常是通过在一个配置文件中显式注册可用组件来实现的。 这意味着,确保组件正确无误成为了一个日常维护问题,尤其是在执行更新操作的是最终用户而非开发人员的情况下。
此外,各组件之间无法进行通信,除非是通过应用程序自身的严格定义的通道。 如果应用程序架构师未预计到需要某项通信,则通常是无法进行相应的通信的。
最后,组件开发人员不得不硬依赖于包含他们实现的接口的程序集。 这样就很难在多个应用程序中使用同一个组件,另外,在为组件创建测试框架时也会造成问题。
如何解决?
我们可以使用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的使用我也在摸索。希望可以和大家一起学习,探讨。