最简单的OSGi.NET插件框架

一个插件平台除了需要考虑插件的结构、插件位置、插件类型空间、插件依赖、插件通讯、插件多版本支持、插件国际化等插件所需的基本要素之外,还需要考虑一个开发人员如何开发、调试和部署插件。本文简要描述了插件类型空间相关的知识——CLR Loader、CLR Loader VS Java ClassLoader和插件的类型空间及类加载机制实现。
1 CLR Loader
关于CLR加载器的详细描述可以查看《Essential .NET, Volume 1: The Common Language Runtime》,也可以下载我翻译的关于CLR部分的文档“CLR加载器 /Files/baihmpgy/CRL加载器.rar”。在这里我将描述一下书中没有说明的关于加载器的一些信息,这些信息包括:(1)何时触发类加载;(2)LoadFile的妙用及缺陷;(3)AppDomain.AssemblyResolve和AssemblyLoad事件。
在我目前认知情况下,我知道类加载发生在:A)CLR执行线程进入一个方法之前会触发JIT对方法进行编译并加载这个方法所需要的类型;B)调用Type.GetType等系统方法也会触发类型加载。为了让读者更好了解到类加载时机,我编写了一个Sample。该Sample的目录结构如下:
 ClassLoaderTesting.exe
 ClassLibrary1.dll
 ClassLibrary2.dll
 lib<directory>
 ---CL3<directory>
 --------ClassLibrary.dll
