内容


扩展 Eclipse 的 Java 开发工具

什么是可能的,从哪里开始以及如何继续

Comments

由于 Eclipse 具有功能强大的 Java 开发环境,这使它获得了人们的一致好评。这个 Java 开发环境(再加上团队环境和其它基础功能)使 Eclipse 成为引人注目的集成开发环境,对 Java 开发人员来说,这是个好消息。而且,Eclipse 还是一个开放源码项目。但真正使人们对 Eclipse 感到兴奋的是它提供了扩展的可能性。

许多基于 Eclipse 的商用产品都显示出这种提供集成产品的方法的实际意义。例如,IBM WebSphere Application Developer 和 Rational XDE 就说明了 Eclipse 已具有的影响。这些产品和其它基于 Eclipse 的产品降低了用户的“学习曲线”,因为它们具有类似的用户界面。当然,对于大型软件公司来说,这颇有价值,但是对于小公司或个人有什么用吗?

这正是 Eclipse 扩展性能力让人感兴趣的地方。不仅那些拥有大型开发组织的公司可以用它进行集成,任何愿意花些时间学习几个 Eclipse 框架的人也都可以利用这一能力。“哦不,”您可能在想,“别再提什么框架;我没时间学习更多框架。”不必担心;这学起来很快而且相当容易。在您的另一丝疑虑在头脑中开始形成之前,先声明一点,本文绝不是对 Eclipse 进行毫无价值的“hello world”式扩展。请放心,在如何增强 Eclipse 的 Java 开发环境的生产性使用方面,您将看到实际的价值以及一个清晰演示。您甚至还可能有点惊奇地发现:要完成某些相当令人惊异的事情,只要几十行代码就可以了。

本文将向您展示什么是可能的,从哪里开始,并将向您提供开始时需要什么的可靠评价。尽管扩展 Eclipse 是一个高级主题,但是您只要先了解如何使用 Eclipse 的 Java 开发环境就可以了(确保查阅了 参考资料中推荐的读物以进行进一步的研究)。

自己轻松重构成员可视性

最初在编写代码时,我没有过多地担心将方法可视性归为 default(包)、private、public 还是 protected。在创建方法时,我使它们都为 public。只有当我最后定下了包的组织结构并完成了对方法的重构 - 不管是通过从现有代码抽取出新方法、在层次结构中上移或下移方法还是将方法整个地移至其它类中 - 我才会重新检查方法的可视性。我认为,在我知道最终类的模样并实际使用过代码之前,我不想声明我的“客户们”可能需要什么。换句话说,在共享新框架之前,必须确定什么是实现细节,什么是必需的,这样别人才能够扩展它。

如果只需在 Outline 视图、Hierarchy 视图或任何您查看方法的地方选择方法 - 然后通过单击菜单选项,就可以将一个或多个方法设置成所期望的可视性 - 那么这会非常方便。诚然,我习惯了在我使用 VisualAge for Smalltalk 那段日子里学到的这一功能。图 1 显示了对 Eclipse 的 Java 开发环境中 Java 编辑器的 Outline 视图上下文的扩展。

图 1. 对方法的上下文菜单进行的扩展
对方法的上下文菜单进行的扩展

对方法的上下文菜单进行的扩展

从用户的角度而言,这很巧妙,因为这是引入用户界面的很自然的方法。没有任何暗示说这些新的菜单选项不属于 Eclipse 最初的 Java 开发工具(Java Development Tool,JDT)。事实上,那就是菜单级联使用“soln”前缀的原因 - 这样您就可以分辨出它是一个扩展!而且,开发人员不必记住只有在特定视图或编辑器中才可以使用这些选项,因为只要方法显示在哪里,它们就可以在哪里显示。

简述“Hello World”

“嘿,等一下,您承诺过不会有‘Hello, World’的!”是的,但在我们开始讨论真正有趣的事情之前,确实需要先来了解一下 Eclipse 的基础。所以,如果您从未编写过自己的 Eclipse 扩展,那么请和我一起快速了解一下 Eclipse 的体系结构和插件开发环境。 否则,请 至下一节。继续这次“旅行”!

本质上,Eclipse 是一组松散绑定但互相连接的代码块。 如果搞清楚这些代码块如何被“发现”,以及它们之间怎样相互发现和扩展,就能了解 Eclipse 体系结构的基本原理。

图 2. Eclipse 平台体系结构
Eclipse 平台体系结构

Eclipse 平台体系结构

