在讨论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文件来进行描述,比如:
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"。
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插件的一个片断):
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的体系结构是
最后来看一下代码:
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.
7. private final PluginManager manager_;
8.
9. private final IPluginDescriptor descriptor_;
10.
11. public Plugin(PluginManager manager, IPluginDescriptor descr) {
12. manager_ = manager;
13. descriptor_ = descr;
14. }
15.
16. /**
17. * @return descriptor of this plug-in
18. */
19. public final IPluginDescriptor getDescriptor() {
20. return descriptor_;
21. }
22.
23. /**
24. * @return manager which controls this plug-in
25. */
26. public final PluginManager getManager() {
27. return manager_;
28. }
29.
30. final void start() throws PluginException {
31. if (!started_) {
32. doStart();
33. started_ = true;
34. }
35. }
36.
37. final void stop() throws PluginException {
38. if (started_) {
39. doStop();
40. started_ = false;
41. }
42. }
43.
44. public final boolean isActive() {
45. return started_;
46. }
47.
48. /**
49. * Get the resource string
50. * @param key
51. * @return
52. */
53. public String getResourceString(String key) {
54. IPluginDescriptor desc = getDescriptor();
55. return desc.getResourceString(key);
56. }
57.
58. /**
59. * Get the Plugin Path
60. *
61. * @return
62. */
63. public String getPluginPath() {
64. return getDescriptor().getPluginHome();
65. }
66.
67. /**
68. * Template method, which will do the really start work
69. *
70. * @throws Exception
71. */
72. protected abstract void doStart() throws PluginException;
73.
74. /**
75. * Template method, which will do the really stop work
76. *
77. * @throws Exception
78. */
79. protected abstract void doStop() throws PluginException;
80. }
可见,这只是一个抽象类,每个插件需要定义自己的派生自"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.
4. IPluginDescriptor descr = pluginRegistry_.getPluginDescriptor(id);
5. if (descr == null) {
6. throw new PluginException("Cannot found this plugin " + id);
7. }
8.
9. result = activatePlugin(descr);
10.
11. return result;
12. }
13.
14. private synchronized Plugin activatePlugin(IPluginDescriptor descr)
15. throws PluginException {
16. ... ...
17.
18. try {
19. try {
20. // 首先需要检查这个插件所依赖的插件是否都已经启动,
21. // 如果没有,则需要先启动那些插件,才能启动本插件
22. checkPrerequisites(descr);
23. } catch (PluginException e) {
24. badPlugins_.add(descr.getId());
25. throw e;
26. }
27.
28. // 得到插件的主类名
29. // 这个信息也是定义在"Plugin.xml"中,
30. // 并且在加载插件信息的时候读入到PluginDescriptor中的
31.
32. String className = descr.getPluginClassName();
33. if ((className == null) || "".equals(className.trim())) {
34. result = null;
35. } else {
36. Class pluginClass;
37. try {
38.
39. // 用每个插件自己的PluginClassLoader来得到这个插件的主类
40.
41. pluginClass = descr.getPluginClassLoader().loadClass(
42. className);
43. } catch (ClassNotFoundException cnfe) {
44. badPlugins_.add(descr.getId());
45. throw new PluginException("can't find plug-in class "
46. + className);
47. }
48. try {
49. Class pluginManagerClass = getClass();
50. Class pluginDescriptorClass = IPluginDescriptor.class;
51.
52. Constructor constructor = pluginClass
53. .getConstructor(new Class[] { pluginManagerClass,
54. pluginDescriptorClass });
55.
56. // 调用插件默认的构造函数
57. // Plugin(PluginManager, IPluginDescriptor);
58.
59. result = (Plugin) constructor.newInstance(new Object[] {
60. this, descr });
61. } catch (InvocationTargetException ite) {
62. ... ...
63. } catch (Exception e) {
64. ... ...
65. }
66.
67. try {
68. result.start();
69. } catch (Exception e) {
70. ... ...
71. }
72.
73. ... ...
74. }
75. }
76.
77. return result;
78. }
其实最核心的工作就是三步:
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 plugin-id="com.xerdoc.desktop.core.ui.swt" plugin-version="0.2.0" match="compatible"/>
4. </requires>
这是Office Excel Parser插件清单的片断。如果这个插件的类加载器无法加载某个需要的类或者资源,将会委托"com.xerdoc.desktop.core"插件或者"com.xerdoc.desktop.core.ui.swt"插件的类加载器去加载。
系统Native Library(比如SWT插件中要用到的系统本地库)的加载也是PluginClassLoader的功能。
就举SWT的例子,熟悉SWT的人都知道,运行SWT应用程序的时候需要添加以下命令行参数:
-Djava.library.path="/home/elan/workspace/xerdoc_ds/swt-native"
这就是为了让类加载器能够在相应的目录("/home/elan/workspace/xerdoc_ds/swt-native")下面找到需要的系统本地库资源。但是这样的命令行参数对于某些应用并不合适。对于Xerdoc DS来说,SWT的UI界面也同样是一个插件,同时也还会有其它用到本地资源库的插件,总不能增加一个插件还要修改命令行参数吧?因此,需要修改ClassLoader,使之能够加载指定的Native Library。方法就是重写findLibrary函数:
1. /**
2. * Returns the absolute path name of a native library. The VM invokes this
3. * method to locate the native libraries that belong to classes loaded with
4. * this class loader. If this method returns <tt>null</tt>, the VM
5. * searches the library along the path specified as the
6. * "<tt>java.library.path</tt>" property. </p>
7. *
8. * @param libname
9. * The library name
10. *
11. * @return The absolute path of the native library
12. *
13. * @see System#loadLibrary(String)
14. * @see System#mapLibraryName(String)
15. *
16. * @since 1.2
17. */
18. protected String findLibrary(String libname) {
19. return null;
20. }
重写其实很简单,只需要根据每个插件需要加载Native Library的目录来搜索就可以了,比如这是Linux下面,SWT插件清单的片断:
1. <library id="swt-native" path="swt-native/" type="native_library">
2. <export prefix="*"/>
3. </library>
这样,在找Native Library的时候就可以从"$PLUGIN_HOME/swt-native/"这个目录中找到相应的so文件(Linux下的动态链接库)。
最后来说说资源文件(比如说png, ico等等),其实同加载类资源一样,只要在"library"中声明的目录,就都会加入到类加载器的类搜索路径中去,这样,我们都可以直接访问里面的资源。
5) I18N
I18N(Internationalization,见后注)也是插件平台的一个重要组成部分,国际化软件很重要的一个部分就是I18N的支持。这其实也是"An architecture of participation"的一个方面,只要你留出良好的接口,别人会替你进行各种语言的Translation工作。
Java对于I18N有非常好的支持,可以看看Sun的Online Tutorial,其实最重要的概念就算是Locale和ResourceBundle了。
先来看看Locale:
A Locale object represents a specific geographical, political, or cultural region
常见的比如"zh_CN", "en_US"等等 。它是由Language(语言)和Country(国家/地区)两部分组成的,比如"en"表示英语,"US"表示美国,通常,我们可以通过Locale.getDefault()来得到本地默认的Locale。
当你将Locale设置为其它,理论上,系统的语言、习惯等等也应该被相应的切换。
再来看看ResourceBundle:
Resource bundles contain locale-specific objects. When your program needs a locale-specific resource, a String for example, your program can load it from the resource bundle that is appropriate for the current user’s locale. In this way, you can write program code that is largely independent of the user’s locale isolating most, if not all, of the locale-specific information in resource bundles.
ResourceBundle实例化的策略为:
1) 名字
简单的说,首先会根据你设定的Locale和Base Name来取得相应的名字,比如默认的来说,我们的中文系统中,Base Name = "plugin"的情况下,搜索的顺序为:
plugin_zh_CN
plugin_zh
plugin
所以,Xerdoc DS默认都会提供中、英两种Resource文件("plugin_zh_CN.properties", "plugin.properties"),当然,也可以很方便的根据"plugin.properties"翻译成为其它语言(比如法语、德语)。
这样的结果是,如果你设置为中文Locale,那么则读取"plugin_zh_CN.properties",如果设置为英文,则读取"plugin.properties",如果设置为德文等其它找不到的,也默认的使用英文的"plugin.properties"。
2)类 or Properties文件 ?
ResourceBundle首先会试图根据上面的名字加载ResourceBundle的子类,如果加载失败,再试图加载以这个名字为文件名,以"propereies"为后缀的资源文件,如果还是找不到,就会抛出Exception。
根据这样两步,ResourceBundle完成自己的实例化。
这样,可以为每个插件都配一个自己的ResourceBundle,负责自己插件的I18N工作。
实现起来,在PluginClassLoader中,可以将每个插件的"i18n"目录都默认的加入到搜索路径中去,这样,可以通过下面这段代码得到每个插件自己的ResourceBundle:
1. ...
2.
3. private static final String RESOURCE_BUNDLE_NAME = "plugin";
4.
5. ...
6.
7. /**
8. * I18N Work
9. */
10.
11. /**
12. * Returns the plugin's resource bundle,
13. */
14. public ResourceBundle getResourceBundle() {
15. try {
16. if (resourceBundle_ == null) {
17. ClassLoader loader = getPluginClassLoader();
18. resourceBundle_ = ResourceBundle.getBundle(
19. RESOURCE_BUNDLE_NAME, Locale.getDefault(), loader);
20. }
21. } catch (MissingResourceException x) {
22. resourceBundle_ = null;
23. }
24. return resourceBundle_;
25. }
得到ResourceBundle后,就可以通过它来得到所需要的字符串什么的了:
1. resourceBundle_.getString("parser.word.title");
有两个注意的是:
1. 编译的时候,编码应该指定为utf-8;
2. 程序中所有的字符串应该都由ResourceBundle得到,不应该出现硬编码。
写到这里,想起原来用Visual C++开发项目。I18N的道理其实差不多,不过是将字符串都写到RC文件中(二进制)。相比起来,VC队I18N的支持比Java还是要差不少。
最后补充一点的是,Eclipse插件清单("plugin.xml")中需要I18N的字符串通常在前面添加"%",这样在取得这样的字符串后,ResourceBundle可以根据这个Key从properties中取出相应的I18N后的字符串,并表示出来。
注:I18N作为Internationalization的简称,表示中间省略18个字母,常用的还有G10N(Globalization), A11Y(Accessbility)等等。
6) Lazy Loading
在介绍"Extension Point"之前,先来看一个概念:Eclipse中著名的懒加载原则(Lazy Loading Rule)。
懒加载法则:只有在真正需要的时候才加载插件,实现起来最重要的方面就是声明和实现的分离。
插件的外形(比如名字,ID,图标)等等都在插件描述清单"plugin.xml"中声明,而具体功能封装在class文件中。
这种懒加载原则表现在各个方面,比如最基本的插件启动。系统在启动的时候,只加载和启动最必须的一些插件,而其它插件只有在真正用到的时候才被加载和启动,这样可以最大限度的节省系统启动时的资源和时间。而对用户来说,每次启动也确实有很多插件根本不会去用到。
懒加载还表现在扩展点的应用上,待会儿可以看到具体例子。
7) 扩展点的实现
接下来就看看"Extension Point",像前面曾经介绍的那样,"Extension Point"是Eclipse Plugin Frame中最核心的概念。首先来看一个Xerdoc DS中"Extension Point"和"Extension"的声明:
1. <extension-point id="Parser">
2. <parameter-def id="class" type="string"/>
3. <parameter-def id="icon" type="string"/>
4. </extension-point>
这是"core"插件中关于"Parser"的扩展点,你可以定义不同的扩展,来增强Xerdoc DS能够索引文件类型的范围。
1. <extension plugin-id="com.xerdoc.desktop.core" point-id="Parser" id="MP3FileParser">
2. <parameter id="class" value="com.xerdoc.desktop.parser.mp3.MP3FileParser"/>
3. <parameter id="icon" value="image/mime_icon_Music_mp3.gif"/>
4. </extension>
这是"mp3 parser"插件中对此扩展点的一个扩展声明,声明了自己扩展的类和图标。图标完全是为了显示,而其中的"class"则是为了加载真正的功能。
"core"插件会在需要的时候加载所有扩展了这个扩展点的插件:
1. private static void loadSupportedParsers() {
2. ... ...
3.
4. try {
5. descriptor = manager.getPlugin("com.xerdoc.desktop.core")
6. .getDescriptor();
7.
8. // 得到Parser扩展点声明
9. IExtensionPoint extPoint = descriptor.getRegistry()
10. .getExtensionPoint(descriptor.getId(), "Parser");
11.
12. // 根据这个声明得到所有连接到这个扩展点的扩展对象
13. for (Iterator it = extPoint.getConnectedExtensions().iterator(); it
14. .hasNext();) {
15. IExtension ext = (IExtension) it.next();
16.
17. // 根据扩展对象生成Parser代理
18. // 也就是著名的懒加载法则
19. ParserProxy parser = ParserProxy.createParserProxy(ext);
20.
21. parserList_.add(parser);
22. }
23. } catch (PluginException e) {
24. e.printStackTrace();
25. }
26. }
ParserProxy其实就是Parser的代理,它只读取Parser的表现部分,比如图标,名称等等,而实例化的操作要等到具体使用的时候才去调用。
1. ...
2.
3. /**
4. * Parser Extension Point
5. */
6. private IExtension extension_;
7.
8. /**
9. * Real Parser Instance, it will not be load until really needed
10. */
11. private AbstractParser realParser_;
12.
13. ...
14.
15. private ParserProxy(IExtension extension) {
16. extension_ = extension;
17. }
18.
19. /**
20. * Create the Parser Proxy based on the Extension
21. *
22. * @param extension
23. * @return
24. */
25. public static ParserProxy createParserProxy(IExtension extension) {
26. return new ParserProxy(extension);
27. }
当真正需要这个Parser的时候,ParserProxy会生成相应的真实对象(如其名,这是Proxy模式的典型应用):
1. private AbstractParser getRealInstance() {
2. if (realParser_ == null) {
3. try {
4. // 得到插件
5. // 如果插件还未被激活,这里要激活这个插件
6. // LAZY LOADING!!!
7.
8. Plugin plugin = CorePlugin.getInstance().getManager()
9. .getPlugin(
10. extension_.getDeclaringPluginDescriptor()
11. .getId());
12.
13. if (plugin != null) {
14. Class pluginCls = plugin.getClass();
15.
16. // 得到主类
17. Class cls = extension_.getDeclaringPluginDescriptor()
18. .getPluginClassLoader().loadClass(
19. extension_.getParameter("class")
20. .valueAsString());
21.
22. if (cls != null) {
23. if (pluginCls.isAssignableFrom(cls)) {
24. realParser_ = (AbstractParser) plugin;
25. } else {
26. // 反射生成这个类
27. realParser_ = (AbstractParser) cls.newInstance();
28. }
29. }
30. }
31. } catch (Exception e) {
32. return null;
33. }
34. }
35.
36. return realParser_;
37. }
然后,就可以调用这个Parser完成必要的工作了。这就是"Extension Point"的大概的工作流程。
在Eclipse中,遍地都是这样的例子,比如:Eclipse Platform的菜单显示就是一个扩展点,Eclipse在显示菜单之前首先会从系统的插件列表(PluginRegistry)中寻找所有扩展此扩展点的插件,取得图标和名字显示出来,然后在用户点击的时候生成真实的对象,并调用之,嗯,还是懒加载法则。
BTW:菜单扩展中的类是IAction,点击的时候调用它的run()函数,Command模式。
写这些的时候想起来,在声明每个扩展点前,这个插件都需要定义一定的Interface,也就是扩展这个扩展点的插件需要遵循的API。这个Interface如果用C#中的Delegate实现,是不是会看起来更好呢?(从包的import等等)。不知道有没有C#模仿Eclipse Plugin Framework的实例。
8) 写在后面
对于一个良好的插件平台来说,仅有一个良好的插件架构是不够的,还需要有非常方便易用的插件开发环境。Eclipse的PDE就是这样的产品,它能够很大程度帮助程序员开发插件,极大降低其它人“participation”的难度 。
相比之下,Netbeans就没有这样的插件开发环境(Netbeans中,插件都叫做模块 - "Module"),因此,开发插件还是一件很麻烦的事情,这也就造成了其他人“participation”的困难。
基本上就是这样,Eclipse2.1的插件结构真的非常优秀,看到那些代码的时候更佩服最初设计者的想法。Eclipse2.1 Plugin Framework也有一些缺点,比如不能Load/Unload on the fly(动态加载/卸载),需要重启Eclipse等等。
在Eclipse3.0之后,Eclipse决定遵循OSGi的标准来重构其插件机制,拥抱标准,总是一件美好的事情。
最后强烈推荐 "Contributing to Eclipse",由Erich Gamma、Kent Beck执笔,值得一读。
More entries about : Eclipse, Plugin Framework
原文地址: http://www.mengyan.org/blog/tag/Framework