-- 作者:oceans
-- 发布时间:3/3/2005 3:23:00 PM
-- 使用 XML: 定义和加载扩展点
扩展 Eclipse 插件使其更通用 级别: 高级 BenoÎt Marchal (bmarchal@pineapplesoft.com) 顾问, Pineapplesoft 2005 年 2 月 在这一篇文章中,BenoÎt 将进一步集成简单的内容管理解决方案 XM 和 Eclipse。除了 XML 之外,发布 Web 站点需要处理很多文件类型,因此围绕着可扩展的核心设计一个发布系统是合情合理的。Eclipse 插件非常合适这一点。BenoÎt 说明了如何使 XM 插件变得能够扩展,以便适应多种文件类型。在本文的讨论论坛中与作者和其他读者分享您对本文的看法。(您也可以单击本文顶部或底部的讨论来访问论坛)。 在使用 XML 系列的前两期文章中,主要关注的是一位老朋友:XM,这是一种易用的文档发布解决方案。XM 以 XML 和 XSLT 为基础来管理 Web 站点和印刷(PDF)发布。在这一系列文章中,我已经更新了核心发布引擎的大部分,使其更加灵活,并且和 Eclipse IDE 集成在一起,以获得更加智能化的构建和更好的错误报告能力。到目前为止,XM 插件为平台增加一些固定的特性,即处理 XML 和 XSLT 文件的能力。现在我将说明如何使 XM 插件本身能够扩展,以便能够处理任何文件。 虽然 XM 主要依靠 XML,但是也需要处理其他文档类型,如图像、办公文档和 PDF 文件。从一开始我就围绕着一个可扩展的核心来组织 XM。有一个接口(最初是 Mover,现在改为 Batch)组织了所有关于文件格式的知识。为了支持新的格式(如 PDF),只需要实现一个新的 Batch,同时向引擎注册该 Batch,然后重新编译。如果您愿意花时间修改源代码,这种方法当然很好,但并不一定要如此。通过 Eclipse,您可以将 Batch 功能和主引擎从物理上分开。新的实现可以为放在另一个插件中,这极大地增强了灵活性。 扩展点 这种体系结构包括通过扩展点协作的两个或多个插件(请参阅导入和扩展点)。如果您一直阅读本系列的文章,那么您对扩展点可能已经熟悉了。前面的文章中已经实现了几个扩展点(尤其是 使用 XML:用 Eclipse 和 XM 构建项目 和 使用 XML:利用重构 XM 得来的经验 这两期文章中,开发的 builder 扩展是 XM 插件的重要组成部分)。 导入和扩展点 Eclipse 插件不需要扩展点来协同工作。插件可以直接从其他插件中导入类(通过 plugin.xml 中的 requires 标签)。通过导入,开发人员只能导入他们在编译时已经知道的服务,而扩展点允许开发人员在能够使用甚至编写之前就指定服务,这是一种更加灵活的方法。使用导入的时候由服务提供者定义 API,而使用扩展点则由服务的用户定义 API。 继续讨论之前,首先要澄清扩展点和扩展之间的差别。扩展点是一个端口的定义,即其他插件提供服务的入口。在 Java 语言中最接近的东西是接口。与接口一样,扩展点定义了用户与服务提供者之间的契约。。 扩展实现就是真正的服务,通过特殊的打包方式以便能够在扩展点调用。如果将扩展点看作接口,那么扩展就是实现该接口的类。插件可以同时实现扩展(为其他插件提供服务)和定义扩展点(请求其他插件的服务)。 虽然 Eclipse 平台提供了很多标准插件和对应的标准扩展点,扩展点机制是完全开放的。Eclipse 提供的插件没有使用秘密的后门或者特殊的服务,它们仅仅是插件。用于标准插件的扩展选项同样可用于其他插件。 定义扩展点 与 Eclipse 的多数特性一样,扩展点也首先从 plugin.xml 文件开始。插件通过 extension-point 标签描述自己支持的扩展点,类似于 Batch 扩展点中的定义: <extension-point id="batch" name="Batch" schema="schema/batch.exsd"/> extension-point 标签需要三个参数: id 是扩展点标识符。Eclipse 将它与插件 id 连在一起,作为平台提供的惟一标识符。 name 是用户友好的名称。 schema 只想描述扩展标记的 XML Schema。扩展实现者在自己的插件中使用 plugin.xml 文件中的模式。 多数扩展点都提供一个或多个 Java 接口以实现扩展。 Eclipse 插件体系结构的好处 Java 语言一直支持动态加载类。Eclipse 插件建立在 Java 动态加载的基础上,但是带了两个重要的好处。 首先,平台是插件的媒介。无论如何使用,扩展点和扩展实现必须要发现对方 —— 不是通过重新编译。Eclipse 管理扩展注册来实现这种机制。 其次,插件在 plugin.xml 文件中包含大量的描述信息,还可以通过模式增加信息!因此,在很多情况下,扩展点是用标记来判断是否需要加载扩展。 不要低估了第二方面的好处。很多基于插件的应用程序似乎都一直不停地加载,因为它们在一开始就初始化和加载所有的插件。(Adobe® Photoshop® 是最为声名狼藉的一个例子。)Eclipse IDE 完全建立在插件之外,因此不允许在启动的时候将其全部加载。比如,假设用户已经安装了用于 Java、C++ 和 XML 开发的插件,而目前正在处理 Java 项目。加载 C++ 和 XML 插件就是不必要的,只会拖后腿。Eclipse 直到最后一刻才加载必要的插件。 扩展模式 Eclipse 直到最后一分钟才加载插件,使用 plugin.xml 描述文件中的数据来判断是否要加载给定的插件(请参阅 Eclipse 插件体系结构的好处)。但是如何判断是否需要加载一个扩展点呢?不同的扩展点有不同的条件。 为了解决这个问题,可以扩展 plugin.xml,使站点设计人员能够添加带有适当信息的标记。其中包括扩展点加载扩展所需要的信息,如类名,以及不 加载扩展所需要的信息,如确定插件是否适用的条件。您可能还记得上一期文章中编写的 XM builder,它在 plugin.xml 中提供了类名(以便加载插件)和项目特性(确定是否需要加载该插件)。您可能也注意到,每个扩展都使用不同的标记。 扩展点在 XML Schema 中指定数据。该模式必须声明 extension 元素(带有三个属性:id、name 和 point)。Eclipse 平台需要该元素及其三个属性,以便标识扩展。但是,extension 元素的内容是由开发人员决定的。因为多数文件类型都有惟一的扩展名,我决定采用基于文件名的简单过滤器。清单 1 包括了 Batch 扩展点的模式定义。 清单 1. Batch 模式 <?xml version='1.0' encoding='UTF-8'?> <schema targetNamespace="org.ananas.xm.eclipse"> <annotation> <appInfo> <meta.schema plugin="org.ananas.xm.eclipse" id="batch" name="Batch"/> </appInfo> <documentation>Adds a file type to XM.</documentation> </annotation> <element name="run"> <annotation> <documentation>implementation class</documentation> </annotation> <complexType> <sequence/> <attribute name="class" type="string" use="required"/> </complexType> </element> <element name="target"> <annotation> <documentation>filtering to recognize the file type</documentation> </annotation> <complexType> <sequence/> <attribute name="pattern" type="string" use="required"/> <attribute name="targetAdded" type="boolean"/> <attribute name="targetModified" type="boolean"/> <attribute name="targetRemoved" type="boolean"/> <attribute name="targetUnchanged" type="boolean"/> </complexType> </element> <element name="batch"> <complexType> <sequence> <element ref="run"/> <element ref="target" minOccurs="0"/> </sequence> </complexType> </element> <element name="extension"> <complexType> <sequence><element ref="batch"/></sequence> <attribute name="point" type="string" use="required"> <annotation> <documentation> should be org.ananas.xm.eclipse.batch </documentation> </annotation> </attribute> <attribute name="id" type="string"> <annotation> <documentation>identifier</documentation> </annotation> </attribute> <attribute name="name" type="string"> <annotation> <documentation>name</documentation> </annotation> </attribute> </complexType> </element> </schema> 要注意,Eclipse 仅支持模式定义的一个子集。具体而言,这里只能使用全局元素(直接定义在 schema 元素下,通过 ref 属性来引用)。模式必须从一个特殊的注释开始,meta.schema 指向插件。 调用扩展点 现在已经声明了扩展点,还需要加载它。窍门是只有绝对需要时才加载它。 发现扩展 扩展点背后的假设是:扩展点的实现在编译时还不能使用,因此简单的 new 是不够的。Eclipse 平台管理扩展实现的注册。加载扩展需要从平台上(通过恰当命名的 Platform 对象)来访问注册(通过 IExtensionRegistry 接口),然后查询所关心的插件的扩展点。平台返回一个 IExtensionPoint 对象。 IExtensionPoint 返回一个 IConfigurationElement 对象数组,用它表示 plugin.xml 中的扩展标签。对于每个实现扩展点的插件,您都会收到一个 IConfigurationElement。IConfigurationElement 提供了 getChildren() 和 getAttribute() 之类的方法,以便从 XML 标记中检索数据。最后,createExecutableExtension() 返回实现扩展的 Java 类。它从 XML 标记的一个属性得到 Java 类的名称。 请参见清单 2 中的 loadExtensions() 方法。 代理模式 加载扩展点的最佳解决方案是使用代理模式。这种模式中,一个对象(代理)处理对另一个对象(因为没有更好的名称,暂时称之为实际对象)的访问。这样,代理可以监控对实际对象的请求,并在过滤请求和需要请求的情况下重新组织这些请求。代理有很多用处,比如包装遗留系统、调整库的接口、管理实际对象的副本等。我将使用代理把加载隔离出来。 要记住,除非绝对必要,否则最好不要加载插件。比如,加载处理没有使用的文件类型的插件是没有意义的。代理根据 plugin.xml 文件中的数据(由 IConfigurationElement 返回)过滤调用。 这里的实现根据文件名过滤文件,但是每次调用都检查需要插件是否会带来很大的不便。另一种做法是,把加载管理放在代理对象中,让它来判断是否需要加载组件,以及何时加载插件是合理的。清单 2 显示了加载 Batch 扩展点的代理对象。 清单 2. 加载扩展点的代理 package org.ananas.xm.eclipse; import org.ananas.xm.core.Batch; import org.ananas.xm.core.Location; import org.ananas.xm.core.Filename; import org.ananas.xm.core.Messenger; import org.ananas.xm.core.XMException; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.IConfigurationElement; public class ExtensionBatch implements Batch { protected BatchExtensionPoint extension; protected IConfigurationElement element; protected String pattern; protected boolean targetAdded, targetModified, targetUnchanged, targetRemoved; protected Messenger messenger; public ExtensionBatch(IConfigurationElement element) { this.element = element; extension = null; messenger = null; System.out.println(element.getName()); IConfigurationElement children[] = element.getChildren("target"); if(children == null || children.length == 0) { pattern = null; targetAdded = true; targetModified = true; targetUnchanged = false; targetRemoved = false; } else { pattern = children[0].getAttribute("pattern"); String st = children[0].getAttribute("targetAdded"); targetAdded = st == null ? true : Boolean.valueOf(st).booleanValue(); st = children[0].getAttribute("targetModified"); targetModified = st == null ? true : Boolean.valueOf(st).booleanValue(); st = children[0].getAttribute("targetUnchanged"); targetUnchanged = st == null ? true : Boolean.valueOf(st).booleanValue(); st = children[0].getAttribute("targetRemoved"); targetRemoved = st == null ? true : Boolean.valueOf(st).booleanValue(); } } public void setMessenger(Messenger messenger) throws XMException { this.messenger = messenger; if(extension != null) extension.setMessenger(messenger); } public Messenger getMessenger() { return messenger; } public boolean isTargetAdded() { return targetAdded; } public boolean isTargetModified() { return targetModified; } public boolean isTargetUnchanged() { return targetUnchanged; } public boolean isTargetRemoved() { return targetRemoved; } public String getName() { return element.getAttribute("name"); } public int appliesTo(Filename filename) throws XMException { if(filename.nameMatches(pattern)) { loadExtension(filename); return extension.confirmAppliesTo(filename); } return 0; } public boolean process(Filename publish,Filename file) throws XMException { loadExtension(file); return extension.process(publish,file); } protected void loadExtension(Filename file) throws XMException { try { Object o = null; IConfigurationElement children[] = element.getChildren("run"); if(children != null && children.length != 0) o = children[0].createExecutableExtension("class"); if(o == null || !(o instanceof BatchExtensionPoint)) messenger.fatal( new XMException(messenger.getResourceString( "eclipse.noextension",element.getAttribute("id")), new Location(file, Location.UNKNOWN_POSITION, Location.UNKNOWN_POSITION))); else { extension = (BatchExtensionPoint)o; extension.setMessenger(messenger); } } catch(CoreException x) { messenger.fatal(new XMException(x)); } } static public ExtensionBatch[] loadExtensions(Messenger messenger) throws XMException { IExtensionRegistry registry = Platform.getExtensionRegistry(); IExtensionPoint extensionPoint = registry.getExtensionPoint("org.ananas.xm.eclipse.batch"); IConfigurationElement points[] = extensionPoint.getConfigurationElements(); ExtensionBatch batches[] = new ExtensionBatch[points.length]; for(int i = 0;i < points.length;i++) { batches[i] = new ExtensionBatch(points[i]); batches[i].setMessenger(messenger); } return batches; } } 结束语 Eclipse 不仅仅是一个 IDE。它包含成熟的插件解决方案,这使它成为一个真正的开发平台。您可以将 Eclipse 用于任何应用程序,包括发布解决方案,就像本系列文章所介绍的那样。Eclipse 成功的关键之一是预置到平台中的东西很少。这为开发人员提供了一个非常灵活的工具,从而可以根据任务塑造成最佳的形式。
|