这些功能单元称为 插件。Eclipse 平台运行时(参见图 2)负责在名为 plugin.xml 的文件中查找这些插件的声明(称为 插件清单),每个 plugin.xml 文件都在各插件的子目录中,这些子目录位于 Eclipse 的安装目录下名为 plugins 的公共目录(具体而言,就是 <inst_dir>\eclipse\plugins)。根据这些文件,Eclipse 平台运行时就在启动时在内存中构建一个全局注册表,称为 插件注册表,根据这个注册表,给定的插件就可以在运行时确定其它哪些插件希望扩展它。希望被其它插件扩展的插件将声明一个 扩展点。 这是插件的某种“电源板”,通过对插件声明 扩展,其它插件就可以利用这个插件。

回到我们的示例,那么任务就是通过查找满足我们需要的相应扩展点来决定从哪里“插入” Eclipse。幸运的是,一旦使用 Eclipse 一段时间后,您就会知道有数量惊人的东西可以使用,尽管可能您还没有实际使用过。 这是因为您在 Eclipse 用户界面所看到的与由构成 Eclipse 插件的类所建的模型通常几乎是一一对应的。 图 3 使这一点更为清晰:

图 3. 视图及其模型
视图及其模型

视图及其模型

这里我们看到一系列普通的用户界面,右侧的是最广为人知的用户界面 - 命令提示符(Command Prompt)窗口,在其中用 dir 命令显示文件系统内容,然后是左边专门化程度较高的视图 - JDT 的 Package Explorer。从用户界面的角度来看,所有这些视图都将同一“模型”(也就是一些文件)的表示可视化。作为 Eclipse 用户,我们很自然地会希望这两个 Eclipse 视图同时向我们提供查看同一事物的不同方法:Navigator 展示了部分操作系统文件的专门化视图(Eclipse 的工作空间),而 Package Explorer 向我们展示了同样的一些文件,这些文件是用对 Java 程序员而言更自然更高效的方法组织和表示的。

通过了解 Eclipse 用户界面如何反映其底层模型,以及 Eclipse 模型如何成为相互构建的基础,这向我们提供了该如何找到“插入”我们扩展的最佳位置的重要线索。显示在视图下面的 Eclipse 接口名称 IFileICompilationUnit 就是我们可以预期从构成 Eclipse 的模型中获得的两个接口示例。由于它们通常对应于用户界面中显示的控件项,所以您已经对通过编程获得的效果有一个直观的感受。

这是我们“旅行”的第 I 部分。在第 II 部分中,我们将讨论开发解决方案。 我们不打算提供这个解决方案并逐一解释,探索其中的一些奥秘,这不是更有趣吗?让我们首先讨论与以下这个问题相关的一些问题:使用我们自己的方法可视性重构能力来扩展 JDT。

把问题问在点子上比知道答案更重要

我们先探讨一些常规问题:

我们对基本 Eclipse 领域有了很好的了解之后,将转向一些特定于 JDT 的问题:

当然,还有最后一个大问题:

在用户界面中,如何显示扩展,显示在何处?

这在很大程度上是一个温和的提示,因为我们已得到了答案。 我们希望对一个或多个选中的方法显示上下文菜单选项,这些菜单选项允许我们只用一个操作就可以更改方法的可视性。我们更喜欢在可以显示方法的任何地方都能使用这些菜单选项,如在 Hierarchy 视图和 Package Explorer 中。这把我们带到下一个问题。

通常如何扩展用户界面?

通过示例来学习会更有趣,这方面 Plug-in Project 向导可以提供帮助,它提供了一些样本代码,我们可以修改这些代码来满足我们的需要。我们将回答该向导中的几个问题,它将自动启动用于插件开发的专门透视图,称为 Plug-in Development Environment(PDE),以准备测试。该向导包含了可以帮助我们入手的许多示例。事实上,我们的老朋友“Hello World”也在那里。为了沿袭这个传统,我们将生成这个“Hello World”,查看结果以验证是否正确安装了该环境,随后修改它以帮助我们回答当前的问题,并把我们带到下一个问题: 对用户界面的扩展如何知道类似于“选择”这样的基本事件?这很重要,因为我们希望将我们新近引入的菜单选项应用到当前选中的方法上。

请注意,这些指示信息假定您正从全新的 Eclipse 安装开始。如果修改了该环境或更改了首选项,那么可能不会完全象如下所述那样工作。您可以考虑从全新的工作空间启动 Eclipse:打开命令提示符窗口,更改到 <inst_dir>\eclipse 目录,然后使用 -data 参数启动 Eclipse,如清单 1 所示。

清单 1. 启动全新的 Eclipse 实例
cd c:\eclipse2.1\eclipse
eclipse.exe -data workspaceDevWorks

