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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章