Dissect Eclipse Plugin Framework

来源:http://phoenix-bupt.javaeye.com/blog/47161 2007-01-15

Dissect Eclipse Plugin Framework

在讨论Xerdoc DSearch的架构的时候,我们就讨论决定采用Eclipse Plugin Framework,可惜那时Eclipse Plugin Framework和SWT以及其它耦合比较大,因此,决定借鉴Eclipse Plugin Framework的思想,来实现一个自己的轻量级的Plugin Framework。

一晃已经过去快一年了,其实非常早就想把自己研究Eclipse Plugin Framework的心得写下来,米嘉也一再催促,不过一直比较懒,觉着这个题目实在要写的太多,于是一直拖着。后来想想,真的应该早点儿把自己的一些粗糙想法写出来,即是对自己的一个总结,也能对其他人有些帮助。

Eclipse Plugin Framework是一套非常成功的插件框架结构,它的架构师之一就是鼎鼎大名的Erich Gamma,设计模式的作者之一。Eclipse JDT就是架构在这个插件平台上的一个杰出的Java IDE。Eclipse 良好的插件架构也形成了很好的"An architecture of participation",你可以在Eclipse的社区中找到各种各样的插件,这些插件又极大的扩充了Eclipse的功能,提高了易用性。

记着候捷在写《深入浅出MFC》的时候,用很简单甚至粗糙的一些例子来模仿MFC内部的行为(比如消息循环等),效果非常好。我也想用一些Xerdoc DSearch中的代码来模仿一下Eclipse的插件架构。

注:这里所指的Eclipse Plugin Framework的Codebase是2.1.3,因为当时研究的时候,3.0(OSGi Based)还没出来 :P

1) 插件清单

Eclipse中的插件都用XML文件来进行描述,比如:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <plugin id="org.eclipse.pde.source" name="%pluginName" version="2.1.3" provider-name="%providerName"> 
  3.     <runtime></runtime>
  4.     <extension point="org.eclipse.pde.core.source">
  5.         <location path="src"> </location>
  6.     </extension> 
  7. </plugin>

这个清单中描述了插件的绝大多数信息,包括插件的id, name(这个是经过i18n的),版本,启动类等。同时,所有的扩展、扩展点也都在这里定义,此插件对外提供的库(包括Native库)以及资源也都要定义在这个文件中。

这个文件的名称是"plugin.xml",Eclipse启动的时候会扫描"plugins"目录下的所有"plugin.xml"文件,进而装载所有的插件。(注:为了提高效率,Eclipse会保存一个中间文件来加速装载,这里并不讨论。)

因此,你需要用XML Parser将这些信息Parse出来,形成插件的基本信息,具体选用Dom、SAX还是Pull Parser都无所谓。

Eclipse采用微内核+插件的模式构架,也就是说,除了一个微小的核儿之外,所有的东西都是插件(All are plugins)。

2) 扩展点概述

Eclipse Plugin Framework最核心的概念应该就要算"Extension Point"(扩展点)了。

打个通俗的比方,"Extension Point"就像我们日常生活中的插销板,而"Extension"就是能够插入到这个插销板上面的插销。

系统的开放性很大程度上也取决于系统究竟给你多少"Extension Point"。

WordPressPlugin Framework也同样采用这种"Extension Point"的概念构架,它为自己几乎所有的应用都定义了扩展点。比如,有的插件可以在"Header显示扩展点"的地方加入代码来添加CSS样式表,Google Sitemap插件可以在"文章发布扩展点"的地方进行Google Sitemap的提交,Creative Commons插件可以在"Footer显示扩展点"处增加"Creative Common"信息等等。

对于Eclipse来说,因为采用微内核+插件的方式,因此,定义扩展点也就成了你的任务,在扩展功能的同时,你也可以在任何你觉得可能被扩展的地方定义扩展点,来方便其他人扩展系统的功能。

举个例子,Eclipse SWT UI中,工具栏、视图都留有扩展点,这样可以方便的进行扩展。

Eclipse的插件扩展点都定义在"plugin.xml"文件中,每个插件要扩展哪些扩展点也定义在这个文件中。举个例子(DS中Core插件的一个片断):

  1. <extension-point id="Parser"> 
  2.     <parameter-def id="class" type="string"/> 
  3.     <parameter-def id="icon" type="string"/>
  4. </extension-point>

这并不是Eclipse Plugin的DTD所规范的"plugin.xml"格式,而是一个非常简单的模拟。它描述的是一个"Parser"的扩展点。因此,你可以扩展任何自 己的Parser(比如QQ聊天记录的Parser,Foxmail Mail的Parser,等等),增加Desktop Search可处理文件的范围。

3) ClassLoader

了解Eclipse的Plugin Framework需要对ClassLoader(类装载器)有比较深入的了解,建议读读JDK的源代码,会很有帮助。

ClassLoader - 顾名思义,就是Java中用来装载类的部分,要将一个类的名字装载为JVM中实际的二进制类数据。在JVM中,任何一个类被加载,都是通过 ClassLoader来实现的,同时,每个Class对象也都有一个引用指向装载他的ClassLoader,你可以通过 getClassLoader()方法得到它。