从使用 New Plug-in Project 向导创建一个插件项目开始。选择 File > New > Project。在 New Project 对话框中,在向导列表中选择 Plug-in Development and Plug-in Project,然后选择 Next。将项目命名为 com.ibm.lab.helloworld 。 该向导将根据这个名称创建插件标识,所以它在系统中必须是唯一的(按惯例,项目名和插件标识相同)。 使用显示在“Project contents”下面的推荐缺省工作空间位置就可以了;选择 Next

在下一页上,选择 Next以接受缺省插件项目结构。该插件代码生成器页推荐了许多样本,向导可以帮助您进一步对该项目进行参数化。选择“Hello, World”选项,然后选择 Next。下一页(显示在图 4 中)推荐了插件名和插件类名。这些名称基于插件项目 com.ibm.lab.helloworld 的最后一个单词。这个示例不需要任何插件类便利方法,所以取消对三个代码生成选项的选择(如图 4 所示),然后选择 Next(不是 Finish;您还有一页要完成)。

图 4. Simple Plug-in Content
Simple Plug-in Content

Simple Plug-in Content

您可以在下一页(显示在图 5 中)中指定参数,这些参数对于“Hello, Worlds”示例是唯一的,比如,将要显示的消息。

图 5. Sample Action Set
Sample Action Set

Sample Action Set

要简化所产生的代码,将该操作的目标包名从 com.ibm.lab.helloworld.actions 更改成 com.ibm.lab.helloworld ,即与该项目的名称相同。尽管在实际的插件中,您可以选择用不同的包对相关的类进行分组,但在本例中,只有两个类,所以不必这样做。这样也遵循了“主”包名和项目名相同这个惯例。现在选择 Finish

您应该看到一个信息消息:“Plug-ins required to compile Java classes in this plug-in are currently disabled. The wizard will enable them to avoid compile errors.”。 选择 OK 继续。 如果这是个全新的工作空间,那么您还将看到另一个信息消息:“This kind of project is associated with the Plug-in Development Perspective. Do you want to switch to this perspective now?”。 选择 Yes以根据这个消息的建议进行切换。

要验证所有的东西是否都设置正确,让我们测试新插件。 选择 Run > Run As > Run-Time Workbench。这将启动 Eclipse 的第二个实例,它将包含您的插件。这个新实例将创建一个新的名为 runtime-workspace 的工作空间目录,所以不必担心;对这个实例所作的任何测试都不会影响开发设置。您应该看到类似图 6 的样子,其中有一个新的下拉菜单,其标签为 Sample Menu,它有单一的选项 Sample Action。选择它将显示下面的信息消息。 如果您不是从全新的工作空间启动,那么可以选择 Window > Reset Perspective以查看新生成的下拉菜单;从现有工作空间启动时不会显示这个菜单,因为工作台“记得”上次 Eclipse 运行时哪些操作集是活动的(您还可以从 Window > Customize Perspective...下拉菜单选项上添加/删除操作集)。

图 6. Hello, Eclipse world
Hello, Eclipse world

Hello, Eclipse world

让我们快速浏览一下插件清单文件 plugin.xml。 双击它,以在 Plug-in Manifest 编辑器中打开它。这个编辑器提供了几个类似于向导的页和一个“原始”源代码页。通过选择 Source 选项卡转到源代码页。您将看到与以下清单 2 显示的代码相似的内容;我们感兴趣的是用粗体显示的那几部分。

清单 2. 所生成的“Hello, World” plugin.xml
			<extension
    point="org.eclipse.ui.actionSets">>
  <actionSet
      label="Sample Action Set"
      visible="true"
      id="com.ibm.lab.helloworld.actionSet">
    <menu
        label="Sample &Menu"
        id="sampleMenu">
      <separator 
        name="sampleGroup">
      </separator>
    </menu>
    <action
        label="&Sample Action"
        icon="icons/sample.gif"
        class="com.ibm.lab.helloworld.SampleAction"
        tooltip="Hello, Eclipse world"
        menubarPath="sampleMenu/sampleGroup"
        toolbarPath="sampleGroup"
        id="com.ibm.lab.helloworld.SampleAction">
    </action>
  </actionSet>
</extension>

并不需要过于深入地研究这个代码。我们“旅行”的第 II 部分的目的只是让您熟悉一些基本机制,借此我们可以介绍 JDT 的扩展。这里,您会看到这样一种技术的一个样本:它将菜单和菜单选项作为操作集添加到工作台。它以一个用 <extension point="org.eclipse.ui.actionSets"> 标记声明的扩展开始。工作台用户界面插件定义了这个扩展点 org.eclipse.ui.actionSets ,以及几个类似的扩展点,通过这几个扩展点可以向各种用户界面元素提供其它插件。