你可以点击此处 /Files/baihmpgy/ClassLoaderTesting.rar下载Sample源码,在Program.cs的Main方法你可以看到类加载实验,AssemblyLoad和AssemblyResolve事件发生的时机。
LoadFile和LoadFrome的区别在于LoadFile每次加载一个程序集的时候,不管目前AppDomain是否已经加载了该程序集,它都会加载一次。此外,LoadFile不会加载目标程序集引用的程序集。它为我们提供了加载多版本程序集支持且可以做到按需加载一个程序集依赖的所有程序集。当然,目前而言,我所知的LoadFile的一个最大缺点,就是每次加载一个程序集后,它会Lock住程序集文件。这个缺点在支持插件调试时非常突出。
AssemblyResolve事件在CLR类加载器无法加载到所需类型时触发,它赋予了我们自定义类型加载的扩展,相反AssemblyLoad事件则发生在程序集被加载时。你可以通过调试ClassLoaderTesting这个项目来检查类型何时加载以及这两个事件何事触发。
2 CLR Loader Vs Java ClassLoader
这个问题比较古老了,已经有人比较过CLR Loader和Java ClassLoader。不过,要想公平公正的进行比较还真不容易,这就像公正比较Java和C#,J2EE和.NET一样。前一段时间,就有很多人在批Java。在比较CLR Loader和Java ClassLoader之前,先说几句废话。我先声明一下,我是一个C#程序员,也接触过Java。对Java类加载机制、Flex类加载机制有一定的实践。我体会到一些.NET平台相对于Java平台所不具备的东西,而这些差距除了.NET只支持Windows和Linux之外,还有就是.NET缺少的开放规范和缺少灵活的类加载机制。众所周知,Java有一堆的规范,比如JDBC、JMS、JMX、JNDI、JTX等等,当然,也有基于这些规范的各种Providers,基于Java平台我们可以有很多挑选的余地,并且可以从这些高端的开放规范中学习到很多的知识,.NET平台就很少有开放规范了,如果你非得说C#语言规范的话,那也算,:)。那些标榜“C#比Java绝对好”结论相关的文章我一般都懒得回复,因为这没有任何意义,我只想根据实际需要选择更好的,而不是片面的排斥。要知道,现在用Java的程序员可是C#的几倍,而且Java已经被认为是云计算的标准开发语言(插一句,我不幸的是用C#构建了一个SaaS引擎,深受客户的质疑),存在即合理。
言归正传,我将在http://blog.joycode.com/junfeng/archive/2004/04/10/18901.joy这篇文章之上谈一下二者。在Java,类是通过ClassLoader和类型全程来标识的,这意味着一个ClassLoader不能加载重复的类型,但是两个ClassLoader可以加载同一个名称的类型。不过这时候,被加载的这两个类型创建的实例就是隶属于不同类型了。据我说知,Java类加载触发有两种情况:A)JVM执行过程中;B)Class.forName。JVM在执行字节码的过程中,碰到一个未知类型后,会从当前线程获取对应的ClassLoader,然后使用ClassLoader加载类型。ClassLoader加载类型采用一种层级方式,它首先先调用父类加载器进行加载,如果加载不到才使用当前线程的ClassLoader进行加载。我们可以通过重写ClassLoader对JVM类型加载进行扩展。著名的Eclipse的OSGi内核——Equniox就是通过重写ClassLoader自定义了一个插件类型空间的类型加载器。Class.forName则是显式调用当前线程类加载器加载类型的方法了。很多Java组件框架都是通过扩展ClassLoader来实现模块化技术的,比如JBoss和NetBeans。
CLR加载器概念,我们接触的机会很少,相反,我们对Assembly.Load/LoadFrom/LoadFile、Type.GetType倒是经常碰到了,你可以通过《Essential .NET, Volume 1: The Common Language Runtime》该书深入理解CLR加载器。和Java ClassLoader相比,它的缺点主要有:(1)扩展性差;(2)加载的类型卸载的唯一方式是随着AppDomain一起被卸载。扩展性差表现在,实现CLR自定义类型加载只能通过AppDomain.AssemblyResolve来实现(还有一种方法,就是可以通过.config文件来配置类型加载,但这种方法是静态的,我没有把它当成一种方法),Assembly.Load/LoadFrom/LoadFile这些方法并不能实现对CLR加载器进行扩展,它们只是一些补充,原因很简单,在执行“Class3 cls = new Class3();”时,Class3只能是由CLR来加载的。第二个缺点,很明显,一个类型一旦被加载了,它是无法卸载的,因为在C#,我们在不使用低性能和跨进程调用的AppDomain下是无法实现一个支持动态更新的插件平台。
我的结论:Java ClassLoader简单优雅、功能强大、非常灵活,但对多版本不提供支持;CLR Loader非常简单、支持多版本、但不太灵活且扩展性稍差。
3 OSGi.NET BundleLoader
OSGi.NET是OSGi规范移植到.NET的实现。OSGi是一个基于Java的动态模块化系统的规范。它提供了模块化与插件化、面向服务、安全与隔离和模块扩展支持的功能。由于OSGi具有规范的模块化与插件化定义,且已经经过了Eclipse IDE考验,我们便制定了基于C#的OSGi.NET开放服务规范并基于C#开发实现。
在设计OSGi.NET过程中,模块层花费了大量的设计和开发的时间。我从易用性、类型空间独立性、类加载性能等方面考了,设计了一个几乎和OSGi模块类加载器一样优雅的插件类型加载机制。它类似OSGi实现了一个插件的独立类型空间,在OSGi.NET中,一个插件能加载的类型由插件与子插件本地程序集、插件与子插件依赖程序集、插件动态依赖程序集组成,通俗的讲,一个插件只能从本地和依赖的程序集加载所需的类型。这种方式使得多版本程序集支持也变得可能。OSGi.NET插件类型加载由BundleLoader即插件类加载器来实现,其实现原理和OSGi类似。
不过,为了实现每一个插件独立的类型空间,我们需要对CLR加载器进行扩展,其方式如下:
 1  AppDomain.CurrentDomain.AssemblyResolve  +=   new  ResolveEventHandler(DomainAssemblyResolve);
 2 
 3  Assembly DomainAssemblyResolve( object  sender, ResolveEventArgs args)
 4  {
 5       string  resolvingAsmName  =  args.Name;
 6      AssemblyName name  =   new  AssemblyName(resolvingAsmName);  // 获取程序集全名。
 7        Assembly asm  =  SearchAssemblyFromAllBundles(name);  // 从所有Bundle的程序集中查找匹配的程序集并加载。
 8   
 9       return  asm;
10  }

 

到目前为止我对OSGi.NET的插件化支持还算满意,但限于个人对Java ClassLoader、CLR Loader和OSGi等精深知识理解还不够深入,如有错误,欢迎指正。

 

你相信三分钟能实现一个插件平台和一个插件吗?在这里我将演示一下史上最简单的插件框架。这个Demo以控制台为例,实现一个基于控制台的插件框架和一个控制台插件。当然,除了能三分钟开发一个控制台插件平台和插件,你还可以开发WinForm和Web,该框架完全兼容控制台、WinForm和ASP.NET应用系统,开发过程、开发规范完全一致。OK,我们直切主题。

 

1 控制台宿主——控制台插件框架

1) 在Visual Studio 2008中创建一个名为ConsoleShell1的命令行宿主应用程序。

最简单的OSGi.NET插件框架_第1张图片 

2) 此时自动生成ConsoleShell1项目结构如下,它由一个Program.cs主程序和位于bin/plugin下的系统服务插件构成。该项目在Main方法中通过调用BundleRuntime.Start方法启动插件框架,然后加载运行所有插件。