ClassLoader只是一个抽象类,你可以定义自己的ClassLoader来实现特定的Load的功能。Eclipse Plugin Framework就实现了自己的ClassLoader。

ClassLoader使用所谓的"Delegation Model"(“双亲委托模型”)来查找、定位类资源。每一个ClassLoader都有自己一个父ClassLoader实例(在构造的时候传入),当 这个ClassLoader被要求加载一个类时,它首先会询问自己的父ClassLoader,看看他是否能加载(注意:这个过程是一直递归向上的),如 果不能的话,才自己加载。

Java ClassLoader的体系结构是

ClassLoader Architect

最后来看一下代码:

  1. protected synchronized Class<?> loadClass(String name, boolean resolve)
  2.     throws ClassNotFoundException
  3.     {
  4.     // First, check if the class has already been loaded
  5.     Class c = findLoadedClass(name);
  6.     if (c == null) {
  7.         try {
  8.         if (parent != null) {
  9.             c = parent.loadClass(name, false);
  10.         } else {
  11.             c = findBootstrapClass0(name);
  12.         }
  13.         } catch (ClassNotFoundException e) {
  14.             // If still not found, then invoke findClass in order
  15.             // to find the class.
  16.             c = findClass(name);
  17.         }
  18.     }
  19.     if (resolve) {
  20.         resolveClass(c);
  21.     }
  22.     return c;
  23. }

可见,ClassLoader首先会查找该类是否已经被装载,如果没有,就询问自己的父ClassLoader,如果还不能装载,就调用findClass()方法来装载类。所以,一般简单的自定义ClassLoader只需要重写findClass方法就可以了。

如果你的类不是文件,比如说是序列化在数据库中的二进制流或者网络上的Bit流,就需要重写defineClass()方法,来将二进制数据映射到 运行时的数据结构。另外一种需求也可能是你需要对类文件进行某种操作(比如按位取反?),也需要定义自己的defineClass()方法。

还需要注意的是资源的加载和系统Native库的加载,这个可以留在以后再作讨论。

4) Plugin与PluginClassLoader

准备工作做完,就可以来看看具体实现过程。

我们模拟的几个重要的类是:

Plugin: 插件类,描述每个具体插件;

PluginDescriptor: 插件描述符,记录了插件的ID、Name、Version、依赖、扩展点等;

PluginManager: 插件管理器,负责所有插件资源的管理,包括插件的启动、停止、使能(Enable/Disable)等等;

PluginRegistry: 插件注册表,提供了一个由插件ID到Plugin的映射;

我们首先来定义一个简单的Plugin:

  1. public abstract class Plugin {
  2.     /**
  3.      * Plugin State
  4.      */
  5.     private boolean started_;
  6.     private final PluginManager manager_;
  7.     private final IPluginDescriptor descriptor_;
  8.     public Plugin(PluginManager manager, IPluginDescriptor descr) {
  9.         manager_ = manager;
  10.         descriptor_ = descr;
  11.     }
  12.     /**
  13.      * @return descriptor of this plug-in
  14.      */
  15.     public final IPluginDescriptor getDescriptor() {
  16.         return descriptor_;
  17.     }
  18.     /**
  19.      * @return manager which controls this plug-in
  20.      */
  21.     public final PluginManager getManager() {
  22.         return manager_;
  23.     }
  24.     final void start() throws PluginException {
  25.         if (!started_) {
  26.             doStart();
  27.             started_ = true;
  28.         }
  29.     }
  30.     final void stop() throws PluginException {
  31.         if (started_) {
  32.             doStop();
  33.             started_ = false;
  34.         }
  35.     }
  36.     public final boolean isActive() {
  37.         return started_;
  38.     }
  39.     /**
  40.      * Get the resource string
  41.      * @param key
  42.      * @return
  43.      */
  44.     public String getResourceString(String key) {
  45.         IPluginDescriptor desc = getDescriptor();
  46.         return desc.getResourceString(key);
  47.     }
  48.     /**
  49.      * Get the Plugin Path
  50.      *
  51.      * @return
  52.      */
  53.     public String getPluginPath() {
  54.         return getDescriptor().getPluginHome();
  55.     }
  56.     /**
  57.      * Template method, which will do the really start work
  58.      *
  59.      * @throws Exception
  60.      */
  61.     protected abstract void doStart() throws PluginException;
  62.     /**
  63.      * Template method, which will do the really stop work
  64.      *
  65.      * @throws Exception
  66.      */
  67.     protected abstract void doStop() throws PluginException;
  68. }

可见,这只是一个抽象类,每个插件需要定义自己的派生自"Plugin"的子类,作为本插件的一个入口。其中doStart和doStop是两个简单的模板方法,每个插件的初始化和资源释放操作可以定义在这里。