我们还未回答如何将菜单选项添加到 Java 方法的上下文菜单中。 一个简单示例可以给我们一些提示。 首先打开显示“Hello, World”消息的类 SampleAction ,请注意其 run 方法。 它不是特别有趣;不过我们还看到了另一个方法 selectionChanged 。啊哈!下一个问题的答案有了。

对用户界面的扩展如何知道类似于“选择”这样的基本事件?

工作台“选择”更改时会告知所提供的操作(象我们提供的菜单下拉选项)。这在 selectionChanged 方法前面的 Javadoc 注释中得到了确认。让我们修改这个方法以告知有关“选择”的更多信息。 首先,如果您还没有关闭工作台的运行时实例,那么现在就关闭。然后对 selectionChanged 方法添加清单 3 中的代码。

清单 3. selectionChanged 方法,首次修改
public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  System.out.println(selection);
}

有了这个调试代码,我们将看到选择了什么,并了解到有关什么使 Eclipse 工作的更多信息。保存该方法,然后重新启动运行时工作台。

重要:Eclipse 有一个延迟装入的策略,以在用户执行需要插件代码的操作时才装入插件。所以您必须 选择 Sample Action 菜单选项,以在调用 selectionChanged 方法之前装入您的插件。

现在选择其它东西,如编辑器中的文本、Navigator 中的文件,当然还有 Outline 视图中的成员(回忆一下:您必须创建一个 Java 项目和示例 Java 类来做到这一点,因为运行时实例使用不同的工作空间)。清单 4 显示了您将在 Eclipse 的 开发实例的控制台中看到的某个示例输出。