最简单的OSGi.NET插件框架_第2张图片

3) 编译运行解决方案,此时的ConsoleShell只加载了几个系统的服务模块,使用UIShell.OSGi Remote Management Console来远程监控插件平台的结果如下。在控制台上输入list或者l命令,可以查看到目前启动了框架和6个系统服务模块。

 最简单的OSGi.NET插件框架_第3张图片

该远程管理控制台通过ConsoleShell加载的RemotingManagement模块暴露的WebService来管理远程E-MEF框架运行状况。目前,它支持从远程安装、启动、停止、卸载、查询模块等功能。(声明:该控制台工具是基于EgeyeAddin 开源产品的ConsoleService来做,由于时间比较紧直接重用EgeyeAddin中ConsoleService大部分源代码,在此表示感谢。此外,我们还用了NDigester这个开源组件用于解析XML文件。)

 

到此为止,一个控制台插件框架便架构完成了。 

 

2 控制台插件

1) 继续在1中所演示的工程中添加一个ConsolePlugin项目,命名为ConsolePlugin1。注意其位置是指向 Console Shell1\bin\plugins目录。

2) 此时生成的项目内容如下,它由模块激活器Activator.cs文件和模块清单文件Manifest.xml组成。

最简单的OSGi.NET插件框架_第4张图片

3) 修改Activator类的Start()和Stop()方法,参考示例如下:

 

 1  public   class  Activator : IBundleActivator
 2  {
 3    public   void  Start(IBundleContext context)
 4  {
 5  Console.WriteLine( " ConsolePlugin1 is active. " );
 6  }
 7    public   void  Stop(IBundleContext context)
 8  {
 9  Console.WriteLine( " ConsolePlugin1 is stopped. " );
10  }
11  }

 

4) 至此,一个基本的Plugin示例已经建立完毕。

5) 编译整个解决方案,并运行上节建立的控制台应用程序,结果如下:

最简单的OSGi.NET插件框架_第5张图片

6) 运行远程管理控制台,运行list命令,结果如下,此时ConsolePlugin1已经被架加载并处于Active状态。

最简单的OSGi.NET插件框架_第6张图片

7) 在远程管理控制台输入“stop 3”来停止该模块,结果如下:

最简单的OSGi.NET插件框架_第7张图片

此时,ConsolePlugin1进入Resolved状态并在ConsoleShell运行的控制台上输出“ConsolePlugin1 is stopped.”信息。

 

这样一个插件也实现了!以下是这个过程涉及的名词的简单介绍。

 

3 宿主简介

插件框架是在公共语言运行时之上的模块运行时框架,需要由宿主启动和停止。也就是说,宿主是启动和停止模块运行时的程序,通过这种方式宿主就继承了插件框架动态插件化的所有特性。它支持各种类型的宿主,包括控制台、WinForm和ASP.NET等。控制台宿主为所有的插件提供了一个基于控制台的运行环境,而WinForm则相应的提供了一个桌面应用环境和网络应用环境。

4 激活器简介

激活器(Activator)是插件平台启动插件的入口/出口点,即在启动模块时会调用激活器的Start()函数,如果调用成功,模块将进入Active状态;当定制模块时会调用Stop()函数,一旦调用成功,模块将进入Resolved状态。因此插件可在激活器中实现预处理/清理功能,例如在Start()中模块可向服务总线注册服务,其它插件则可在此获取共享的服务;此外还可以在这里执行启动线程等其它操作。

5 Manifest简介

Manifes.xml包含了各个插件或服务所包含的私有定义, 依赖声明, 扩展及扩展点信息等, 是插件向底层框架暴露自己的唯一途径, 平台可以通过Manifest实现对插件和服务基础支持等一系列功能。 

