简介: 文章首先分析总结了 Eclipse 插件间四种常用的通信方式,然后描述了插件通信中的一些特殊需求,对于某些情况,普通通信方式往往无法满足要求,文章给出了一种通过事件方式实现的 Eclipse 插件通信的方法,并给出了具体的参考实现。
本文的标签:
rcp标记本文!发布日期: 2009 年 11 月 24 日
级别: 初级
访问情况 : 2217 次浏览
评论: 0 (
查看 |
添加评论 - 登录)
平均分 (8个评分)
为本文评分从 Eclipse 3.0 开始,Eclipse 通过选择开放服务网关协议(Open Services Gateway Initiative,OSGi)来替换先前版本中 Eclipse 插件技术,Eclipse 中的插件是通过 OSGi 框架来实现的,可以认为,一个 Eclipse 插件等同于一个 OSGi 的 Bundle。Eclipse 3.0 以后,创建一个 Eclipse 插件,既可以采用基于扩展点的方式,又可以采用基于 OSGi 的 Bundle 概念,相应地,插件间的通信方式也变得多样化。
如不做特殊说明,本文默认的开发和运行环境是:IBM JDK 1.6,Eclipse 3.4.x。
回页首在 Eclipse 插件编程中,我们常用的插件通信方式主要有如下四种:通过包约束条件建立 Eclipse 插件之间的关联关系;使用 Eclipse 扩展点机制实现插件间通信;使用 OSGi 框架实现组件间信息通信和共享;以及使用单例模式实现插件间信息共享。
其中第一、二、四种方式必须显示建立两个插件之间类的关联关系,并且第二种方式需要通过配置文件建立扩展和扩展点之间的关系,第三种方式是 OSGi 框架提供的 Bundle 之间的通信,这种方式不需要通过任何配置文件显示指定两个插件之间的依赖,Bundle 只需要通过注册服务和查找服务便能实现相互间的功能调用和数据传递,因而也是最灵活的一种方式。
从 Eclipse 3.0 开始,Eclipse 插件是通过 OSGi 框架来实现的,OSGi Bundle 的解析是通过分析定义在 MANIFEST.MF 文件中的包约束条件,查找与包约束条件相匹配的 Bundle 并建立关联关系的过程。
MANIFEST.MF 文件中的包约束条件主要是通过 Export-Package,Import-Package, Require-Bundle 和 DynamicImport-Package 四种方式来实现。
Export-Package:定义了 Bundle 中需要被导出的包。一个 Bundle 中的包被导出之后,便可以通过 Import-Package 方式被其他 Bundle 使用。
Import-Package:定义了需要导入的其他 Bundle 的包。所有需导入的包必须都能够找到相应的导出 Bundle,否则解析失败。
Require-Bundle:定义了依赖的 Bundle。Require-Bundle 会对整个 Bundle 产生依赖,而 Import-Package 只会对指定的包产生依赖关系。
DynamicImport-Package:定义需要动态导入的包。定义在运行时动态解析并加载共享包,这种方式很少使用。
当 Bundle 之间的关联关系建立起来之后,插件之间就可以实现类的互相引用。但是,通过建立插件间的关联关系,主要是实现插件之间类的互相关联和依赖,对于插件间的数据通信,可以通过下面三种方式实现。
扩展和扩展点是实现 Eclipse 两个插件间通信的最常见的一种方式。每个插件都可以实现其他插件提供的扩展点,也可以提供扩展点供其他插件来扩展,这样,提供扩展点的插件和实现扩展点的插件间就建立了通信。尽管 Eclipse 3.0 以后的版本采用了 OSGi 框架,但是早期的扩展(Extension)和扩展点(Extension points)仍然保留,从用户的角度来看,并没有发生什么变化。
Eclipse 提供了图形化的方法来创建扩展点和扩展,以 Eclipse 插件工程“SamplePluginProjectA”为例,我们在该插件中创建扩展点和扩展。首先,双击该工程中的 plugin.xml 文件,便可以看到如下视图。该视图包含了所有插件相关的信息。在“Overview”页面中,选中“This Plug-in is a singleton”,将该插件设置为单例,因为提供扩展点的插件必须是一个单例。
其次,在“Extension Points”页面中,点击“Add”按钮,便可通过创建扩展点向导来创建一个新的扩展点。扩展点主要包括:扩展点 ID,扩展点名字以及扩展点 Schema 三个元素。当用户填完这三个值之后,点击“Finish”按钮,便创建完成一个扩展点。
创建完扩展点,我们便可以在其它 Eclipse 插件工程中实现该扩展点,扩展点的一个具体实现我们便称为一个扩展,对于扩展,Eclipse 也提供了图形化的创建方式。在图 1 中的“Extension”tab 页中,单击“add”按钮,便可以通过下图中的创建扩展向导来创建扩展。
我们在插件工程根目录下的 plugin.xml 文件中,可以看到该插件提供的所有扩展点以及实现的扩展的信息。通过 Eclipse 扩展点机制,实现扩展点的插件与提供扩展点的插件,便能够进行数据的传递和方法调用,进而实现插件之间的通信。
OSGi 框架提供了管理和控制组件(Bundle)、服务(Service)生命周期的机制,以及组件和服务在其生命周期内的交互。Eclipse3.0 以后版本中开发 Eclipse 插件时,只需要在创建时选择“an OSGi framework”,便可以创建一个基于 OSGi 的 Eclipse 插件。如下图所示:
这种方式生成的 Eclipse 插件中是看不到扩展和扩展点功能配置的,如下图所示,图中不再有“Extension”和“Extension Points”两个 tab 页面。
基于 OSGi 之后,Eclipse 插件编程变得更为灵活,Bundle 之间的通信将通过面向服务的组件编程来实现,关于过面向服务的组件编程的更多信息可以参见参考资料的《基于 OSGi 的面向服务的组件编程》一文。
另一种常见也比较简单的方法,就是使用单例模式来实现插件间的信息共享和消息通信。
对于两个 Eclipse 插件 A 和插件 B,插件 A 需要调用 B 中方法,将数据传给 B,并得到相应结果。对于这种单向的关联,使用单例模式是一种很简单的解决办法:我们可以在 B 中创建一个单例对象,单例对象中实现了 A 所需要调用的功能,同时将插件 B 的单例所在的包通过 Export-Package 导出,在 A 中通过 Import-Package 导入单例所在的包,从而建立两个插件之间的关联关系。这样,在 A 中便可以得到该单例对象,进而调用相应 B 的方法和使用插件 B 中的数据。这种方式主要用于实现插件间的数据通信,其实和第一种方式可以归为一类,第一种方式侧重建立插件间的类的关联,因而这种方式单独作为一类。
回页首在 Eclipse 插件开发中,我们曾经遇到过各种奇怪的插件间通信问题,这些问题由于受到版本,兼容性,产品维护性,遗留问题等限制,对于一些现有插件和遗留插件之间的通信,使用前面列举的插件间通信方式,往往比较麻烦或者解决不了问题。现将这些问题列举如下,并在后面给出一种通过事件机制解决问题的办法,结合前面总结的四种方式,读者可以在今后的工作中参考使用。
问题编号问题描述
第一类问题两个 Eclipse 插件 A 和 B,插件 A 能单独作为产品发布,不受 B 的约束,而插件 B 必须依赖于插件 A,作为产品发布时和 A 一起发布,数据传输的方向从 A 到 B。
第二类问题两个 Eclipse 插件 A 和 B,插件 A 能单独作为产品发布,不受 B 的约束,而插件 B 必须依赖于插件 A,作为产品发布时要和 A 一起发布,并且当 A 和 B 同时存在时,数据传输的方向是双向的,也就是说,不单有从 A 到 B 的数据传递,还有从 B 到 A 的数据传递。
第三类问题两个 Eclipse 插件 A 和 B,插件 A,B 均能单独作为产品发布,并且当 A 和 B 同时存在时,A 和 B 之间存在数据交换,数据传输的方向可能是单向的也可能是双向的。
第四类问题两个 Eclipse 插件 A 和 B,插件 A 是早期产品,基于 Eclipse3.0 以前环境开发,后期因为产品功能的增强的需要,基于 Eclipse3.0 以后环境开发了插件 B,并且插件 B 需要从 A 获取数据。为了实现产品的维护,我们既要对 Eclipse3.0 以前的产品提供支持,又要对 Eclipse3.0 以后产品的支持。也就是说既要提供对 Eclipse 扩展方式插件的支持,又要实现对基于 OSGi 组件插件的支持。
回页首下面介绍一种通过事件机制实现插件之间的通信,这也是一类非常通用的办法。相对于前面总结的四类方式,可以作为很好的补充。
事件机制,简单地说,就是当一个对象接收到外部或者自身的一些事件,将这些事件通知给另一个对象,另一个对象采取相应的处理。在 JDK 中,已经很好地对事件机制进行了抽象。
Java 事件模型由三种类型的元素组成:事件对象,事件源对象,监听器对象。
事件(Event)是由外部系统或者用户操作触发的一系列事情,例如外部系统调用本系统提供的一些服务,计时器触发某些操作,用户移动鼠标、单击鼠标和按下键盘等。事件对象(Event Object)是描述事件信息的对象,包括该事件所处的事件源,事件的具体内容等等。
事件源对象(Event Object)表示事件发生的位置。当源对象上发生一个事件时,就会创建一个该事件对应的事件对象。
监听器对象(Event Listener)是监听并接收该事件,并根据事件内容产生相应操作的对象。监听器对象要监听某个事件源上的事件,必须向该事件源注册。而事件源对象维护一个监听器列表,事件发生时,事件源会把事件对象发送给所有的注册监听器。
上面便是 Java 事件模型的一些基本概念,比较容易理解。在 Eclipse 插件通信中,我们可以借鉴这一模型。
对于第二章中描述的第一、二类问题,考虑到数据从 A 向 B 传递,我们可以将 A 作为一个事件源,A 中用户的操作作为事件,B 作为事件监听者。无论插件是采用扩展点机制实现,还是采用 OSGi 技术实现的,当 B 被加载时,B 向 A 注册。注册成功之后,一旦 A 中有 B 感兴趣的事件发生,A 便会将事件通知给监听者 B。
对于第四类问题,因为既要解决早期的基于扩展机制实现的产品,又要实现对新的基于 OSGi 插件的支持,我们也可以采用事件机制来实现,这样可以避免维护两套代码。对于第三类问题,事件机制是无法实现的,这类问题要求插件间是完全可以独立存在的,因而只能使用 OSGi 框架实现组件间信息通信。
下面,我们将基于 Java 的事件机制来实现上述问题中 Eclipse 插件间的通信。
1 .首先,前两类问题中,由于插件 A 都能单独作为产品发布,因而插件 A 需要知道是否和插件 B 也存在于 Eclipse 环境中,如果存在,则启用相应的功能,否则,隐藏与插件 B 相关的功能。考虑到插件 ID 必须是唯一的,我们可以通过插件 B 的 ID 来检测 B 是否存在,使用 Bundle 类的 getSymbolicName() 即得到插件 ID。
private boolean isPluginBExist(BundleContext context){ boolean isBExist = false; Bundle[] bundles = context.getBundles(); int len = bundles.length; for(int i = 0; i < len; i++){ Bundle bundle = bundles[i]; // 插件 B 的 ID 是 PluginProjectB String name = (String)bundle.getSymbolicName(); if(name.equals("PluginProjectB")){ isBExist = true; } } return isBExist; }
2 .其次,在插件 A 中定义事件源对象,事件,事件监听器接口。
事件源对象提供对监听对象管理,包括监听器对象的注册方法,注销方法,以及将事件通知给监听器。事件源维护了一个监听者队列,所有监听者都需要向事件源注册,加入该队列,当监听者不再监听事件时,便向事件源注销,退出该队列。
public class PluginAEventSource { // 事件源中维护了一个管理所有监听者的队列 private List<PluginAEventListenerInterface> listenerList = new ArrayList<PluginAEventListenerInterface>(); public PluginAEventSource() { } // 将监听者对象添加到队列中 public synchronized void addMyEventListener(PluginAEventListenerInterface listener) { listenerList.add(listener); listener.handleEvent(null); } // 将监听者对象从队列中移出 public synchronized void removedMyEventListener( PluginAEventListenerInterface listener) { listenerList.remove(listener); }
// 事件源将事件通知给监听者。在此方法中,事件源可以根据一些条件,选择将事件通知给特定的监听者
public void notifyDemoEvent(PluginAEvent pe) { for(int i = 0; i < listenerList.size(); i++) { PluginAEventListenerInterface listener = listenerList.get(i); listener.handleEvent(pe); } } }
事件对象包装了事件相关的具体信息、作为参数传递给监听器。事件对象的具体信息内容用户可以自己定制,例如包含事件发生的时间,事件源对象等等。
public class PluginAEvent extends java.util.EventObject {
// 事件的构造方法
public PluginAEvent(Object source) { super(source); }
// 提供与事件相关的信息
public String getEventInformation() { return "sample event information"; } }
事件监听器接口,所有事件监听者都应该实现该接口,并在具体实现中提供相应的事件处理方法。事件发生时,事件源便调用监听器接口提供的事件处理方法统一通知监听者队列中的所有事件监听器对象。
public interface PluginAEventListenerInterface extends java.util.EventListener {
// 处理事件
public void handleEvent(PluginAEvent pe);
}
3 .在插件 B 中实现监听器对象,监听器对象必须实现插件 A 中的事件监听接口,提供自身的事件处理方法。并且在插件 B 被加载时,调用插件 A 中的 PluginAEventSource 类中的 addMyEventListener 方法,将该监听器对象向插件 A 的事件源注册。
public class PluginBEventListener implements PluginAEventListenerInterface { // 实现事件处理接口,提供插件 B 自身的处理方式 public void handleEvent(PluginAEvent de) { System.out.println("Plugin B operation..."); de.getEventInformation(); } }
将两个插件之间的调用关系通过事件机制建立起来之后,我们便可以在监听器和事件中添加用户的逻辑功能。当然,上面是基于 Java 事件模型的一种参考实现,JDK 提供的 java.util.EventObject 和 java.util.EventListener 分别被用作事件和事件监听器接口的父类。开发人员也可以完全不依赖 Java 事件机制,所有基类都可以由用户自己定义。
回页首本文首先介绍了四种最常用的 Eclipse 插件间通信方法,其中使用 Eclipse 扩展和使用 OSGi 框架是由 Eclipse 框架本身提供的,这两种方式实现的插件也最符合 Eclipse 架构的设计理念。事件机制是一类比较通用的解决问题的方法,可以作为插件常用通信方式的一种很好的补充,将其用于解决一些特殊的插件间通信问题,往往能取得事半功倍的效果。