接下来我们看看系统的启动流程:首先将所有的插件清单读入("plugin.xml"),并根据这个文件解析出 PluginDescriptor(包括这个Plugin的所有导出库、依赖插件、扩展点等等),放到PluginRegistry中。这个过程也是整个 插件平台的一个非常重要的部分,需要从插件清单中解析的部分包括:

  1. 每个插件所依赖的的插件列表(在"plugin.xml"中用"require" element标识);
  2. 每个插件要输出的资源和类(在"plugin.xml"中用"library" element标识);
  3. 每个插件所声明的扩展点列表;
  4. 每个插件所声明的扩展列表(扩展其它扩展点的扩展)。

当把所有的插件信息都读入到系统中,就可以根据自己的需要来启动指定的插件了(比如,在Xerdoc DS中,首先,我们会启动Core插件)。

启动一个插件的步骤是:

  1. public Plugin getPlugin(String id) throws PluginException {
  2.     ... ...
  3.     IPluginDescriptor descr = pluginRegistry_.getPluginDescriptor(id);
  4.     if (descr == null) {
  5.         throw new PluginException("Cannot found this plugin " + id);
  6.     }
  7.     result = activatePlugin(descr);
  8.     return result;
  9. }
  10. private synchronized Plugin activatePlugin(IPluginDescriptor descr)
  11.         throws PluginException {
  12.     ... ...
  13.    
  14.     try {
  15.         try {
  16.             // 首先需要检查这个插件所依赖的插件是否都已经启动,
  17.             // 如果没有,则需要先启动那些插件,才能启动本插件
  18.             checkPrerequisites(descr);
  19.         } catch (PluginException e) {
  20.             badPlugins_.add(descr.getId());
  21.             throw e;
  22.         }
  23.         //    得到插件的主类名
  24.         //    这个信息也是定义在"Plugin.xml"中,
  25.         //    并且在加载插件信息的时候读入到PluginDescriptor中的
  26.        
  27.         String className = descr.getPluginClassName();
  28.         if ((className == null) || "".equals(className.trim())) {
  29.             result = null;
  30.         } else {
  31.             Class pluginClass;
  32.             try {
  33.            
  34.                 //    用每个插件自己的PluginClassLoader来得到这个插件的主类
  35.                
  36.                 pluginClass = descr.getPluginClassLoader().loadClass(
  37.                         className);
  38.             } catch (ClassNotFoundException cnfe) {
  39.                 badPlugins_.add(descr.getId());
  40.                 throw new PluginException("can't find plug-in class "
  41.                         + className);
  42.             }
  43.             try {
  44.                 Class pluginManagerClass = getClass();
  45.                 Class pluginDescriptorClass = IPluginDescriptor.class;
  46.                 Constructor constructor = pluginClass
  47.                         .getConstructor(new Class[] { pluginManagerClass,
  48.                                 pluginDescriptorClass });
  49.                 //    调用插件默认的构造函数
  50.                 //    Plugin(PluginManager, IPluginDescriptor);
  51.                
  52.                 result = (Plugin) constructor.newInstance(new Object[] {
  53.                         this, descr });
  54.             } catch (InvocationTargetException ite) {
  55.                 ... ...
  56.             } catch (Exception e) {
  57.                 ... ...
  58.             }
  59.             try {
  60.                 result.start();
  61.             } catch (Exception e) {
  62.                 ... ...
  63.             }
  64.             ... ...
  65.         }
  66.     }
  67.     return result;
  68. }

其实最核心的工作就是三步:

  1. 首先检查这个插件所依赖的其它插件是否已经被启动,如果没有,则需要首先将那些插件启动;
  2. 根据类名,用插件类加载器加载这个类(这个类是Plugin类的一个派生类);
  3. 调用Plugin类的默认的构造函数(主要是为了将PluginManager和PluginDescriptor传进去)。

这就用到了前面说过的类加载器(ClassLoader),Eclipse中定义了插件类加载器(PluginClassLoader)。插件类加载器(PluginClassLoader)其实很简单,它派生自URLClassLoader -

This class loader is used to load classes and resources from a search path of URLs referring to both JAR files and directories.

PluginClassLoader会将PluginDescriptor中声明输出的路径(可以是JAR文件,可以是类路径,可以是资源路径)加入到此URLClassLoader类加载器的搜索路径中去。

比如:

  1. <runtime>
  2.     <library id="com.xerdoc.desktop.view.htmlrender" path="XerdocDSHTMLRender.jar" type="code">
  3.         <export prefix="*"/>
  4.     </library>   
  5.     <library id="resources" path="image/" type="resources">
  6.         <export prefix="*"/>                   
  7.     </library>           
  8. </runtime>

PluginClassLoader会将"XerdocDSHTMLRender.jar"和"image/"目录都加入到URLClassLoader的类搜索路径中去,这样,就可以用这个类加载器来加载相应的插件类和资源了。

PluginClassLoader加载插件的策略是:

首先试图从父ClassLoader加载(系统类加载器),如果无法加载则会试图从本类加载器加载,如果还是找不到,这时的行为与一般的 URLClassLoader不同,也PluginClassLoader最大的特色:它会试图从此插件的需求依赖插件("require"形容的插件) 中去加载需求的类或者资源。

比如下面这个例子:

  1. <requires>
  2.     <import plugin-id="com.xerdoc.desktop.core" plugin-version="0.4.0" match="compatible"/>
  3.     <import
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章