到目前为止,我们的OSGi.NET平台已经越来越完善了,因此我开始着手实现基于OSGi.NET的Desktop Plugin Framework和ASP.NET Web Plugin Framework。Desktop Plugin Framewok比较简单,可以直接将OSGi集成到启动类里面,不过在ASP.NET遇到了一个很有意思的问题。
以下是Web Plugin Framework的结构,它只是暴露出一个Default.aspx页面,这是一个完全空白的页面,根据具体应用由Plugin来填充内容。Global.asax用于启动和停止OSGi框架。bin目录包含了对OSGi.NET类库的引用。
WebPluginFramework
--Default.aspx
--Global.asax
--bin
--------WebPluginFramework.dll
--------UIShell.OSGi.dll
--------UIShell.OSGi.Web.dll
--Plugins
--------TestPlugin(使用VS创建的ASP.NET网站项目)
-----------------bin/lib/share…*.dll
-----------------Default.aspx
-----------------Manifest.xml
--------TestPlugin2(使用VS创建的ASP.NET网站项目)
-----------------bin/lib/share…*.dll
-----------------Default.aspx
-----------------Manifest.xml
我们在设计用户开发和调试插件的场景如下:(1)用户在Plugins目录下创建一个ASP.NET网站工程项目,比如TestPlugin项目;(2)添加Manifest.xml文件,设置插件的名称、插件运行时类库、插件激活器、插件注册的服务;(3)用户可以通过2种模式调试插件:A)将WebPluginFramework部署到IIS,利用附加进程到w3wp.exe进行调试;B)用户打开WebPluginFramework项目,利用VS直接进行调试。
这种模式听起来我觉得还不错,但是当我运行Web应用时,一旦加载TestPlugin/Default.aspx时,我便遇到一个Parser Error错误。这个错误指示无法加载TestPlugin/Default.aspx绑定的类TestPlugin._Default。

 

最简单的OSGi.NET插件框架_第8张图片 

