本文将用尽可能简单的文字来描述插件框架原理。很多人以为插件化很复杂,所以就一直将这 类框架阻挡在门外。实际上,在我们的实践过程中,从框架的使用角度来看,它非常简单,我们团队里面非正规院校毕业的女生也可以来实际使用。如果说插件框架 难的地方,我反倒觉得克服人的天然惰性更加困难。我们不能习惯于墨守成规,日复一日年复一年,按照相同的模式来开发,将自己打造成一部“编码机器”,成为 没有价值的“程序猿/媛”。使用插件框架,没有多少技术难点,不过需要我们提升我们的软件开发思想,改变现有开发方式。
1 插件框架本质
在.NET 平台,一个程序是由“程序集 + 资源”构成的。程序集是由我们开发的一个个的类。这些类可能是通用功能的辅助类、数据访问类、业务逻辑类,也可以是WinForm应用的窗体类或者Web 应用的Web窗体类。以传统模式开发的程序,一般情况下,不管是我们开发的程序集,还是引用的第三方程序集,它们都在应用程序的bin目录下,如下所示。
插件化开发方式与传统方式不同在于,它会把程序集按照一定结构进行组织,比如下面这个程序是基于插件化方式来构建的,不同功能的程序集则组织到Plugins目录下,如下所示。
此时,bin目录则仅仅包含几个很通用的程序集。
在Plugins目录下,则按照功能进行分组,每一个目录为一个插件,它实现一组功能。
下面我们来看其中一个插件——AlarmManagementPlugin插件的目录结构,如下所示,你会发现,插件拥有自己独立的bin目录,在自己的bin目录下,放置着这个功能涉及的程序集。
在传统开发方式中,放在bin目录下的程序集会由.NET类加载器按需去加载,但是当我们要实现插件化方式开发时,需要依赖于插件框架实现从不同的插件目录中加载程序集。因此,插件框架本质上是扩展了.NET类加载器的功能,使其能够从插件目录中加载程序集。
2 进一步看插件框架
插件化开发方式不仅仅从程序集的组织方式上发上了变化,更重要的是,在功能的组织和实现也发生了变化。我们用一个非常典型的分层架构看看二者区别。
下 图是一个分层架构的应用程序,由表示层、业务层、数据层等组成,每一个层次都有相对应的功能组成,在表示层,我们一般是构建了一个主界面,然后由不同的开 发者在主界面上直接放置上菜单及菜单点击事件的响应,同理,其它层次也类似,不同开发者根据需要实现的功能来添加不同的代码。
在 这种模式下,所有程序员开发的不同层次的功能代码一般都在同一个程序集里面,在开发过程中,团队需要不停的进行合并,并执行集成测试。下图是大家都很熟悉 的PetShop的项目结构。这里面BLL程序集(项目)放置的是业务逻辑层的代码,Web程序集(项目)放置的是表示层代码,DALFactory及涉 及的其它DAL程序集则是数据访问层的相关代码。
下面我们来看看一个功能所涉及的程序集。这意味着,所有程序员都需要获取到项目的代码,然后遵循规则,在各个项目中添加自己的代码,再将这些代码合并起来,最后完整编译再交付软件系统。
现在我们来看看使用插件化开发方式的组织方式,如下图所示。
在这种模式下,功能被封装到插件中,一个程序员负责若干个插件,每一个插件由其相应功能涉及的界面、业务逻辑和数据访问等代码组成。每一个插件可以独立开发、测试和部署,开发者间不再需要对代码进行合并。下图是一个插件化应用程序的项目。
在这里,每一个插件完成一组功能,它们可以有自己独立的界面、业务逻辑和数据访问实现,插件间具有物理隔离性,开发者可以独立开发自己的功能,独立测试、部署与升级,一旦开发完成后,可以由插件框架在进行组合,不再需要进行代码合并和整体发布。
3 从简单示例看插件化
现在我们看看使用插件化来开发的示例。下图是该软件的集成后的界面,有5个一级菜单,其中“服务器”为GPRS通讯服务器插件定义,由程序猿A开发;“基础数据、能耗分类、安装数据”菜单为基础数据管理插件定义,由程序猿B开发。
下图是“服务器”的子菜单和其中一个页面。
下图是“基础数据”的子菜单和其中一个页面。
该系统使用插件化开发方法如下。
(1)该系统使用了一个通用界面框架插件,你可以从iOpenWorks网站来下载到该插件(http://www.iopenworks.com/Products/ProductDetails/Introduction?proID=386),这个界面框架插件提供了一个空白的界面,这个界面支持三级菜单,允许我们通过以下配置将自己定义的菜单及菜单对应的窗体注册到主界面。如下XML定义是由GPRS服务器插件来定义的。它配置了一个一级菜单和两个二级菜单,以及对应的两个用户控件。
< Extension Point = "UIShell.WpfShellPlugin.LinkGroups" > < LinkGroup DisplayName = "服务器" DefaultContentSource = "UIShell.EcmCommServerPlugin.CommServerUserControl" > < Link DisplayName = "通讯服务器" Source = "UIShell.EcmCommServerPlugin.IntegratedCommServerUserControl" /> < Link DisplayName = "测试服务器" Source = "UIShell.EcmCommServerPlugin.CommServerUserControl" /> </ LinkGroup > </ Extension > |
(2)程序猿A创建一个自己的项目来开发通讯服务器,独立的实现具体的应用和业务逻辑,其项目结构如下所示。在这里,他分成两个项目来实现GPRS 通讯服务器,一个是界面表示层UIShell.EcmCommServerPlugin项目,另一个是业务逻辑层 UIShell.EcmCommServerService项目。在开发过程中与程序猿B互相独立,他们可以独立开发、测试、部署和维护。
运行这个项目后,程序猿A得到以下的结果。
(3)同理,程序猿B也创建一个自己的项目来开发基础数据管理,独立的实现具体的应用和业务逻辑,其项目结构如下所示。在这里,他分成两个项目来实 现,一个是界面表示层UIShell.EcmConfigurationPlugin项目,另一个是业务逻辑层 UIShell.EcmDomainService项目,该项目可以被A来重用。在开发过程中与程序猿A互相独立,他们可以独立开发、测试、部署和维护。
运行这个项目后,程序猿B得到以下的结果。
(4)程序猿A和B开发到一个阶段的时候,就可以来随时发布自己的插件,点击项目右键,直接将插件发布到插件仓库。
以下是发布的结果,该项目的插件仓库由3个项目组成。
通讯服务器项目的插件列表如下。
配置管理项目的插件列表如下所示。
(5)测试人员/部署人员可以通过插件管理来获取到这两个程序猿开发的插件,然后将这些插件下载组装起来。
下载安装后,就是如下的效果了。
4 插件化有什么好处
从以上简单的例子,我们看到,插件化最直接的好处就是可以以模块化的方式来独立并行构建软件系统,在构建的过程中可以随时进行集成。下面我把使用插件化的优点总结一下:
(1) 插件化优化了团队协作,避免团队开发过程中互相交叉,不再需要更改各自的代码将开发的成果集成到一起;
(2) 使用插件化开发后,每一个人的工作都非常的独立,可以有独立的架构、独立开发、独立测试、独立部署、独立升级,并行构建;
(3) 插件化使得重用度更高,在我们开发的一个项目中,超过50%的模块都直接重用了原有的结果,不需要更改任何的代码;
(4) 插件化使开发和维护更加简单,这也是得益于每一个开发人员可以单独开发自己的应用,每个人都很专注,并且只需要关注系统中很小的一部分,在以上示例中,每一个开发人员能看到的代码都是自己来开发的;
(5) 使用插件化开发,我们的发布、升级也很简单,右键发布到插件仓库,部署测试人员通过插件管理界面来下载每一个人的成果,组装成软件,并且可以随时升级,这避免了很多手工发布、集成;
(6) 使用插件化,可以很容易的构建自己的知识库,是完全可复用,并持续不断改进和增长。
5 插件框架原理
从上述小节,我们看到,插件化开发更多的是软件方法和思想上的改变。为了满足这种开发方法的团队协作模型,插件框架需要来解决一下几个问题:
5.1 如何实现模块化
实现模块化,意味着我们需要把功能相关的类组织到若干独立的程序集,这些程序集是在插件目录下。因此,实现模块化就必须解决以下几个问题:
(1)正确的类加载:即插件框架必须对CLR类加载进行扩展,使其能够从插件目录中正确加载到插件程序集;
(2)插件描述:必须引入一个插件描述方法,它来告知当前插件的基本信息、版本标识、以及这个插件所包含的程序集;
(3)解决插件的依赖关系:在一个实际应用系统中,必然存在相互依赖。在这里,依赖是指一个插件使用了另一个插件定义的类型或者创建的对象。也就是 说,我们可以直接使用另一个插件定义的类型,在更加麻烦的场景中,还存在循环依赖。插件框架够必须很好的处理好插件依赖关系的解析与管理。只有当所有的依 赖关系都满足后,一个插件才能够对外暴露功能;
(4)类加载空间定义:插件既有自己的程序集,也可以依赖其它插件的程序集,那这时候就必须对插件的类型空间做一个唯一的定义,保证当前插件能够加载的类型独立性和隔离性;
(5)插件多版本问题:由于一个插件应用系统中的插件是由若干团队开发,每一个团队独立开发自己的插件,很有可能一个插件使用了NLog的1.0版本呢,而另一个团队则更新到最新的1.1版本,这时候,我们必须保证两个团队的插件能够正确运行;
(6)插件状态的定义:插件可以具备不同的状态,并在不同状态下有不同的体现。最常用的有:安装、解析、正在启动、激活、正在停止、卸载等,当插件 处于解析状态表示这个插件所有依赖关系都已经满足,可以被正常启动;当插件处于激活状态则表示这个插件已经被启动,所有功能都已经暴露了;
(7)插件启动初始状态和启动顺序:当插件被框架加载时,插件处于什么状态;用什么样的方式来设置插件的启动顺序;当插件有依赖关系时,这时候对启动顺序又有什么影响;
(8)插件的初始化和终止处理:即相当于插件的入口和出口,当插件被启动或停止需要执行的操作是如何来定义的。
5.2 如何实现模块通讯
插件框架必须解决模块通讯。传统的通讯,我们都是基于CLR类加载来实现的,即我们可以直接引用另一个程序集的类型,比如:ClassA cls = new ClassA();或者ClassA.Singleton.SayHello()。插件框架可以提供两种通讯方式,如下:
(1)传统通讯方式:即我们可以像以前一样,直接通过引用类型来进行通讯,或可以通过动态加载类型使用反射来通讯;
(2)面向服务:基于SOA模型来实现通讯,即服务 = 服务契约(接口) + 实现(类),服务提供商将服务注册到总线,服务消费者使用服务契约从服务总线获取服务,绑定使用。
5.3 如何实现模块扩展
模块扩展指的是在不更改已经发布的插件的任何代码的情况下,来变更该插件的功能。这也是插件重用的支撑之一。在OSGi.NET插件框架,使用了基 于ExtensionPoint-Extension机制,暴露扩展点的插件可以被扩展,扩展的插件通过XML来定义Extension的内容,从而被暴 露扩展点的插件来获取,并变更其功能。
5.4 插件框架高级支持
除了上述的三大基础功能是插件框架需要实现的,它还可以提供以下更高级的功能:
(1)对自动升级的支持:插件框架可以支持插件的更新,当发现有新版本在插件框架内部时,可以应用更新;此外,还可以对插件框架本身实现自动更新;
(2)对动态性的支持:插件框架可以支持动态安装、启动、停止、更新和卸载插件,允许灵活控制整个应用系统;
(3)对远程管理的支持:可以通过远程管理的API,来实现插件内核的浏览和管理,从而实现内核情况的查看和插件的远程管理控制;
(4)对插件仓库的支持:可以将插件发布到插件仓库,而插件框架则可以利用插件插件仓库来实现动态安装和更新,有利于应用系统的知识积累、发布和团队协作;
(5)对DevOps的支持:插件开发者可以一键发布更新,而测试人员和部署的应用,则可以及时下载到更新。
6 OSGi.NET与其它插件框架的比较
一个好的插件框架不能有太多的侵略性,如果有太多侵略性的话,我们需要了解特别多的框架API,意味着我们在开发过程中动不动就得和框架打交道,这 非常不利于应用插件化开发。OSGi.NET框架之所以能够成为一个通用的插件框架产品,主要原因在于它的低侵入性。有很多插件框架,它基本上是和UI、 数据访问等操作严格绑定起来的,要应用这些框架时,你需要了解框架关于插件化、UI、数据访问等操作的API,需要完全掌握这些API并在项目中与这些 API打交道,更麻烦的是,一旦这些API无法满足时,这个框架便难以再使用。OSGi.NET插件框架没有像其它框架这么来干了,它把所有功能都当前插 件,都可以被定制或者替代,这里,我们可以根据需要来定制和安装界面框架插件、数据访问插件等,使用这些插件时,都不再需要与框架API打交道了。
下面我来用几个指标详细的介绍OSGi.NET与其他插件框架比较。
目前在.NET平台,除了OSGi.NET插件框架之外,流行的插件开发平台有SharpDevelop内核、微软的MEF/MAF/SCSF、思科的Mono.Addins。这些框架详细比较如下表所示。
比较项 | OSGi.NET | SharpDevelop | MEF | MAF | Mono.Addins | SCSF |
Web开发 | 支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
线程安全 | 支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
国际规范 | OSGi R4 | 无 | 无 | 无 | 无 | 无 |
多版本 | 支持 | 不支持 | 不支持 | 支持 | 不支持 | 不支持 |
插件仓库 | 支持 | 不支持 | 不支持 | 不支持 | 支持 | 不支持 |
依赖管理 | 支持 | 支持 | 不支持 | 不支持 | 支持 | 不支持 |
面向服务 | 支持 | 弱 | 不支持 | 不支持 | 不支持 | 支持 |
动态性 | 支持 | 很弱 | 不支持 | 弱 | 较弱 | 不支持 |
晚加载 | 支持 | 不支持 | 不支持 | 支持 | 支持 | 不支持 |
扩展性 | 强 | 强但抽象 | 强 | 强但复杂 | 强 | 不支持 |
隔离性 | 强 | 弱 | 很弱 | 强但跨域 | 强 | 很弱 |
可移植性 | 强 | 强 | 差 | 差 | 强 | 差 |
易用性 | 强 | 一般 | 强 | 差 | 强 | 差 |
辅助支持 | 强,模板、打包工具、迁移工具、远程控制、调试 | 无 | 无 | 无 | 很弱,打包工具 | 弱,IDE模板 |
.NET框架 | .NET 2.0+ | .NET 2.0+ | .NET 4.0+ | .NET 1.0+ | .NET 2.0+ | .NET 2.0+ |
在目前所有流行的插件框架中,OSGi.NET框架是唯一支持Web和线程安全的框架,同时OSGi.NET框架能够支持所有.NET应用,它使我 们可以在ASP.NET、ASP.NET MVC、WPF、WinForm、控制台、Windows服务、移动框架中使用统一的开发模式。
从规范化角度来看,只有OSGi.NET插件框架是遵循OSGi R4规范来设计的,其它框架均使用自己的方法来开发。OSGi R4规范是由OSGi联盟在1999年提出的服务平台规范,Eclipse 3.0以后其内核使用OSGi规范来构建,从此OSGi得到快速发展,在IDE、Web容器、云计算等复杂领域中得到成功应用。
对于大规模复杂应用,多版本是必须具备的功能,因为有可能两个团队开发的功能会使用同一个第三方库的不同版本。目前只有OSGi.NET框架和 MAF框架支持多版本,允许在同一个应用系统中加载不同版本的库。MAF框架对多版本的支持是通过使用AppDomain来实现,因此插件间需要跨域调 用;OSGi.NET框架对多版本的支持是通过对CLR底层类加载机制进行扩展,无需跨域调用,具备很高的性能,也是支持Web等其它应用开发的保证。
插件仓库可以用于管理插件的不同版本,并实现插件的升级和动态安装,在这些框架中只有OSGi.NET插件框架和Mono支持插件仓库。不 过,Mono的插件仓库仅实现插件的文件管理,而OSGi.NET的插件仓库则从项目开发、管理、发布、维护的角度出发,构建了基于插件仓库的软件生产线 平台。OSGi.NET插件仓库不仅仅实现插件的各个版本管理,除此之外,它提供了项目管理、共有插件仓库、私有插件仓库、项目成员及权限、插件内核版本 管理等,适合各个团队的统一协作,是一个创新性的软件生产线平台。
插件式应用开发,一个插件会引用另一个插件的类,也就意味着插件框架需要对这种依赖关系提供支持并实现管理。MEF、MAF和SCSF没有提供这类 功能,SharpDevelop、Mono.Addins和OSGi.NET框架提供了完善的依赖管理,其中OSGi.NET对循环依赖提供了更好的支持 并且在插件仓库中也实现依赖管理,因而在下载安装插件时,可以自动下载安装必要的插件。
面向服务开发模型实现了服务提供商和服务消费者的解耦,具有良好的扩展性和动态性。OSGi R4规范的服务层对面向服务提供了支持。在这些框架中,OSGi.NET和Mono.Addins对服务提供了支持,SharpDevelop则很弱,其 它框架不支持该开发模型。面向服务实现了插件间的通讯,这种通讯方式是送耦合的方式。
动态性是指在应用系统运行过程中可以动态的安装、启动、停止、卸载和更新插件。目前只有OSGi.NET框架具备最全面的动态性支持,它提供了插件 各个生命周期状态,并提供了相应的API来支持,而相比较只有SD、MAF和Mono.Addins提供比较弱的动态性,他们对这些操作仅提供部分的支 持。
晚加载是指在必要的时候才加载插件的程序集,这可以更好的提高应用程序的启动速度和节省内存开支。目前支持MAF、Mono.Addins和OSGi.NET框架支持晚加载,其中只有OSGi.NET框架支持插件晚激活,即插件的激活可以推迟到必要的时候。
可扩展性是插件框架非常重要的功能,因为在这里,插件一般是封装好的,对外需要暴露可扩展能力,可以通过扩展来变更功能。在这些框架中,只有 SCSF不支持扩展。OSGi.NET和Mono.Addins均提供了灵活且简单的ExtensionPoint-Extension机制的扩展,而 SharpDevelop和MAF的扩展则非常的抽象且复杂。
隔离性是指插件具备自己独立的目录结构和类型空间。MAF、Mono.Addins和OSGi.NET框架提供了良好的隔离,插件具备独立的结构和类型空间,但是SharpDevelop、MEF和SCSF的隔离性则相对来讲弱很多。
可移植性指的是可以将传统的代码快速移植成插件,在插件框架中使用,目前MEF、MAF和SCSF对移植性支持很差,需要更改很多代码才可以在插件 框架中应用,这不利于我们的使用。在SharpDevelop、Mono.Addins和OSGi.NET框架中,他们虽然具备很强可移植性,不过,只有 OSGi.NET框架提供了迁移工具,可以在5分钟内将传统程序集移植成插件。
易用性是是否应用插件框架的重要指标,OSGi.NET框架具备最强的易用性,拥有完善的编程工具,并且提供了很好且很友好的API,可以在5分钟 内构建出一个插件应用。相比较,MEF的易用性也不差,它的编程模型也很简单。次之,Mono.Addins和SharpDevelop的易用性为一般, 但MAF和SCSF的易用性最差,应用起来非常复杂。
辅助支持是易用性的重要支撑。在辅助支持方面,OSGi.NET框架构建了插件迁移工具、插件发布工具、远程管理控制台、项目模板、调试日志、插件 仓库、示例和文档,提供了非常完善的辅助支撑,相比较,SCSF只提供项目开发模板,Mono.Addins只提供插件打包工具,而其它框架则没有任何辅 助支持。
这些框架对.NET框架版本的支持不完全一致,MAF对.NET框架版本没有要求,OSGi.NET、MAF、SharpDevelop、SCSF、Mono.Addins要求.NET框架版本为2.0以上,而MEF则要求.NET框架版本为4.0以上。
根据上述比较可以发现,OSGi.NET框架目前是最简单、最强大、最通用且最易移植的框架,并构建了符合现代复杂大规模软件的开发方法——基于软件生产线的组装式开发。其独特优势有:
(1)唯一支持Web开发,并同时支持WPF、WinForm、ASP.NET、ASP.NET MVC等任意.NET应用;
(2)唯一支持线程安全,这也是支持Web开发的前提;
(3)唯一支持国际规范;
(4)唯一不使用跨域支持多版本的框架;
(5)唯一具备完善的辅助支持,并构建了完善的软件生产线平台。
对插件框架的介绍,我简单到这,这是一个免费的框架,可以从http://www.iopenworks.com来下载,有问题可以加入到我们的插件交流群:121369588。