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)還沒出來 。
1) 插件清單
Eclipse中的插件都用XML文件來進行描述,比如:
- <?xml version="1.0" encoding="utf-8"?>
- <plugin id="org.eclipse.pde.source" name="%pluginName" version="2.1.3" provider-name="%providerName">
- <runtime></runtime>
- <extension point="org.eclipse.pde.core.source">
- <location path="src"> </location>
- </extension>
- </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"。
WordPress的Plugin Framework也同樣採用這種"Extension Point"的概念構架,它爲自己幾乎所有的應用都定義了擴展點。比如,有的插件可以在"Header顯示擴展點"的地方加入代碼來添加CSS樣式表,Google Sitemap插件可以在"文章發佈擴展點"的地方進行Google Sitemap的提交,Creative Commons插件可以在"Footer顯示擴展點"處增加"Creative Common"信息等等。
對於Eclipse來說,因爲採用微內核+插件的方式,因此,定義擴展點也就成了你的任務,在擴展功能的同時,你也可以在任何你覺得可能被擴展的地方定義擴展點,來方便其他人擴展系統的功能。
舉個例子,Eclipse SWT UI中,工具欄、視圖都留有擴展點,這樣可以方便的進行擴展。
Eclipse的插件擴展點都定義在"plugin.xml"文件中,每個插件要擴展哪些擴展點也定義在這個文件中。舉個例子(DS中Core插件的一個片斷):
- <extension-point id="Parser">
- <parameter-def id="class" type="string"/>
- <parameter-def id="icon" type="string"/>
- </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的體系結構是
最後來看一下代碼:
- protected synchronized Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- // First, check if the class has already been loaded
- Class c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClass0(name);
- }
- } catch (ClassNotFoundException e) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
可見,ClassLoader首先會查找該類是否已經被裝載,如果沒有,就詢問自己的父ClassLoader,如果還不能裝載,就調用findClass()方法來裝載類。所以,一般簡單的自定義ClassLoader只需要重寫findClass方法就可以了。
如果你的類不是文件,比如說是序列化在數據庫中的二進制流或者網絡上的Bit流,就需要重寫defineClass()方法,來將二進制數據映射到 運行時的數據結構。另外一種需求也可能是你需要對類文件進行某種操作(比如按位取反?),也需要定義自己的defineClass()方法。
還需要注意的是資源的加載和系統Native庫的加載,這個可以留在以後再作討論。
4) Plugin與PluginClassLoader
準備工作做完,就可以來看看具體實現過程。
我們模擬的幾個重要的類是:
Plugin: 插件類,描述每個具體插件;
PluginDescriptor: 插件描述符,記錄了插件的ID、Name、Version、依賴、擴展點等;
PluginManager: 插件管理器,負責所有插件資源的管理,包括插件的啓動、停止、使能(Enable/Disable)等等;
PluginRegistry: 插件註冊表,提供了一個由插件ID到Plugin的映射;
我們首先來定義一個簡單的Plugin:
- public abstract class Plugin {
- /**
- * Plugin State
- */
- private boolean started_;
- private final PluginManager manager_;
- private final IPluginDescriptor descriptor_;
- public Plugin(PluginManager manager, IPluginDescriptor descr) {
- manager_ = manager;
- descriptor_ = descr;
- }
- /**
- * @return descriptor of this plug-in
- */
- public final IPluginDescriptor getDescriptor() {
- return descriptor_;
- }
- /**
- * @return manager which controls this plug-in
- */
- public final PluginManager getManager() {
- return manager_;
- }
- final void start() throws PluginException {
- if (!started_) {
- doStart();
- started_ = true;
- }
- }
- final void stop() throws PluginException {
- if (started_) {
- doStop();
- started_ = false;
- }
- }
- public final boolean isActive() {
- return started_;
- }
- /**
- * Get the resource string
- * @param key
- * @return
- */
- public String getResourceString(String key) {
- IPluginDescriptor desc = getDescriptor();
- return desc.getResourceString(key);
- }
- /**
- * Get the Plugin Path
- *
- * @return
- */
- public String getPluginPath() {
- return getDescriptor().getPluginHome();
- }
- /**
- * Template method, which will do the really start work
- *
- * @throws Exception
- */
- protected abstract void doStart() throws PluginException;
- /**
- * Template method, which will do the really stop work
- *
- * @throws Exception
- */
- protected abstract void doStop() throws PluginException;
- }
可見,這只是一個抽象類,每個插件需要定義自己的派生自"Plugin"的子類,作爲本插件的一個入口。其中doStart和doStop是兩個簡單的模板方法,每個插件的初始化和資源釋放操作可以定義在這裏。
接下來我們看看系統的啓動流程:首先將所有的插件清單讀入("plugin.xml"),並根據這個文件解析出 PluginDescriptor(包括這個Plugin的所有導出庫、依賴插件、擴展點等等),放到PluginRegistry中。這個過程也是整個 插件平臺的一個非常重要的部分,需要從插件清單中解析的部分包括:
- 每個插件所依賴的的插件列表(在"plugin.xml"中用"require" element標識);
- 每個插件要輸出的資源和類(在"plugin.xml"中用"library" element標識);
- 每個插件所聲明的擴展點列表;
- 每個插件所聲明的擴展列表(擴展其它擴展點的擴展)。
當把所有的插件信息都讀入到系統中,就可以根據自己的需要來啓動指定的插件了(比如,在Xerdoc DS中,首先,我們會啓動Core插件)。
啓動一個插件的步驟是:
- public Plugin getPlugin(String id) throws PluginException {
- ... ...
- IPluginDescriptor descr = pluginRegistry_.getPluginDescriptor(id);
- if (descr == null) {
- throw new PluginException("Cannot found this plugin " + id);
- }
- result = activatePlugin(descr);
- return result;
- }
- private synchronized Plugin activatePlugin(IPluginDescriptor descr)
- throws PluginException {
- ... ...
- try {
- try {
- // 首先需要檢查這個插件所依賴的插件是否都已經啓動,
- // 如果沒有,則需要先啓動那些插件,才能啓動本插件
- checkPrerequisites(descr);
- } catch (PluginException e) {
- badPlugins_.add(descr.getId());
- throw e;
- }
- // 得到插件的主類名
- // 這個信息也是定義在"Plugin.xml"中,
- // 並且在加載插件信息的時候讀入到PluginDescriptor中的
- String className = descr.getPluginClassName();
- if ((className == null) || "".equals(className.trim())) {
- result = null;
- } else {
- Class pluginClass;
- try {
- // 用每個插件自己的PluginClassLoader來得到這個插件的主類
- pluginClass = descr.getPluginClassLoader().loadClass(
- className);
- } catch (ClassNotFoundException cnfe) {
- badPlugins_.add(descr.getId());
- throw new PluginException("can't find plug-in class "
- + className);
- }
- try {
- Class pluginManagerClass = getClass();
- Class pluginDescriptorClass = IPluginDescriptor.class;
- Constructor constructor = pluginClass
- .getConstructor(new Class[] { pluginManagerClass,
- pluginDescriptorClass });
- // 調用插件默認的構造函數
- // Plugin(PluginManager, IPluginDescriptor);
- result = (Plugin) constructor.newInstance(new Object[] {
- this, descr });
- } catch (InvocationTargetException ite) {
- ... ...
- } catch (Exception e) {
- ... ...
- }
- try {
- result.start();
- } catch (Exception e) {
- ... ...
- }
- ... ...
- }
- }
- return result;
- }
其實最核心的工作就是三步:
- 首先檢查這個插件所依賴的其它插件是否已經被啓動,如果沒有,則需要首先將那些插件啓動;
- 根據類名,用插件類加載器加載這個類(這個類是Plugin類的一個派生類);
- 調用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類加載器的搜索路徑中去。
比如:
- <runtime>
- <library id="com.xerdoc.desktop.view.htmlrender" path="XerdocDSHTMLRender.jar" type="code">
- <export prefix="*"/>
- </library>
- <library id="resources" path="image/" type="resources">
- <export prefix="*"/>
- </library>
- </runtime>
PluginClassLoader會將"XerdocDSHTMLRender.jar"和"image/"目錄都加入到URLClassLoader的類搜索路徑中去,這樣,就可以用這個類加載器來加載相應的插件類和資源了。
PluginClassLoader加載插件的策略是:
首先試圖從父ClassLoader加載(系統類加載器),如果無法加載則會試圖從本類加載器加載,如果還是找不到,這時的行爲與一般的 URLClassLoader不同,也PluginClassLoader最大的特色:它會試圖從此插件的需求依賴插件("require"形容的插件) 中去加載需求的類或者資源。
比如下面這個例子:
- <requires>
- <import plugin-id="com.xerdoc.desktop.core" plugin-version="0.4.0" match="compatible"/>
- <import