清单 4. selectionChanged 输出,首次修改
==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
org.eclipse.jface.text.TextSelection@9fca283
==========> selectionChanged
<empty selection>
==========> selectionChanged
[package com.ibm.lab.soln.jdt.excerpt [in [Working copy] ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]
==========> selectionChanged
[IMember[] members [in ChangeIMemberFlagAction [in [Working copy] 
    ChangeIMemberFlagAction.java 
    [in com.ibm.lab.soln.jdt.excerpt [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]
==========> selectionChanged
<empty selection>
==========> selectionChanged
[ChangeIMemberFlagAction.java [in com.ibm.lab.soln.jdt.excerpt 
      [in src [in com.ibm.lab.soln.jdt.excerpt]]]
  package com.ibm.lab.soln.jdt.excerpt
  import org.eclipse.jdt.core.Flags
  import org.eclipse.jdt.core.IBuffer
  ...lines omitted...
    void selectionChanged(IAction, ISelection)]
==========> selectionChanged
[boolean isChecked(IAction, IMember) [in ToggleIMemberFinalAction 
    [in ToggleIMemberFinalAction.java [in com.ibm.lab.soln.jdt.excerpt 
    [in src [in com.ibm.lab.soln.jdt.excerpt]]]]]]

唔,这不如我们所希望的那么有启发性。很明显,这个“选择”不象 String 的实例那么基本,但没有明示涉及了什么类,因为这些类很明显覆盖了它们的缺省 toString 方法。如果不用多做一点研究就可以明白这些类向我们显示的信息,那我们就轻松了,但目前我们还没有达到这种程度。回到 selectionChanged 方法,浏览 selection 参数的接口 ISelection 的层次结构。其层次结构表明它的通用子类型接口并不多,只有 IStructuredSelection (用于列表)和 ITextSelection 。通过输出所选的类,我们可以使 selectionChanged 方法稍微更智能一点。如清单 5 所示修改 selectionChanged 方法。

清单 5. selectionChanged 方法,第二次修改
public void selectionChanged(IAction action, ISelection selection) {
  System.out.println("==========> selectionChanged");
  if (selection != null) {
    if (selection instanceof IStructuredSelection) {
      IStructuredSelection ss = (IStructuredSelection) selection;
      if (ss.isEmpty())
        System.out.println("<empty selection>");
      else
        System.out.println("First selected element is " + 
          ss.getFirstElement().getClass());
    } else if (selection instanceof ITextSelection) {
      ITextSelection ts = (ITextSelection) selection;
      System.out.println("Selected text is <" + ts.getText() + ">");
    }
  } else {
    System.out.println("<empty selection>");
  }			
}

同样,请记住关闭运行时实例,然后重新启动。现在当您选择用户界面的各种元素时,它们是不是提供了更多信息,如清单 6 所示。

清单 6. selectionChanged 输出,第二次修改
----selected some methods in the Outline view
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
<selection is empty>
    activated the Java editor
==========> selectionChanged
Selected text is <isChecked>
==========> selectionChanged
<selection is empty>
    selected same methods and classes, package in the Package Explorer
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceType
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.PackageFragment
    activated the Navigator view, selected some files, folders, and projects
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
<selection is empty>
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.File
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Project
==========> selectionChanged
First selected element is class org.eclipse.core.internal.resources.Folder
==========> selectionChanged
<selection is empty>
    reactivated the Package Explorer, 
    selected some classes and methods in JARs of reference libraries
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.JarPackageFragment
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.ClassFile
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod

特别地,我们确认在用户界面中看到的东西与 JDT 模型类一一对应。我们之所以了解所显示的是作为选择的模型,而不是类似于字符串和图像的较低级的基本类型,要归功于另一个 Eclipse 框架,称为 JFace。这个框架在象字符串这样的基本类型(接近操作系统的窗口小部件希望使用这些基本类型)和更高级的模型对象(您的代码更愿意使用这些对象)之间进行映射。本文只是略微提及这个主题,因为我们设定的目标是扩展 JDT。 参考资料一节推荐了有关 JFace 的其它参考资料,它们将拓展您的理解。 本文将只讨论理解 JDT 扩展的基础所必需的知识。

回到输出,特殊的选项结果引起了我们的注意:它们对应于用户界面中 Java 成员的选择。清单 7 中重复了它们。

清单 7. selectionChanged 输出,再次研究
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.SourceMethod
==========> selectionChanged
First selected element is class org.eclipse.jdt.internal.core.BinaryMethod

这些类的包名中间的 internal 使人有点担心。但是,正如您通常会发现的,Eclipse 会有一个公共(public)接口,它对应于(内部)实现类,就如这里的例子。快速类查找揭示出:这些类都实现了看来很有希望成为这个问题答案的一组公共接口,也就是 ISourceReferenceIJavaElement ,尤其还有 IMember 。最后!现在我们可以开始早就希望进行的扩展,这使我们去回答下一个问题。

如何扩展 JDT 的特定元素的用户界面(象 Outline 视图中的成员)?扩展视图还是它们的底层模型?

简单的“Hello, World”示例显示了添加菜单选项只需要在插件清单文件中添加几行 XML( <extension point="org.eclipse.ui.actionSet"> )以及一个处理实际操作的类( com.ibm.lab.helloworld.SampleAction )。 向视图的下拉菜单、公共编辑器的工具栏以及弹出菜单添加操作基本上很简单。所提供的弹出菜单归结为两类:一类只与视图相关而与所选的对象无关(也就是,对视图的“空白处”单击鼠标右键通常会显示的“缺省”弹出菜单),另一类则更常见,它们与应用于所选对象的选项相关。在我们的例子中,我们希望目标只是具体选择的对象,所以通过在插件清单文件中定义一个扩展,我们将 提供的操作对象提供给这些对象的弹出菜单(对下面几个标识符进行了缩写,以获得更佳的格式;用‘…’表示),如清单 8 所示。

清单 8. 修饰符操作
<extension point="org.eclipse.ui.popupMenus">
  <objectContribution
      objectClass="org.eclipse.jdt.core.IMember"
      id="...imember">
      
    <menu
        label="Soln: Modifiers"
        path="group.reorganize"
        id="...imember.modifiers">
      <separator name="group1"/>
      <separator name="group2"/>
    </menu>
    
    <action
      label="Private"
      menubarPath="...imember.modifiers/group1"
      class="...jdt.excerpt.MakeIMemberPrivateAction"
      id="...imember.makeprivate">
    </action>
    
    <action
      label="Protected"
      menubarPath="...imember.modifiers/group1"
      class="...jdt.excerpt.MakeIMemberProtectedAction"
      id="...imember.makeprotected">
    </action>
    
    ...all menu choices not shown...
    
  </objectContribution>
</extension>

扩展点命名为 org.eclipse.ui.popupMenus ,顾名思义,它定义了向出现在工作台中的弹出菜单提供的对象。这个特殊示例只提供给明确选择的对象,即实现 IMember 接口的对象(请回忆一下 Java 语言规范中的定义,成员包含方法和字段)。我们的研究没有白费;我们得到了当前问题的答案,我们差不多准备好回答下一个问题了。

在这样做之前,此时请注意,我们找到的用于简单“Hello, World”操作示例的模式将对所提供的其它菜单操作进行重复。 即,将把选择更改告知 class 属性中指定的类(通过其 selectionChanged 方法),并还将告知它用户何时选择菜单选项(通过其 run 方法)。我们“旅行”的用户界面部分快要结束了;更困难的部分,也是影响我们所期望更改的部分还在前面。正如下一个问题所说的那样,在继续之前只要做一两次观察。

Package Explorer 中显示的元素和其它视图(如 Outline 视图)中显示的相同元素之间有什么关系?我们的 扩展是否需要知道它们之间的任何区别?

您可能已经注意到,当您在 Outline 视图和 Hierarchy 视图中选择了一个方法时,所选对象的类并非总是相同的。 例如,如果您在 Package Explorer 中展开一个库(JAR 文件)的内容,随后选择了一个类(或方法),那么它也不会是 Java 编辑器的 Outline 视图中同一选择的那个类。到底怎么回事?

这里,我们正在观察 JDT 的 Java 模型中“可编辑的”部分和始终为只读的部分之间的差别。这两部分 Java 模型都实现了公共接口(象 IMember ),但是它们拥有用来理解底层限制的实现类是不同的。另一个示例是,有一个表示 Java 编译单元的实现类,它派生自 Package Explorer 所显示的 JAR 文件的 .class 文件,还有另一个类表示直接派生自 .java 文件的编译单元。后一个实现将允许进行一些前者所不允许的修改,而它们 API 的共享部分是由接口 ICompilationUnit 表示的。

您以前在编辑 Java 源代码时,一定会观察到:在您输入方法特征符时 Outline 视图进行了更新(如果您没有注意到,那就试一下!)。这个示例说明了 JDT 如何在不同的区域暂放其“未提交的更改”,这与处理那些已保存、编译和集成到 Java 模型中的更改不同。有些视图(象 Java 编辑器的 Outline 视图)知道未提交的更改和已提交的更改,而其它象 Navigator 这样的视图只关心已保存到文件系统的已提交更改。

随后,我们所提供的用来修改 Java 成员的操作必须(至少在某种程度上)知道在什么上下文中调用它。 即,它必须识别出某些所选成员是可修改的(那些位于 Java 编辑器的 Outline 视图中的成员),而另一些成员是不可以修改的(存储在 JAR 文件中以及显示在 Package Explorer 中的 .class 文件中的成员)。记住这一点,让我们继续下一个问题。

如何通过编程更改 JDT 模型?

如果您在前面的“旅行”中稍作了研究,那么可能已经注意到 IMemberIJavaElement 以及我们的操作所看到的由所选的与 Java 相关的项实现的作为大部分接口的那部分都没有 setXXX 方法。那么如何修改这些接口呢?

您将发现这出奇地简单,不过可能在直觉上不那么明显。JDT 的 Java 模型在大多数实践情况下都是“只读”的。 通过与 Java 编译器的集成协作,给定元素的底层 Java 源代码进行的更改就与 Java 模型的其余部分的更改同步了。 实际上,您要做的就是更新 Java 源代码,而对模型所作的其余必要更改就传送给任何依赖它们的元素中。例如,每当 Java 源代码/Java 模型发生更改时,JDT 的索引会自动更新,所以仍旧可以快速执行搜索,重新编译从属类(按照项目特性中指定的 Java 构建路径所指示的),等等。

可以大松一口气了!以下就是 Java 模型是插件集成之关键的原因所在:Java 模型提供了整个 Java 环境的常见的内存中共享的模型,它的范围从一个项目,一直到其所有被引用的库,所有这些都不要您费心去操作文件系统中的 .java 文件、 .class 文件以及 .jar 文件。 您可以将精力集中于高级模型,而让 JDT 处理这其中的许多杂乱细节。

还不能确信它很容易?清单 9 包含了这个解决方案的核心代码的一小部分,它是从提供操作的 run 方法上抽取出的,并出于可读性考虑,稍作了简化:

清单 9. selectionChanged 方法,小型解决方案
public void selectionChanged(IAction action, ISelection selection) {			
  IMember member = (IMember) 
      ((IStructuredSelection) selection).getFirstElement();
  ICompilationUnit cu = member.getCompilationUnit();
  if (cu.isWorkingCopy()) {
    IBuffer buffer = cu.getBuffer();
    buffer.replace(...);
    cu.reconcile();
  }
}

似乎有点虎头蛇尾,不是吗?对您提供的操作提供了选中的成员,向它请求其父容器(Java .class.java 文件的模型,用 JDT 的说法,全都称为 编译单元),因为其父容器管理底层源代码,验证该成员是否属于“未提交的” Java 模型(换句话说,它目前在编辑器中是打开的),然后修改作为缓冲器返回的源代码。 IBuffer 接口类似于 StringBuffer ,其原理不同之处在于,更改与编译单元相关的缓冲区更新了 Java 模型的对应元素。对 reconcile 的最终调用告知 JDT 去通知其它相关各方(象 Package Explorer 视图):您的模型更新已准备好作为公共消费品。

您一定注意到上述代码中的省略号。在那里,您必须分析源代码本身以进行修改。 同样,JDT 会提供帮助,正如我们将在下一个问题中看到的。

怎样分析 Java 代码以进行修改?

JDT 提供了几个工具来帮助您分析代码。本文有意选择了最简单的 IScanner 接口进行演示,它的作用域也最有限。这个接口属于 JDT 工具箱,可以通过 JDT 的 ToolFactory 类访问它。 其 createScanner 方法返回一个扫描程序,该扫描程序会简化对一串 Java 代码作标记的工作。它不处理任何特别困难的操作,只是对所返回的标记进行简单的解析和分类。例如,它指出下一个标记是 public 关键字,其后的标记是一个标识符,再后面的标记是左圆括号,等等。随后,只有当您希望分析一小段代码(您明确理解想要在这段代码中得到什么)时,这个扫描程序才是合适的。您决不会使用扫描程序分析整个 Java 源代码;因为您会转而使用一些对编译器迷而言十分熟悉的工具:JDT 的抽象语法树(Abstract Syntax Tree,AST)框架。

与简单的扫描程序不同,AST 理解语言元素(它们不再只是“标记”)之间的关系。它可以识别象局部变量、实例变量、表达式以及 if 语句等六十多种不同的语言元素。它将帮助您进行涉及范围广泛的重构,或难以满足对标记进行一对一分类的模糊程度特别高的重构。要更清晰地了解何时使用扫描程序与何时使用 AST 之间的差别,请考虑清单 10 中的代码。

清单 10. 模糊的变量引用
public class Foo {
  int foo = 1;
  
  public int foo(int foo) {
      return foo + this.foo;
  }
  
  public int getFoo() {
    return foo;
  }
}

如果作为重构的一部分,您希望查找对实例变量 foo 的引用,那么就会明白一个单纯的解析会使区分本地引用和实例变量引用成为一个难题。AST 创建了完整的分析树,其中表示了 Java 源代码的每个元素并对这些元素进行了区分。在这个特例中,不同的类会考虑“foo”引用的上下文,将“foo”引用表示成 AST 的节点(如 FieldDeclarationSimpleNameThisExpression ),因此您会很轻松地识别它们。

正如前面提到的,本文将只讨论我们所选择的简单例子。 对于比较复杂的修改和分析示例,请参阅 参考资料一节。 现在,让我们回到我们前面跳过的用省略号表示的代码。这个代码将使用 IScanner 的实例以确定并替换源代码中确定成员可视性的关键字。我们将处理的可视性修饰符是 publicprivateprotectedfinal 。通过采用“蛮力”方法,我们可以简化这个解决方案,即,采用两个步骤就可以完成。首先删除方法特征符中所有的可视性修饰符(或至少扫描查找它们,如果找到,就删除),然后插入所希望的修饰符。特别地:

  1. 如果在方法特征符中找到 publicprivateprotected ,就删除它们。
  2. 插入所请求的可视性修饰符(对于包可视性的情况,不作任何操作,因为这是缺省操作;即没有任何修饰符)。

final 修饰符很简单。因为所希望的行为就是插入和除去这个修饰符,所以如果它存在,我们除去它;否则就插入它。 清单 11 中的代码只显示了一个例子,它无条件地将成员的可视性从 pubilc 改成 private。在与本文相关的解决方案中,您将看到每个操作的公共代码都被移到了抽象超类中。它基本上与下面的代码相同,只不过稍作了整理以避免冗余。

清单 11. 扫描是否有 pubilc 关键字
ICompilationUnit cu = member.getCompilationUnit();
if (cu.isWorkingCopy()) {
  IBuffer buffer = cu.getBuffer();
  
  IScanner scanner =
    ToolFactory.createScanner(false, false, false, false);
  scanner.setSource(buffer.getCharacters());
  ISourceRange sr = member.getSourceRange();
  scanner.resetTo(
    sr.getOffset(),
    sr.getOffset() + sr.getLength() - 1);
  int token = scanner.getNextToken();
  while (token != ITerminalSymbols.TokenNameEOF
      && token != ITerminalSymbols.TokenNameLPAREN)
    token = scanner.getNextToken();
    if (token == ITerminalSymbols.TokenNamePUBLIC) {
      buffer.replace(
        scanner.getCurrentTokenStartPosition(),
        scanner.getCurrentTokenEndPosition(),
        scanner.getCurrentTokenStartPosition() + 1,
        "private");
      break;
    }
  }
  cu.reconcile();
}

注:ITerminalSymbols 定义了扫描程序可以返回的标记名称,它们对应于 Java 语法的标准标记。 您可以进一步查询扫描程序以询问当前标记在缓冲区中开始和结束的具体位置,它出现在哪一行上,当然还有标记本身(特别是象 ITerminalSymbols.TokenNameStringLiteralITerminalSymbols.TokenNameIdentifier 这样的例子,它们不是保留的关键字)。

上述代码片段中,向 scanner.setSource 方法提供了编译单元的完整源代码,即 Java 源文件中的所有内容。正如前面提到的,扫描程序并不非常适合于大型分析,所以我们必须将它限制用于只有以目标方法的第一个字符开始,一直到调用 setSourceRange 方法作为结束的那部分源代码。 IMember 接口继承了 ISourceReferenceISourceReference 是一个允许您查询包含编译单元内的源代码字符串和源代码位置的接口。这使我们不必确定目标方法在 Java 源代码内开始和结束的位置。 原本可以用 AST 实现这一点,而 ISourceReference 接口使 AST 成了多余的工具。 由于 Java 方法特征符易于解析,所以 IScanner 接口的解析能力和它很匹配。 我们必须做的就是查找 public 关键字,它出现在方法声明的前一个字符之后,参数声明的左圆括号之前,用 private 关键字替换它。当然,在该解决方案中,这个接口将处理所有的可能情况,不管该方法最初是 public、private、protected 还是 package(缺省)。

下一步是什么?

本文设定的目标是向您提供一个对 Eclipse 的 Java 开发环境颇具价值的扩展,这样的扩展增强了这个开发环境的生产率。 坦率地说,出于简洁性考虑,我多次跳过了一些细节。该解决方案本身就作了一些简化假设,象只允许在编辑器中对已打开的 Java 源代码进行修改。您可能希望在更完整的实现中取消这个限制。

虽然如此,但我还是希望您能感受到什么是可能的,并确信这样做不是特别困难。本文中我们讨论的是 The Java Developer's Guide to Eclipse 一书某一高级章节的部分内容。该书中有十一个比较浅显的章节讨论了插件开发的基础。 象本文一样,大多数章节都包含了一个已文档化的工作解决方案,它可以强化您所学到的知识,大多数内容是以本文中您已看到的相同风格编写的(不过可能没有以这么快的节奏进行讨论!)。

除了我们的书以外,您还可以在 developerWorks和 Eclipse 的主页 eclipse.org上找到一些优秀的文章 - 其中的许多文章已经列在了下面的 参考资料一节中。

了解有关解决方案的更多信息并下载它

在解决方案摘录中获得本文所涉及内容的更多细节(请参阅 参考资料以获取链接)。 该解决方案摘录还描述了其它几个有用的 JDT 扩展,它们包含在 The Java Developer's Guide to Eclipse 所附带的光盘中。 要安装解决方案摘录,先下载它,将包含在其中的项目解压缩到您的工作空间中(例如, c:\eclipse2.1\eclipse\workspace ),然后通过选择 File > Import > Existing Project into Workspace,将该项目导入到当前的 Eclipse 工作空间。

重要:您可能需要向工作空间添加必要的插件,这样解决方案才能编译和运行。选择 Window > Preferences > Plug-in Development > Target Platform,然后选择 Not in Workspace。这将确保解决方案所依赖的基础插件在导入和重新编译过程中可用。

一旦导入完成,您可能需要切换至 Plug-in Development 透视图,在 com.ibm.lab.soln.jdt.excerpt 项目中选择 plugin.xml ,然后选择 Update Classpath。这将修改由于 Eclipse 安装路径和解决方案的安装路径不同所引起的编译错误。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 下载本文所用的 源代码,或者先 浏览关于源代码的更多细节。
  • eclipse.org 网站是 Eclipse 的“家”。
  • 本文的解决方案部分基于由 Sherry Shavor、Jim D'Anjou、Dan Kehn、Scott Fairbrother、John Kellerman 和 Pat McCarthy 编写的 The Java Developer's Guide to Eclipse(Addison Wesley Professional 于 2003 年出版;ISBN 0321159640)第 26 章中附带的解决方案。
  • developerWorks上查找适合 Eclipse 用户的更多参考资料。

评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=21989
ArticleTitle=扩展 Eclipse 的 Java 开发工具
publish-date=09102003