使用 XML

定义和加载扩展点

扩展 Eclipse 插件使其更通用

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 XML

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 XML

敬请期待该系列的后续内容。

使用 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 插件的重要组成部分)。

继续讨论之前,首先要澄清扩展点和扩展之间的差别。扩展点是一个端口的定义,即其他插件提供服务的入口。在 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 直到最后一分钟才加载插件,使用 plugin.xml 描述文件中的数据来判断是否要加载给定的插件(请参阅 Eclipse 插件体系结构的好处)。但是如何判断是否需要加载一个扩展点呢?不同的扩展点有不同的条件。

为了解决这个问题,可以扩展 plugin.xml,使站点设计人员能够添加带有适当信息的标记。其中包括扩展点加载扩展所需要的信息,如类名,以及 加载扩展所需要的信息,如确定插件是否适用的条件。您可能还记得上一期文章中编写的 XM builder,它在 plugin.xml 中提供了类名(以便加载插件)和项目特性(确定是否需要加载该插件)。您可能也注意到,每个扩展都使用不同的标记。

扩展点在 XML Schema 中指定数据。该模式必须声明 extension 元素(带有三个属性:idnamepoint)。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 中的扩展标签。对于每个实现扩展点的插件,您都会收到一个 IConfigurationElementIConfigurationElement 提供了 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 成功的关键之一是预置到平台中的东西很少。这为开发人员提供了一个非常灵活的工具,从而可以根据任务塑造成最佳的形式。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=58236
ArticleTitle=使用 XML: 定义和加载扩展点
publish-date=02012005