实现软件模块化是一项众所周知且困难的任务。与由多元化社区编写的庞大代码库的互操作性也很难管理。在 Eclipse 中,我们在这两方面都取得了成功。2010 年 6 月,Eclipse 基金会发布了其 Helios 协调版本,超过 39 个项目和来自 40 多家公司的 490 名提交者共同努力,在基础平台的功能之上进行构建。Eclipse 最初的架构愿景是什么?它是如何演变的?应用程序的架构如何促进社区参与和发展?让我们回到起点。
2001 年 11 月 7 日,一个名为 Eclipse 1.0 的开源项目发布了。当时,Eclipse 被描述为“适用于任何事物的集成开发环境 (IDE),也可能不适用于任何特定事物”。这种描述是有意泛化的,因为架构愿景不仅仅是另一套工具,而是一个框架;一个模块化且可扩展的框架。Eclipse 提供了一个基于组件的平台,可以作为构建开发者工具的基础。这种可扩展的架构鼓励社区在核心平台上构建,并将其扩展到最初愿景的限制之外。Eclipse 起源于一个平台,而 Eclipse SDK 是其概念验证产品。Eclipse SDK 允许开发者自托管并使用 Eclipse SDK 本身来构建更新版本的 Eclipse。
开源开发人员的典型形象是一个利他主义者,彻夜工作修复错误并实现奇妙的新功能以满足他们自己的个人兴趣。相比之下,如果您回顾 Eclipse 项目的早期历史,一些最初捐赠的代码基于 IBM 开发的 VisualAge for Java。参与这个开源项目的首批提交者是 IBM 子公司 Object Technology International (OTI) 的员工。这些提交者被付费全职参与开源项目,在新闻组上回答问题、解决错误和实现新功能。一个由感兴趣的软件供应商组成的联盟成立以扩展这种开放工具的努力。Eclipse 联盟的初始成员包括 Borland、IBM、Merant、QNX Software Systems、Rational Software、RedHat、SuSE 和 TogetherSoft。
通过投资这项工作,这些公司将拥有基于 Eclipse 发布商业产品的专业知识。这类似于公司在为 Linux 内核做贡献方面的投资,因为让他们员工改进其商业产品所依赖的开源软件符合他们的自身利益。2004 年初,Eclipse 基金会成立以管理和扩展不断增长的 Eclipse 社区。这个非盈利基金会由企业会员费资助,并由董事会管理。如今,Eclipse 社区的多样性已经扩展到包括 170 多家成员公司和近 1000 名提交者。
最初,人们只将“Eclipse”视为 SDK,但如今它已远不止于此。2010 年 7 月,eclipse.org 上有 250 个不同的项目正在开发中。有用于支持 C/C++、PHP、Web 服务、模型驱动开发、构建工具等开发的工具。每个项目都包含在一个顶级项目 (TLP) 中,该项目由项目管理委员会 (PMC) 管理,该委员会由项目中高级成员组成,他们被提名为负责设定技术方向和发布目标。为简洁起见,本章的范围将限于 Eclipse1 和 Runtime Equinox2 项目中 Eclipse SDK 架构的演变。由于 Eclipse 拥有悠久的历史,我将重点关注早期 Eclipse 以及 3.0、3.4 和 4.0 版本。
在 21 世纪初,软件开发人员有很多工具,但很少有工具能够协同工作。Eclipse 试图为应用程序开发人员提供一个创建互操作工具的开源平台。这将允许开发人员专注于编写新工具,而不是编写代码来处理基础设施问题,例如与文件系统交互、提供软件更新以及连接到源代码存储库。Eclipse 最著名的可能是 Java 开发工具 (JDT)。其目的是让这些示例性的 Java 开发工具成为希望为其他语言提供工具的人们的示例。
在深入探讨 Eclipse 的架构之前,让我们看看 Eclipse SDK 对开发人员来说是什么样子的。启动 Eclipse 并选择工作台后,您将看到 Java 透视图。透视图组织了当前正在使用的工具特有的视图和编辑器。
图 6.1:Java 透视图
早期版本的 Eclipse SDK 架构有三个主要元素,对应于三个主要子项目:平台、JDT(Java 开发工具)和 PDE(插件开发环境)。
Eclipse 平台是用 Java 编写的,需要 Java VM 才能运行。它由称为插件的小型功能单元构建而成。插件是 Eclipse 组件模型的基础。插件本质上是一个 JAR 文件,其中包含一个清单文件,用于描述自身、其依赖项以及如何使用或扩展它。此清单信息最初存储在插件目录根目录中的plug-in.xml
文件中。Java 开发工具提供了用于 Java 开发的插件。插件开发环境 (PDE) 提供了用于开发插件以扩展 Eclipse 的工具。Eclipse 插件是用 Java 编写的,但也可以包含非代码贡献,例如用于在线文档的 HTML 文件。每个插件都有自己的类加载器。插件可以通过在plugin.xml
中使用requires
语句来表达对其他插件的依赖关系。查看org.eclipse.ui
插件的plugin.xml
,您可以看到其指定的名称和版本,以及它需要从其他插件导入的依赖项。
<?xml version="1.0" encoding="UTF-8"?> <plugin id="org.eclipse.ui" name="%Plugin.name" version="2.1.1" provider-name="%Plugin.providerName" class="org.eclipse.ui.internal.UIPlugin"> <runtime> <library name="ui.jar"> <export name="*"/> <packages prefixes="org.eclipse.ui"/> </library> </runtime> <requires> <import plugin="org.apache.xerces"/> <import plugin="org.eclipse.core.resources"/> <import plugin="org.eclipse.update.core"/> : : : <import plugin="org.eclipse.text" export="true"/> <import plugin="org.eclipse.ui.workbench.texteditor" export="true"/> <import plugin="org.eclipse.ui.editors" export="true"/> </requires> </plugin>
为了鼓励人们在 Eclipse 平台上构建,需要有一种机制来向平台做出贡献,以及平台接受这种贡献。这是通过使用扩展和扩展点来实现的,这是 Eclipse 组件模型的另一个元素。导出标识了您期望其他人编写扩展时使用的接口,这将可用于插件外部的类限制为导出的类。它还对插件外部可用的资源提供了其他限制,而不是使所有公共方法或类可供使用者使用。导出的插件被视为公共 API。所有其他插件都被视为私有实现细节。要编写一个插件,该插件将向 Eclipse 工具栏贡献一个菜单项,您可以使用org.eclipse.ui
插件中的actionSets
扩展点。
<extension-point id="actionSets" name="%ExtPoint.actionSets" schema="schema/actionSets.exsd"/> <extension-point id="commands" name="%ExtPoint.commands" schema="schema/commands.exsd"/> <extension-point id="contexts" name="%ExtPoint.contexts" schema="schema/contexts.exsd"/> <extension-point id="decorators" name="%ExtPoint.decorators" schema="schema/decorators.exsd"/> <extension-point id="dropActions" name="%ExtPoint.dropActions" schema="schema/dropActions.exsd"/> =
您的插件扩展以向org.eclipse.ui.actionSet
扩展点贡献菜单项,如下所示
<?xml version="1.0" encoding="UTF-8"?> <plugin id="com.example.helloworld" name="com.example.helloworld" version="1.0.0"> <runtime> <library name="helloworld.jar"/> </runtime> <requires> <import plugin="org.eclipse.ui"/> </requires> <extension point="org.eclipse.ui.actionSets"> <actionSet label="Example Action Set" visible="true" id="org.eclipse.helloworld.actionSet"> <menu label="Example &Menu" id="exampleMenu"> <separator name="exampleGroup"> </separator> </menu> <action label="&Example Action" icon="icons/example.gif" tooltip="Hello, Eclipse world" class="com.example.helloworld.actions.ExampleAction" menubarPath="exampleMenu/exampleGroup" toolbarPath="exampleGroup" id="org.eclipse.helloworld.actions.ExampleAction"> </action> </actionSet> </extension> </plugin>
启动 Eclipse 时,运行时平台会扫描安装程序中插件的清单文件,并构建一个存储在内存中的插件注册表。扩展点和相应的扩展通过名称进行映射。生成的插件注册表可以通过 Eclipse 平台提供的 API 进行引用。注册表被缓存到磁盘,以便在下次重新启动 Eclipse 时可以重新加载此信息。所有插件在启动时都被发现以填充注册表,但只有在实际使用代码时才会激活(加载类)。这种方法称为延迟激活。通过不在需要之前加载与插件关联的类,可以减少将其他捆绑包添加到安装程序中的性能影响。例如,对 org.eclipse.ui.actionSet 扩展点做出贡献的插件在用户选择工具栏中的新菜单项之前不会被激活。
生成此菜单项的代码如下所示
package com.example.helloworld.actions; import org.eclipse.jface.action.IAction; import org.eclipse.jface.viewers.ISelection; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.IWorkbenchWindowActionDelegate; import org.eclipse.jface.dialogs.MessageDialog; public class ExampleAction implements IWorkbenchWindowActionDelegate { private IWorkbenchWindow window; public ExampleAction() { } public void run(IAction action) { MessageDialog.openInformation( window.getShell(), "org.eclipse.helloworld", "Hello, Eclipse architecture world"); } public void selectionChanged(IAction action, ISelection selection) { } public void dispose() { } public void init(IWorkbenchWindow window) { this.window = window; } }
一旦用户在工具栏中选择了新项目,实现扩展点的插件就会查询扩展注册表。提供扩展的插件会实例化贡献并加载插件。插件激活后,我们示例中的ExampleAction
构造函数将运行,然后初始化Workbench
动作委托。由于工作台中的选择已更改并且已创建委托,因此操作可以更改。消息对话框将打开,显示消息“Hello, Eclipse architecture world”。
这种可扩展的架构是 Eclipse 生态系统成功发展的重要因素之一。公司或个人可以开发新的插件,并将它们作为开源发布或商业销售。
关于 Eclipse 最重要的概念之一是一切都是插件。无论插件是否包含在 Eclipse 平台中,或者您自己编写,插件都是组装应用程序的一流组件。图 6.3 显示了早期版本的 Eclipse 中插件提供的相关功能集群。
图 6.3:早期 Eclipse 架构
工作台是 Eclipse 平台用户最熟悉的 UI 元素,因为它提供了组织 Eclipse 在桌面用户界面上显示方式的结构。工作台由透视图、视图和编辑器组成。编辑器与文件类型相关联,因此在打开文件时会启动正确的编辑器。视图的一个示例是“问题”视图,它指示 Java 代码中的错误或警告。编辑器和视图共同形成一个透视图,以组织的方式向用户呈现工具。
Eclipse 工作台构建在 Standard Widget Toolkit (SWT) 和 JFace 之上,SWT 值得我们进行一些探索。窗口小部件工具包通常分类为本机或模拟。本机窗口小部件工具包使用操作系统调用来构建用户界面组件,例如列表和按钮。组件的交互由操作系统处理。模拟窗口小部件工具包在操作系统外部实现组件,处理鼠标和键盘、绘图、焦点和其他窗口小部件功能本身,而不是委托给操作系统。这两种设计都有不同的优缺点。
本机窗口小部件工具包是“像素完美的”。它们的窗口小部件的外观和感觉与其在桌面上的其他应用程序中的对应项相同。操作系统供应商不断更改其窗口小部件的外观和感觉并添加新功能。本机窗口小部件工具包可以免费获得这些更新。不幸的是,本机工具包很难实现,因为它们的基础操作系统窗口小部件实现差异很大,导致不一致和不可移植的程序。
模拟的部件工具包要么提供自己的外观和感觉,要么尝试绘制并表现得像操作系统一样。与原生工具包相比,它们的优势在于灵活性(尽管现代原生部件工具包,如 Windows Presentation Framework (WPF),也同样灵活)。因为实现部件的代码是工具包的一部分,而不是嵌入在操作系统中,所以部件可以被设置为以任何方式绘制和表现。使用模拟部件工具包的程序具有很高的可移植性。早期的模拟部件工具包名声不佳。它们通常速度缓慢,并且在模拟操作系统方面做得不好,这使得它们在桌面上显得格格不入。特别是,当时的 Smalltalk-80 程序很容易识别,因为它们使用了模拟部件。用户意识到他们正在运行一个“Smalltalk 程序”,这不利于用 Smalltalk 编写的应用程序的接受度。
与其他计算机语言(如 C 和 C++)不同,Java 的第一个版本附带了一个名为抽象窗口工具包 (AWT) 的原生部件工具包库。AWT 被认为是有限的、有错误的和不一致的,并遭到广泛的批评。在 Sun 和其他地方,部分由于使用 AWT 的经验,人们认为一个可移植且高性能的原生部件工具包是不可行的。解决方案是 Swing,一个功能齐全的模拟部件工具包。
大约在 1999 年,OTI 使用 Java 实现了一个名为 VisualAge Micro Edition 的产品。VisualAge Micro Edition 的第一个版本使用了 Swing,而 OTI 使用 Swing 的经验并不积极。早期的 Swing 版本存在错误、时序和内存问题,当时的硬件也不够强大,无法提供可接受的性能。OTI 已成功为 Smalltalk-80 及其他 Smalltalk 实现构建了一个原生部件工具包,以提高 Smalltalk 的接受度。这段经验被用来构建 SWT 的第一个版本。VisualAge Micro Edition 和 SWT 都取得了成功,并且在 Eclipse 开发开始时,SWT 是自然的选择。在 Eclipse 中使用 SWT 而不是 Swing 导致了 Java 社区的分裂。有些人认为这是阴谋,但 Eclipse 取得了成功,而使用 SWT 使其与其他 Java 程序区别开来。Eclipse 性能出色,像素完美,人们普遍认为,“我不敢相信这是一个 Java 程序”。
早期的 Eclipse SDK 在 Linux 和 Windows 上运行。在 2010 年,它支持十多个平台。开发人员可以为一个平台编写应用程序,并将其部署到多个平台。为 Java 开发新的部件工具包当时在 Java 社区中是一个有争议的问题,但 Eclipse 提交者认为,为了在桌面上提供最佳的原生体验,付出这些努力是值得的。这一断言至今仍然适用,并且有数百万行代码依赖于 SWT。
JFace 是建立在 SWT 之上的一个层,它为常见的 UI 编程任务提供工具,例如首选项和向导框架。与 SWT 一样,它旨在与许多窗口系统一起工作。但是,它是纯 Java 代码,不包含任何原生平台代码。
该平台还提供了一个基于称为主题的小信息单元的集成帮助系统。主题由一个标签及其位置的引用组成。位置可以是 HTML 文档文件,也可以是描述其他链接的 XML 文档。主题在目录 (TOC) 中分组。可以将主题视为叶子,将 TOC 视为组织的分支。要向您的应用程序添加帮助内容,您可以为 org.eclipse.help.toc
扩展点做出贡献,就像下面的 org.eclipse.platform.doc.isv
plugin.xml
所做的那样。
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.0"?> <plugin> <!-- ===================================================================== --> <!-- Define primary TOC --> <!-- ===================================================================== --> <extension point="org.eclipse.help.toc"> <toc file="toc.xml" primary="true"> </toc> <index path="index"/> </extension> <!-- ===================================================================== --> <!-- Define TOCs --> <!-- ===================================================================== --> <extension point="org.eclipse.help.toc"> <toc file="topics_Guide.xml"> </toc> <toc file="topics_Reference.xml"> </toc> <toc file="topics_Porting.xml"> </toc> <toc file="topics_Questions.xml"> </toc> <toc file="topics_Samples.xml"> </toc> </extension>
Apache Lucene 用于索引和搜索在线帮助内容。在早期的 Eclipse 版本中,在线帮助作为 Tomcat Web 应用程序提供服务。此外,通过在 Eclipse 本身中提供帮助,您还可以使用帮助插件的子集来提供独立的帮助服务器。3
Eclipse 还提供团队支持,以与源代码存储库交互、创建补丁和其他常见任务。工作区提供了一组文件和元数据,用于在文件系统中存储您的工作。还有一个调试器用于跟踪 Java 代码中的问题,以及一个用于构建特定于语言的调试器的框架。
Eclipse 项目的目标之一是鼓励开源和商业消费者扩展该平台以满足他们的需求,而鼓励这种采用的方法之一是提供稳定的 API。API 可以被认为是指定应用程序行为的技术契约。它也可以被认为是社会契约。在 Eclipse 项目中,座右铭是,“API 永恒”。因此,在编写 API 时必须仔细考虑,因为它旨在无限期地使用。稳定的 API 是客户端或 API 消费者与提供者之间的契约。此契约确保客户端可以长期依赖 Eclipse 平台提供 API,而无需客户端进行痛苦的重构。良好的 API 也足够灵活,允许实现发展。
JDT 提供 Java 编辑器、向导、重构支持、调试器、编译器和增量构建器。编译器还用于内容辅助、导航和其他编辑功能。Eclipse 没有附带 Java SDK,因此用户需要选择在其桌面上安装哪个 SDK。为什么 JDT 团队编写了一个单独的编译器来在 Eclipse 中编译您的 Java 代码?他们从 VisualAge Micro Edition 获得了初始的编译器代码贡献。他们计划在编译器之上构建工具,因此编写编译器本身是一个合乎逻辑的决定。这种方法还允许 JDT 提交者为扩展编译器提供扩展点。如果编译器是由第三方提供的命令行应用程序,则这将很困难。
编写自己的编译器提供了一种机制,可以在 IDE 中提供对增量构建器的支持。增量构建器提供更好的性能,因为它只重新编译已更改的文件或其依赖项。增量构建器是如何工作的?当您在 Eclipse 中创建 Java 项目时,您将在工作区中创建资源以存储您的文件。Eclipse 中的构建器获取工作区中的输入(.java
文件),并创建输出(.class
文件)。通过构建状态,构建器了解工作区中的类型(类或接口)以及它们如何相互引用。每次编译源文件时,编译器都会向构建器提供构建状态。当调用增量构建时,构建器会收到一个资源增量,该增量描述任何新的、修改的或删除的文件。已删除的源文件将其对应的类文件删除。新的或修改的类型将添加到队列中。队列中的文件按顺序编译,并与旧的类文件进行比较,以确定是否存在结构性更改。结构性更改是对类的修改,可能会影响引用它的另一个类型。例如,更改方法签名或添加或删除方法。如果存在结构性更改,则引用它的所有类型也将添加到队列中。如果类型有任何更改,则新的类文件将写入构建输出文件夹。构建状态将使用已编译类型的引用信息进行更新。此过程将对队列中的所有类型重复,直到队列为空。如果存在编译错误,Java 编辑器将创建问题标记。多年来,JDT 提供的工具随着 Java 运行时本身的新版本而得到了极大的扩展。
插件开发环境 (PDE) 提供了开发、构建、部署和测试插件以及用于扩展 Eclipse 功能的其他工件的工具。由于 Eclipse 插件是 Java 世界中的一种新型工件,因此没有构建系统可以将源代码转换为插件。因此,PDE 团队编写了一个名为 PDE Build 的组件,该组件检查插件的依赖项并生成 Ant 脚本以构建构建工件。
Eclipse 3.0 可能是最重要的 Eclipse 版本之一,因为在此发布周期中发生了许多重大变化。在 3.0 之前的 Eclipse 架构中,Eclipse 组件模型由可以以两种方式相互交互的插件组成。首先,它们可以通过在其 plugin.xml
中使用 requires
语句来表达它们的依赖关系。如果插件 A 需要插件 B,则插件 A 可以看到来自 B 的所有 Java 类和资源,并尊重 Java 类可见性约定。每个插件都有一个版本,它们还可以指定其依赖项的版本。其次,组件模型提供了扩展和扩展点。从历史上看,Eclipse 提交者编写了自己的 Eclipse SDK 运行时来管理类加载、插件依赖关系以及扩展和扩展点。
Equinox 项目是在 Eclipse 中创建的一个新的孵化器项目。Equinox 项目的目标是用已存在的组件模型替换 Eclipse 组件模型,并提供对动态插件的支持。正在考虑的解决方案包括 JMX、Jakarta Avalon 和 OSGi。JMX 不是一个完全开发的组件模型,因此它被认为不合适。Jakarta Avalon 未被选中,因为它似乎正在失去作为项目的动力。除了技术要求外,考虑支持这些技术的社区也很重要。他们是否愿意合并 Eclipse 特定的更改?它是否正在积极开发并获得新的采用者?Equinox 团队认为,围绕他们最终选择的技术的社区与技术考虑一样重要。
在研究和评估了可用的替代方案后,提交者选择了 OSGi。为什么选择 OSGi?它具有用于管理依赖项的语义版本控制方案。它提供了一个 JDK 本身缺乏的模块化框架。可用于其他捆绑包的包必须显式导出,所有其他包都隐藏起来。OSGi 提供了自己的类加载器,因此 Equinox 团队不必继续维护自己的类加载器。通过标准化一个在 Eclipse 生态系统之外具有更广泛采用的组件模型,他们认为可以吸引更广泛的社区并进一步推动 Eclipse 的采用。
Equinox 团队感到放心,因为 OSGi 已经拥有一个现有的和充满活力的社区,他们可以与该社区合作,帮助包含 Eclipse 在组件模型中所需的特性。例如,当时,OSGi 仅支持在包级别列出需求,而不是 Eclipse 所需的插件级别。此外,OSGi 尚未包含片段的概念,这是 Eclipse 为现有插件提供特定于平台或环境的代码的首选机制。例如,片段提供用于使用 Linux 和 Windows 文件系统的代码,以及提供语言翻译的片段。一旦决定使用 OSGi 作为新的运行时,提交者就需要一个开源框架实现。他们评估了 Oscar(Apache Felix 的前身)和 IBM 开发的服务管理框架 (SMF)。当时,Oscar 是一个研究项目,部署有限。最终选择 SMF,因为它已用于交付产品,因此被认为是企业级的。Equinox 实现作为 OSGi 规范的参考实现。
还提供了一个兼容性层,以便现有的插件仍然可以在 3.0 安装中工作。要求开发人员重写他们的插件以适应 Eclipse 3.0 底层基础架构的变化将阻碍 Eclipse 作为工具平台的势头。Eclipse 用户的期望是该平台应该继续工作。
随着转向 OSGi,Eclipse 插件被称为捆绑包(bundles)。插件和捆绑包是同一件事:它们都提供了一个模块化的功能子集,并使用清单文件中的元数据进行自我描述。以前,依赖项、导出包以及扩展和扩展点都在plugin.xml
中描述。随着迁移到 OSGi 捆绑包,扩展和扩展点继续在plugin.xml
中描述,因为它们是 Eclipse 的概念。其余信息在META-INF/MANIFEST.MF
中描述,这是 OSGi 的捆绑包清单版本。为了支持此更改,PDE 在 Eclipse 中提供了一个新的清单编辑器。每个捆绑包都有名称和版本。org.eclipse.ui
捆绑包的清单如下所示
Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.ui; singleton:=true Bundle-Version: 3.3.0.qualifier Bundle-ClassPath: . Bundle-Activator: org.eclipse.ui.internal.UIPlugin Bundle-Vendor: %Plugin.providerName Bundle-Localization: plugin Export-Package: org.eclipse.ui.internal;x-internal:=true Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.2.0,4.0.0)", org.eclipse.swt;bundle-version="[3.3.0,4.0.0)";visibility:=reexport, org.eclipse.jface;bundle-version="[3.3.0,4.0.0)";visibility:=reexport, org.eclipse.ui.workbench;bundle-version="[3.3.0,4.0.0)";visibility:=reexport, org.eclipse.core.expressions;bundle-version="[3.3.0,4.0.0)" Eclipse-LazyStart: true Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0, J2SE-1.3
从 Eclipse 3.1 开始,清单还可以指定捆绑包所需的执行环境(BREE)。执行环境指定捆绑包运行所需的最低 Java 环境。Java 编译器不理解捆绑包和 OSGi 清单。PDE 提供了用于开发 OSGi 捆绑包的工具。因此,PDE 解析捆绑包的清单,并为该捆绑包生成类路径。如果您在清单中指定了 J2SE-1.4 的执行环境,然后编写了一些包含泛型的代码,您将收到有关代码中编译错误的提示。这确保您的代码符合您在清单中指定的约定。
OSGi 为 Java 提供了一个模块化框架。OSGi 框架管理自描述捆绑包的集合并管理它们的类加载。每个捆绑包都有自己的类加载器。捆绑包可用的类路径是通过检查清单的依赖项并生成捆绑包可用的类路径来构建的。OSGi 应用程序是捆绑包的集合。为了充分利用模块化,您必须能够以可靠的格式为使用者表达您的依赖项。因此,清单描述了可供此捆绑包的客户端使用的导出包,这对应于可供使用的公共 API。使用该 API 的捆绑包必须相应地导入它正在使用的包。清单还允许您为依赖项表达版本范围。查看上面清单中的Require-Bundle
标题,您会注意到org.eclipse.ui
依赖的org.eclipse.core.runtime
捆绑包必须至少为 3.2.0 且小于 4.0.0。
图 6.4:OSGi 捆绑包生命周期
OSGi 是一个动态框架,支持捆绑包的安装、启动、停止或卸载。如前所述,延迟激活是 Eclipse 的核心优势,因为插件类只有在需要时才会加载。OSGi 捆绑包生命周期也支持这种方法。当您启动 OSGi 应用程序时,捆绑包处于已安装状态。如果满足其依赖项,则捆绑包将更改为已解析状态。解析后,可以加载和运行该捆绑包中的类。启动状态意味着捆绑包正在根据其激活策略进行激活。激活后,捆绑包处于活动状态,它可以获取所需的资源并与其他捆绑包交互。当捆绑包正在执行其激活器停止方法以清理在激活时打开的任何资源时,它处于停止状态。最后,捆绑包可能会被卸载,这意味着它不可用于使用。
随着 API 的发展,需要一种方法来向使用者发出更改信号。一种方法是使用捆绑包的语义版本控制以及清单中的版本范围来指定依赖项的版本范围。OSGi 使用四部分版本命名方案,如图 6.5所示。
图 6.5:版本命名方案
使用 OSGi 版本编号方案,每个捆绑包都有一个唯一的标识符,该标识符由名称和四部分版本号组成。ID 和版本一起表示使用者的一组唯一的字节。根据 Eclipse 的约定,如果您要对捆绑包进行更改,则版本的每个部分都向使用者表明正在进行的更改类型。因此,如果您想表明您打算破坏 API,则增加第一个(主)部分。如果您只是添加了 API,则增加第二个(次要)部分。如果您修复了一个不影响 API 的小错误,则增加第三个(服务)部分。最后,增加第四个或限定符部分以指示构建 ID 源代码控制存储库标签。
除了表达捆绑包之间的固定依赖项外,OSGi 中还有一个名为服务的机制,它提供了捆绑包之间进一步的解耦。服务是一组具有属性的对象,这些属性已注册到 OSGi 服务注册表中。与在 Eclipse 启动期间扫描捆绑包时注册到扩展注册表中的扩展不同,服务是动态注册的。使用服务的捆绑包需要导入定义服务契约的包,并且框架从服务注册表中确定服务实现。
类似于 Java 类文件中的 main 方法,有一个特定的应用程序定义用于启动 Eclipse。Eclipse 应用程序是使用扩展定义的。例如,启动 Eclipse IDE 本身的应用程序是org.eclipse.ui.ide.workbench
,它在org.eclipse.ui.ide.application
捆绑包中定义。
<plugin> <extension id="org.eclipse.ui.ide.workbench" point="org.eclipse.core.runtime.applications"> <application> <run class="org.eclipse.ui.internal.ide.application.IDEApplication"> </run> </application> </extension> </plugin>
Eclipse 提供了许多应用程序,例如运行独立帮助服务器、Ant 任务和 JUnit 测试的应用程序。
在开源社区工作最有趣的事情之一是人们以完全意想不到的方式使用软件。Eclipse 的最初目的是提供一个平台和工具来创建和扩展 IDE。但是,在 3.0 版本发布之前的这段时间里,错误报告显示,社区正在采用平台捆绑包的一个子集并使用它们来构建富客户端平台 (RCP) 应用程序,许多人会将其识别为 Java 应用程序。由于 Eclipse 最初是围绕 IDE 为中心构建的,因此必须对捆绑包进行一些重构,以便用户社区更容易采用这种用例。RCP 应用程序不需要 IDE 中的所有功能,因此一些捆绑包被拆分为更小的捆绑包,社区可以使用这些捆绑包来构建 RCP 应用程序。
RCP 应用程序的实际示例包括使用 RCP 监控由美国宇航局喷气推进实验室开发的火星探测器机器人、用于生物信息学数据可视化的 Bioclipse 以及用于监控火车性能的荷兰铁路。这些应用程序中共同的主题是,这些团队决定他们可以利用 RCP 平台提供的实用程序,并专注于在其之上构建他们的专业工具。他们可以通过专注于在具有稳定 API 的平台上构建工具来节省开发时间和成本,该平台保证他们的技术选择将获得长期支持。
图 6.6:Eclipse 3.0 架构
查看图 6.6中的 3.0 架构,您会注意到 Eclipse 运行时仍然存在以提供应用程序模型和扩展注册表。管理组件之间的依赖项,插件模型现在由 OSGi 管理。除了能够继续为自己的 IDE 扩展 Eclipse 之外,使用者还可以基于 RCP 应用程序框架构建更通用的应用程序。
轻松将应用程序更新到新版本并添加新内容的能力被认为是理所当然的。在 Firefox 中,它可以无缝进行。对于 Eclipse 来说,这并不容易。更新管理器是最初用于向 Eclipse 安装添加新内容或更新到新版本的机制。
要了解在更新或安装操作期间发生了哪些变化,有必要了解 Eclipse 所谓的“功能”的含义。功能是 PDE 工件,它定义了一组捆绑包,这些捆绑包打包在一起,采用可以构建或安装的格式。功能还可以包含其他功能。(参见图 6.7。)
图 6.7:Eclipse 3.3 SDK 功能层次结构
如果您希望将 Eclipse 安装更新到仅包含一个新捆绑包的新版本,则必须更新整个功能,因为这是更新管理器使用的粗粒度机制。更新功能以修复单个捆绑包效率低下。
有一些 PDE 向导可以创建功能并在您的工作区中构建它们。feature.xml
文件定义了功能中包含的捆绑包以及捆绑包的一些简单属性。功能就像捆绑包一样,有名称和版本。功能可以包含其他功能,并为其包含的功能指定版本范围。功能中包含的捆绑包以及特定的属性将被列出。例如,您可以看到org.eclipse.launcher.gtk.linux.x86_64
片段指定了它应该使用的操作系统(os
)、窗口系统(ws
)和体系结构(arch
)。因此,升级到新版本,此片段将仅安装在此平台上。这些平台过滤器包含在此捆绑包的 OSGi 清单中。
<?xml version="1.0" encoding="UTF-8"?> <feature id="org.eclipse.rcp" label="%featureName" version="3.7.0.qualifier" provider-name="%providerName" plugin="org.eclipse.rcp" image="eclipse_update_120.jpg"> <description> %description </description> <copyright> %copyright </copyright> <license url="%licenseURL"> %license </license> <plugin id="org.eclipse.equinox.launcher" download-size="0" install-size="0" version="0.0.0" unpack="false"/> <plugin id="org.eclipse.equinox.launcher.gtk.linux.x86_64" os="linux" ws="gtk" arch="x86_64" download-size="0" install-size="0" version="0.0.0" fragment="true"/>
Eclipse 应用程序不仅仅包含功能和捆绑包。还有一些平台特定的可执行文件用于启动 Eclipse 本身、许可证文件和平台特定的库,如包含在 Eclipse 应用程序中的此文件列表所示。
com.ibm.icu org.eclipse.core.commands org.eclipse.core.conttenttype org.eclipse.core.databinding org.eclipse.core.databinding.beans org.eclipse.core.expressions org.eclipse.core.jobs org.eclipse.core.runtime org.eclipse.core.runtime.compatibility.auth org.eclipse.equinox.common org.eclipse.equinox.launcher org.eclipse.equinox.launcher.carbon.macosx org.eclipse.equinox.launcher.gtk.linux.ppc org.eclipse.equinox.launcher.gtk.linux.s390 org.eclipse.equinox.launcher.gtk.linux.s390x org.eclipse.equinox.launcher.gtk.linux.x86 org.eclipse.equinox.launcher.gtk.linux.x86_64
这些文件无法通过更新管理器更新,因为同样,它只处理功能。由于许多这些文件在每个主要版本中都会更新,这意味着用户每次发布新版本时都必须下载一个新的 zip 文件,而不是更新其现有安装。这对 Eclipse 社区来说是不可接受的。PDE 提供了对产品文件的支持,这些文件指定了构建 Eclipse RCP 应用程序所需的所有文件。但是,更新管理器没有一种机制可以将这些文件置于您的安装中,这对用户和产品开发人员来说都非常令人沮丧。2008 年 3 月,p2 作为新的供应解决方案发布到 SDK 中。为了向后兼容性,更新管理器仍然可用,但 p2 默认启用。
Equinox p2 全都与安装单元 (IU) 有关。IU 是您正在安装的工件的名称和 ID 的描述。此元数据还描述了工件的功能(提供的内容)及其要求(其依赖项)。如果工件仅适用于特定环境,则元数据还可以表达适用性过滤器。例如,org.eclipse.swt.gtk.linux.x86 片段仅在您在 Linux gtk x86 机器上安装时适用。从根本上说,元数据是捆绑包清单中信息的表达。工件只是正在安装的二进制位。通过分离元数据及其描述的工件,实现了关注点分离。p2 存储库包含元数据和工件存储库。
图 6.8:P2 概念
配置文件是安装中 IU 列表。例如,您的 Eclipse SDK 具有一个描述当前安装的配置文件。在 Eclipse 中,您可以请求更新到较新版本的构建,这将创建一个具有不同 IU 集的新配置文件。配置文件还提供与安装关联的属性列表,例如操作系统、窗口系统和体系结构参数。配置文件还存储安装目录和位置。配置文件由配置文件注册表保存,该注册表可以存储多个配置文件。目录负责调用供应操作。它与计划程序和引擎协同工作。计划程序检查现有配置文件,并确定必须执行哪些操作才能将安装转换为其新状态。引擎负责执行实际的供应操作并在磁盘上安装新的工件。触摸点是引擎的一部分,与正在安装的系统的运行时实现协同工作。例如,对于 Eclipse SDK,有一个 Eclipse 触摸点,它知道如何安装捆绑包。对于从 RPM 二进制文件安装 Eclipse 的 Linux 系统,引擎将处理 RPM 触摸点。此外,p2 可以执行进程内或进程外的安装,例如构建。
新的 p2 供应系统带来了许多好处。Eclipse 安装工件可以从一个版本更新到另一个版本。由于以前的配置文件存储在磁盘上,因此还可以恢复到以前的 Eclipse 安装。此外,给定一个配置文件和一个存储库,您可以重新创建报告错误的用户所使用的 Eclipse 安装,以便在您自己的桌面上重现该问题。使用 p2 进行供应提供了一种更新和安装不仅仅是 Eclipse SDK 的方法,它是一个也适用于 RCP 和 OSGi 用例的平台。Equinox 团队还与另一个 Eclipse 项目(Eclipse 通信框架 (ECF))的成员合作,为使用 p2 存储库中的工件和元数据提供可靠的传输。
当 p2 发布到 SDK 中时,Eclipse 社区内部进行了许多热烈的讨论。由于更新管理器对于供应 Eclipse 安装来说不是一个最佳的解决方案,因此 Eclipse 用户习惯于将捆绑包解压缩到其安装中并重新启动 Eclipse。这种方法以尽力而为的方式解决您的捆绑包。这也意味着安装中的任何冲突都在运行时解决,而不是安装时。约束应该在安装时解决,而不是运行时。但是,用户通常忽略这些问题,并假设由于捆绑包存在于磁盘上,因此它们正在工作。以前,Eclipse 提供的更新站点是一个简单的目录,包含 JAR 捆绑包和功能。一个简单的 site.xml
文件提供了可以在站点中使用的功能的名称。随着 p2 的出现,p2 存储库中提供的元数据变得更加复杂。要创建元数据,需要调整构建过程以在构建时生成元数据或对现有捆绑包运行生成器任务。最初,缺乏描述如何进行这些更改的文档。而且,一如既往,将新技术展现给更广泛的受众会暴露必须解决的意外错误。但是,通过编写更多文档并加班加点解决这些错误,Equinox 团队能够解决这些问题,现在 p2 是许多商业产品的底层供应引擎。此外,Eclipse 基金会每年都会使用所有参与项目的 p2 聚合存储库发布其协调版本。
必须不断检查体系结构以评估它是否仍然适用。它能否整合新技术?它是否鼓励社区发展?它是否易于吸引新贡献者?在 2007 年后期,Eclipse 项目提交者决定对这些问题的答案是否定的,并开始设计 Eclipse 的新愿景。同时,他们意识到有数千个 Eclipse 应用程序依赖于现有的 API。一个孵化器技术项目于 2008 年后期创建,具有三个具体目标:简化 Eclipse 编程模型、吸引新的提交者以及使平台能够利用新的基于 Web 的技术,同时提供开放的体系结构。
图 6.9:Eclipse 4.0 SDK 早期采用者版本
Eclipse 4.0 首次于 2010 年 7 月发布,供早期采用者提供反馈。它由 3.6 版本中的一部分 SDK 捆绑包和从技术项目毕业的新捆绑包组成。与 3.0 一样,存在一个兼容性层,以便现有捆绑包可以与新版本一起工作。与往常一样,有一个警告,即用户需要使用公共 API 才能确保该兼容性。如果您的捆绑包使用内部代码,则没有此类保证。4.0 版本提供了 Eclipse 4 应用程序平台,该平台提供了以下功能。
在 4.0 中,使用 Eclipse 建模框架 (EMFgc) 生成模型工作台。模型和视图的呈现之间存在关注点分离,因为渲染器与模型对话,然后生成 SWT 代码。默认情况下使用 SWT 渲染器,但其他解决方案也是可能的。如果您创建了一个示例 4.x 应用程序,则会为默认工作台模型创建一个 XMI 文件。可以修改模型,工作台将立即更新以反映模型中的更改。图 6.10 是为示例 4.x 应用程序生成的模型示例。
图 6.10:为示例 4.x 应用程序生成的模型
Eclipse 于 2001 年发布,早于可以通过 CSS 更改外观以提供不同外观和感觉的富互联网应用程序时代。Eclipse 4.0 提供了使用样式表轻松更改 Eclipse 应用程序外观和感觉的功能。默认 CSS 样式表可以在 org.eclipse.platform
捆绑包的 css
文件夹中找到。
Eclipse 扩展注册表和 OSGi 服务都是服务编程模型的示例。按照惯例,服务编程模型包含服务生产者和消费者。代理负责管理生产者和消费者之间的关系。
图 6.11:生产者和消费者之间的关系
传统上,在 Eclipse 3.4.x 应用程序中,消费者需要知道实现的位置,并了解框架中的继承才能使用服务。因此,消费者代码的可重用性较低,因为人们无法覆盖消费者接收的实现。例如,如果您想更新 Eclipse 3.x 中状态行上的消息,则代码将如下所示
getViewSite().getActionBars().getStatusLineManager().setMessage(msg);
Eclipse 3.6 由组件构建,但其中许多组件耦合过于紧密。为了组装由更松散耦合的组件组成的应用程序,Eclipse 4.0 使用依赖注入向客户端提供服务。Eclipse 4.x 中的依赖注入是通过使用自定义框架实现的,该框架使用作为通用机制来为消费者查找服务的上下文概念。上下文存在于应用程序和框架之间。上下文是分层的。如果上下文有一个无法满足的请求,它会将请求委托给父上下文。称为 IEclipseContext
的 Eclipse 上下文存储可用的服务并提供 OSGi 服务查找。基本上,上下文类似于 Java 映射,因为它提供名称或类到对象的映射。上下文处理模型元素和服务。模型的每个元素都将有一个上下文。服务在 4.x 中通过 OSGi 服务机制发布。
图 6.12:服务代理上下文
生产者将服务和对象添加到存储它们的上下文中。服务由上下文注入到消费者对象中。消费者声明它想要什么,上下文确定如何满足此请求。这种方法使使用动态服务变得更容易。在 Eclipse 3.x 中,消费者必须附加侦听器才能在服务可用或不可用时收到通知。使用 Eclipse 4.x,一旦上下文注入到消费者对象中,任何更改都会自动再次传递到该对象。换句话说,依赖注入再次发生。消费者通过使用符合 JSR 330 标准的 Java 5 注解(例如 @inject
)以及一些自定义 Eclipse 注解来指示它将使用上下文。支持构造函数、方法和字段注入。4.x 运行时扫描对象以查找这些注释。执行的操作取决于找到的注释。
上下文和应用程序之间这种关注点分离允许更好地重用组件,并使消费者免于理解实现。在 4.x 中,更新状态行的代码将如下所示
@Inject IStatusLineManager statusLine; ⋮ ⋮ ⋮ statusLine.setMessage(msg);
Eclipse 4.0 的主要目标之一是简化消费者的 API,以便于实现常见服务。简单服务的列表被称为“二十件事”,被称为 Eclipse 应用程序服务。目标是提供客户端可以使用且无需深入了解所有可用 API 的独立 API。它们被构建为单独的服务,以便它们也可以在 Java 以外的其他语言(如 Javascript)中使用。例如,有一个 API 可以访问应用程序模型,读取和修改首选项以及报告错误和警告。
Eclipse 的基于组件的体系结构已经发展到可以整合新技术,同时保持向后兼容性。这代价高昂,但回报是 Eclipse 社区的增长,因为建立了用户可以继续基于稳定 API 发布产品的信任。
Eclipse 有如此多的用户,他们有着不同的用例,我们广泛的 API 变得难以被新用户采用和理解。回想起来,我们应该让我们的 API 更简单。如果 80% 的用户只使用 20% 的 API,那么就需要简化,这是创建 Eclipse 4.x 流的原因之一。
群众的智慧确实揭示了一些有趣的用例,例如将 IDE 分解成可以用来构建 RCP 应用程序的捆绑包。相反,用户经常会提出很多关于边缘情况场景的请求,这些请求需要花费大量时间来实现。
在 Eclipse 项目的早期,提交者有幸能够将大量时间投入到文档、示例和回答社区问题上。随着时间的推移,这项责任已转移到整个 Eclipse 社区。我们本可以在提供文档和用例以帮助社区方面做得更好,但鉴于每个版本计划的项目数量众多,这非常困难。与软件发布日期推迟的预期相反,在 Eclipse 中,我们始终如期交付我们的版本,这使得我们的用户可以相信他们也能做到这一点。
通过采用新技术,并重新设计 Eclipse 的外观和工作方式,我们继续与我们的用户进行对话,并让他们参与到社区中。如果您有兴趣参与 Eclipse,请访问 http://www.eclipse.org。
http://www.eclipse.org
http://www.eclipse.org/equinox
http://help.eclipse.org
。