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 </