起初我以为是类加载机制出了问题,因为OSGi.NET实现了一套插件类加载机制,确保能够从插件的私有程序集中加载到相应的类,即OSGi.NET能够从TestPlugin/bin等目录下加载到所需的类。然后经过初步调试和判断,发现问题出在ASP.NET的Compilation。ASPX页面是在请求时动态编译成相应的dll,这个动态编译是由BuildManager来实现,它在编译时无法找到TestPlugin/bin/TestPlugin.dll,也就无法找到TestPlugin._Default这个类了。我搜索了一下相关解决方案,发现搞插件平台的国内外同行也遇到过类似问题,但是都没有给出一个好的方案。我还查看了Kooboo的插件方案,看能否借鉴点思路。Kooboo CMS开源系统在解决这个问题的时候,采用了很简单的方式,即将TestPlugin.dll直接拷贝到WebPluinFramework/bin目录底下。然而,我不能这样做,因为这种做法破坏了OSGi.NET优雅的类加载机制,我希望能够在保持这种优雅类加载前提下,实现ASP.NET插件框架。
于是我下载了.NET框架的PDB,直接Debug进去到BuildManager,发现BuildManager编译ASPX页面的原理,该类在BuildManager.CompileWebFile实现ASPX页面动态编译,看完源码后,我想到一种简单的解决方法,就是在编译这个页面之前利用反射将TestPlugin.dll添加到BuildManager.TopLevelReferencedAssemblies私有属性。这种方法最终解决了ASP.NET插件的实现问题。

 

 1  var assemblyFile  =  context.Request.PhysicalApplicationPath  +  
 2    @" \\plugins\\TestPlugin\\bin\\TestPlugin.dll " ;
 3  Assembly assembly  =  Assembly.LoadFile(assemblyFile);
 4  IList < Assembly >  assemblies  =   null ;
 5  PropertyInfo buildManagerProp  =   typeof (BuildManager).GetProperty( " TheBuildManager "
 6      BindingFlags.NonPublic  |  BindingFlags.Static  |  BindingFlags.GetProperty);
 7    if  (buildManagerProp  !=   null )
 8  {
 9      BuildManager buildManager  =  buildManagerProp.GetValue( null null as  BuildManager;
10       if  (buildManager  !=   null )
11      {
12          PropertyInfo toplevelAssembliesProp  =   typeof (BuildManager).GetProperty(
13    " TopLevelReferencedAssemblies " , BindingFlags.NonPublic  |  
14          BindingFlags.Instance  |  BindingFlags.GetProperty);
15           if  (toplevelAssembliesProp  !=   null )
16          {
17              assemblies  =  toplevelAssembliesProp.GetValue(
18  buildManager,  null as  IList < Assembly > ;
19          }
20      }
21  }
22    if  (assemblies  !=   null   &&  assembly  !=   null )
23  {
24      assemblies.Add(assembly);
25  }  

我们团队从2008年5月份开始设计一个App Store,目前已经实现了产品的原型,现在产品已经进入了完善阶段。该产品最开始是基于我在2005年开发的一个Common Form Framework,此后结合了CAB & SCSF设计了Common UI Platform,从2008年5月份开始正式确定了产品的RoadMap,当然中间还是有不少变更了。它类似Google App Engine和Sina App Engine,只不过市场策略和模式是完全不同的。对于构建一个企业级的App Store,我个人认为.NET平台自身的特性在这领域确实不如Java,不过,好在也不是有什么是不可解决的。在产品设计过程中,我时刻关注业内的各种App Engine、App Store和Plugin Framework,包括Equinox、SharpDevelop、Egeye、Mono.Addin、MAF、MEF、SCSF、Google/Sina App Engine、Discuz等。每出现一种类似的产品,我都有一种心惊肉跳的感觉,担心别人抢在我之前设计了更好的同质产品,当然,也好在目前还没有看到同质的产品。

 

今天我先介绍一下该Store的内核OSGi.NET,同时也谈一下我见过比较简陋的插件系统Discuz。

 

OSGi是一个开放服务规范,“开放”意味着基于该平台可以使得很多人来共同使用和协作,而“服务”则是实现协作的一个手段。该规范可以总结为:A 插件化支持规范; B 面向服务支持规范; C 插件扩展规范; D 安全性与隔离性规范; E 系统服务规范。插件化规范完整详细定义了插件的结构、插件依赖、插件类加载、L10N和I18N、宿主插件和片段插件;面向服务支持规范定义了模块间服务协作的支持,这个服务并不是传统意义的企业级Web Service,确切的讲,仅是“接口+实现”,并对实现的引用进行管理;插件扩展规范定义了一个插件如何对另一个插件进行扩展,这种扩展手段非常简单,在这里提出了扩展点概念;安全性与隔离性则要确保被内核加载的插件不会对内核和其它插件产生一些副作用,比如我们决不能允许一个非法的插件来停止另一个插件;系统服务规范则预定义了几个系统服务。OSGi规范是基于Java编写的,此前还没有一个针对.NET平台OSGi规范,原因在于.NET并不支持ava那样优雅的类加载机制,不过还在我们目前都找到了绕过这些固有缺陷的方法并设计了针对.NET的规范。

 

我们团队在设计OSGi.NET时候,把易用性放在首位,通过场景驱动来设计Usecase。从而,基于OSGi.NET开发一个插件和开发一个.NET项目方式基本一样(当然,我们并不认为目前已经做到最好了,易用性肯定还有很大的改进空间)。以下是一个Hello World的插件,开发人员应该可以在5分钟内开发一个Hello World插件的。

 

1 在OSGi.NET的Plugins目录下,创建一个Class Library Project,并创建一个MyActivator类。

 1  using  System;
 2    using  System.Collections.Generic;
 3    using  System.Text;
 4    using  UIShell.OSGi;
 5 
 6    namespace  UIShell.TestBundle
 7  {
 8       public   class  MyActivator : IBundleActivator
 9      {                           

10     public void Start(IBundleContext context)
11        {
12            Console.WriteLine("Hello World! Plugin is started.");
13        }
14     public void Stop(IBundleContext context)
15        {
16            Console.WriteLine("Plugin is stopped.");
17        }
18    }
19}
20

 

2 定义一个Manifest.xml文件

 1  <? xml version="1.0" encoding="utf-8"  ?>
 2    < Bundle  Name ="TestBundle"
 3          SymbolicName ="UIShell.TestBundle"
 4          InitializedState ="Started" >
 5     < Activator  Type ="UIShell.TestBundle.MyActivator"   />
 6     < Runtime >
 7       < Assembly  Path ="bin/Debug/UIShell.TestBundle.dll"  Share ="false"   />
 8     </ Runtime >
 9    </ Bundle >
10   

 

通过Console运行OSGi.NET后,这个插件便会被内核加载启动,然后Print出Hello World。

 

基于OSGi.NET插件开发,一般只需做的事情有:(1)定义插件要实现的功能;(2)判断插件是否需要引用其它插件的功能,如果有,则可以通过A)在Runtime声明一个Dependeny节点;B)在MyActivator.Start方法中使用context.GetService方法获取依赖的服务;(3)通过定义Extension来扩展其它插件功能,通过ExtensionPoint暴露扩展点。

 

对于Discuz的关注是始于我们在设计基于ASP.NET的Store的。在设计这个Store的时候,我想比较一下基于OSGi.NET的ASP.NET插件平台和其它插件平台。Discuz插件系统是我见过设计的最为简单也是最为简陋的插件系统了。我只看了一下反编译的结果,就没有深入研究它的欲望了。以下是通过反编译看到的插件定义。

最简单的OSGi.NET插件框架_第9张图片 

它的插件系统是基于接口和实现类来设计了。这种简陋的设计从严格意义上来讲不是一个插件平台,对开放性的支持也就更弱了,注定不可能有太多的人参与到这个产品插件的设计与扩展了。此外,每次内核的升级都可能会导致原有的插件无法正常使用。不过,这也可能是因为Discuz在产品设计阶段并没有提出开放性目标。

本文基于 Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名 道法自然(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。

你可能感兴趣的:(.net)