Kim Moir
实现软件模块化是一项众所周知的艰巨任务。与由不同社区编写的庞大代码库的互操作性也很难管理。在Eclipse,我们在这两方面都取得了成功。2010 年 6 月,Eclipse 基金会发布了 Helios 协调版本,来自 40 多家公司的 39 个项目和 490 名提交者共同合作,在基础平台的功能基础上进行开发。Eclipse最初的架构愿景是什么?它是如何发展的?应用的架构如何起到鼓励社区参与和发展的作用?让我们回到最开始。
2001年11月7日,一个名为Eclipse 1.0的开源项目发布。当时,Eclipse被描述为"一个综合开发环境(IDE),适用于任何事物,也适用于任何具体事物"。这种描述是故意通用的,因为其架构愿景不仅仅是另一套工具,而是一个框架;一个模块化和可扩展的框架。Eclipse提供了一个基于组件的平台,可以作为开发人员构建工具的基础。这种可扩展的架构鼓励社区在核心平台的基础上,将其扩展到最初愿景的极限之外。Eclipse以平台的形式开始,Eclipse SDK是概念验证产品。Eclipse SDK允许开发者自我托管并使用Eclipse SDK本身来构建较新版本的Eclipse。
开放源码开发者的刻板印象是,一个利他主义者为了解决自己的个人利益,辛辛苦苦到深夜修复bug,实现奇妙的新功能。相反,如果你回顾Eclipse项目的早期历史,一些最初捐赠的代码是基于IBM开发的VisualAge for Java。第一批从事这个开源项目的提交者是IBM一家名为Object Technology International(OTI)的子公司的员工。这些投入者有偿全职参与这个开源项目,回答新闻组的问题,解决错误,实现新功能。一个由感兴趣的软件供应商组成的联盟成立了,以扩大这种开放工具的努力。Eclipse 联盟的最初成员是 Borland、IBM、Merant、QNX 软件系统公司、Rational 软件公司、RedHat、SuSE 和 TogetherSoft。
通过对这一努力的投资,这些公司将拥有基于Eclipse的商业产品的专业知识。这类似于企业对Linux内核的投资,因为让员工改进作为其商业产品基础的开源软件符合他们的自身利益。2004年初,Eclipse基金会成立,以管理和扩大不断发展的Eclipse社区。这个非营利性的基金会由企业会员会费资助,并由董事会管理。今天,Eclipse社区的多样性已经扩大到包括170多家成员公司和近1 000名承诺者。
最初,人们对"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版本。
<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>
<requirements>
<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"/>。
requirements>
plugin>
为了鼓励人们在Eclipse平台的基础上进行构建,需要有一种机制来为平台做出贡献,并让平台接受这种贡献。这是通过使用扩展和扩展点来实现的,扩展点是Eclipse组件模型的另一个元素。出口确定了你希望别人在编写他们的扩展时使用的接口,这就限制了你的插件之外的类可以使用到那些被导出的类。它还对插件外部可用的资源提供了额外的限制,而不是将所有公共方法或类都提供给消费者。导出的插件被认为是公共API。所有其他的都被认为是私有的实现细节。要编写一个能为Eclipse工具栏贡献菜单项的插件,可以使用org.eclipse.ui插件中的actionSets扩展点。
<extension-point id="actionSets" name="%ExtPoint.actionSets"
schema="schema/actionSets.exsd"/>
<extension-point id="command" name="%ExtPoint.command"
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="schemma/dropActions.exsd"/>=
你的插件扩展可以为org.eclipse.ui.actionSet扩展点贡献一个菜单项,它看起来像这样。
<plugin
id="com.example.helloworld"
name="com.example.helloworld"
版本="1.0.0">。
<runtime>
<library name="helloworld.jar"/>。
runtime>
<requires>
<import plugin="org.eclipse.ui"/>。
requires>
<extension
point="org.eclipse.ui.actionSets">。
<actionSet
label="示例行动集"
visible="true"
id="org.eclipse.helloworld.actionSet">。
<menu
label="示例&菜单"
id="exampleMenu">
<separator
name="exampleGroup">
separator>
menu>
<action
label="&示例动作"
icon="icon/example.gif"
tooltip="你好,Eclipse世界"
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构造函数就会被运行,然后初始化一个工作台动作委托。由于工作台中的选择已经发生了变化,并且已经创建了委托人,所以可以改变动作。打开消息对话框,显示"你好,Eclipse架构世界"。
这种可扩展的架构是Eclipse生态系统成功发展的关键之一。公司或个人可以开发新的插件,并将其作为开放源码发布或进行商业销售。
关于Eclipse最重要的一个概念是,一切都是插件。无论是Eclipse平台中包含的插件,还是你自己编写的插件,插件都是组装好的应用程序的一级组件。图6.3显示了Eclipse早期版本中插件所贡献的相关功能集群。
<plugin>
<extension
point="org.eclipse.help.toc">
<toc
file="toc.xml"
primary="true">
toc>
<index path="index"/>
extension>
<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是客户端或API消费者与提供者之间的合同。这种合同确保了客户端可以长期依赖Eclipse平台提供API,而不需要客户端进行痛苦的重构。一个好的API也是足够灵活的,可以让实现不断发展。
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.
捆绑-本地化:插件
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)";可见性:=reexport。
org.eclipse.jface;bundle-version="[3.3.0,4.0.0)";可见性:=reexport。
org.eclipse.ui.workbench;bundle-version="[3.3.0,4.0.0)";visibility:=reexport.bundle-version="[3.3.0,4.0.0)"。
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开始,清单还可以指定 bundle所需的执行环境(BREE)。执行环境指定了 bundle 运行所需的最小 Java 环境。Java编译器不理解捆绑包和OSGi清单。PDE提供了开发OSGi bundle的工具。因此,PDE 会解析 bundle 的清单,并为该 bundle 生成 classpath。如果你在manifest中指定了J2SE-1.4的执行环境,然后写了一些包含generics的代码,你会被告知你的代码中存在编译错误。这可以确保你的代码遵守你在清单中指定的合同。
OSGi为Java提供了一个模块化框架。OSGi框架管理自描述捆绑的集合,并管理它们的类加载。每个捆绑包都有自己的类加载器。捆绑包可用的classpath是通过检查manifest的依赖关系来构造的,并生成捆绑包可用的classpath。OSGi应用程序是捆绑的集合。为了充分拥抱模块化,你必须能够以可靠的格式为消费者表达你的依赖关系。因此,manifest描述的是这个bundle的客户端可用的导出包,它对应的是可供消费的公共API。正在消费该API的捆绑包必须有一个对应的他们正在消费的包的导入。manifest还允许你表达你的依赖的版本范围。看上面清单中的Require-Bundle标题,你会注意到org.eclipse.core.runtime bundle所依赖的org.eclipse.ui必须至少是3.2.0,小于4.0.0。
<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测试的应用程序。
<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
这些文件不能通过更新管理器更新,因为它只处理功能。由于这些文件中的许多文件在每一个主要的版本都会更新,这意味着每次有新的版本发布时,用户都必须下载一个新的压缩包,而不是更新他们现有的安装。这对Eclipse社区来说是不可接受的。PDE提供了对产品文件的支持,它指定了构建Eclipse RCP应用程序所需的所有文件。然而,更新管理器并没有一个机制来提供这些文件到你的安装中,这对用户和产品开发者来说都是非常令人沮丧的。2008年3月,p2作为新的供应解决方案被发布到SDK中。为了向后兼容,Update Manager仍然可以使用,但p2是默认启用的。
6.3.1. p2概念
Equinox p2是关于安装单元(IU)的。IU是对你要安装的工件的名称和ID的描述。这个元数据也描述了工件的能力(提供了什么)和需求(它的依赖性)。元数据还可以表达适用性过滤器,如果一个工件只适用于特定的环境。例如,org.eclipse.swt.gtk.linux.x86片段只有在Linux gtk x86机器上安装时才适用。从根本上说,元数据是捆绑清单中信息的表达。人造物只是被安装的二进制位。通过分离元数据和它们所描述的工件来实现关注点的分离。一个p2资源库由元数据和工件资源库组成。
图6.8:P2概念
配置文件是您安装中的 IU 列表。例如,您的 Eclipse SDK 有一个描述您当前安装的 profile。在 Eclipse 中,您可以请求更新到较新版本的构建,这将创建一个具有不同 IU 集的新配置文件。配置文件还提供了与安装相关的属性列表,例如操作系统、窗口系统和架构参数。配置文件还存储了安装目录和位置。配置文件由一个配置文件注册表持有,该注册表可以存储多个配置文件。导演负责调用供应操作。它与规划师和引擎一起工作。规划师检查现有的配置文件,并确定将安装转化为新状态所必须进行的操作。引擎负责执行实际的供应操作,并将新工件安装到磁盘上。触点是引擎的一部分,与被安装系统的运行时实现一起工作。例如,对于Eclipse SDK,有一个Eclipse接触点,它知道如何安装捆绑包。对于从RPM二进制文件中安装Eclipse的Linux系统,引擎会处理一个RPM接触点。此外,p2还可以在进程内或进程外的单独进程中执行安装,例如构建。
新的p2供应系统有很多好处。Eclipse安装工件可以从一个版本更新到另一个版本。由于以前的配置文件存储在磁盘上,所以也有办法恢复到以前的Eclipse安装。此外,给定一个配置文件和一个存储库,你可以重新创建报告错误的用户的Eclipse安装,尝试在自己的桌面上重现问题。使用p2提供了一种更新和安装的方法,而不仅仅是Eclipse SDK,它是一个适用于RCP和OSGi用例的平台。Equinox团队还与另一个Eclipse项目的成员合作,即Eclipse Communication Framework(ECF),为消费p2仓库中的工件和元数据提供可靠的传输。
当p2发布到SDK中时,Eclipse社区内有许多热烈的讨论。由于update manager对于Eclipse安装来说是一个不太理想的解决方案,Eclipse消费者习惯于将捆绑包解压缩到他们的安装中,然后重新启动Eclipse。这种方法在尽力的基础上解决了你的捆绑包。这也意味着你安装中的任何冲突都是在运行时解决的,而不是在安装时解决。约束应该在安装时解决,而不是在运行时解决。然而,用户往往对这些问题熟视无睹,并认为既然捆绑包存在于磁盘上,那么它们就可以工作。以前,Eclipse提供的更新站点是一个由JARred bundles和特性组成的简单目录。一个简单的site.xml文件提供了站点中可供消耗的特性的名称。随着p2的出现,p2仓库中提供的元数据要复杂得多。为了创建元数据,需要对构建过程进行调整,以便在构建时生成元数据,或者在现有的捆绑包上运行一个生成器任务。起初,缺乏描述如何进行这些改变的文档。同样,如同往常一样,向更多的人展示新技术会暴露出意想不到的bug,这些bug必须得到解决。然而,通过编写更多的文档和长时间的工作来解决这些bug,Equinox团队能够解决这些问题,现在p2是许多商业产品背后的底层供应引擎。以及,Eclipse基金会每年都会使用所有贡献项目的p2聚合仓库来运送其协调发布。
6.4.6.4. Eclipse 4.0
必须不断地检查架构,以评估它是否仍然合适。它是否能够纳入新技术?它是否能鼓励社区的发展?它是否容易吸引新的贡献者?2007年末,Eclipse项目的提交者们决定,这些问题的答案都是否定的,他们开始为Eclipse设计新的愿景。同时,他们意识到,有成千上万的Eclipse应用依赖于现有的API。2008年底,一个孵化器技术项目应运而生,有三个具体目标:简化Eclipse编程模型,吸引新的提交者,使该平台能够利用新的网络技术,同时提供一个开放的架构。
图6.9:Eclipse 4.0 SDK早期采用者版本
Eclipse 4.0于2010年7月首次发布,供早期采用者提供反馈。它由3.6版本中的SDK捆绑包和技术项目中的新捆绑包组成。和3.0一样,有一个兼容层,这样现有的捆绑包就可以和新版本一起使用。和以往一样,有一个警告,即消费者需要使用公共API,以确保兼容性。如果你的捆绑包使用的是内部代码,就没有这样的保证。4.0版本提供了Eclipse 4应用平台,它提供了以下功能。
6.4.1.模型工作台
在4.0中,使用Eclipse建模框架(EMFgc)生成了一个模型工作台。模型和视图的渲染之间的关注点是分离的,因为渲染器与模型对话,然后生成SWT代码。默认情况下是使用SWT渲染器,但也可以使用其他解决方案。如果您创建一个4.x应用程序示例,将为默认的工作台模型创建一个XMI文件。该模型可以被修改,工作台将被立即更新以反映模型的变化。图 6.10 是为 4.x 示例应用程序生成模型的一个例子。
图6.10:为例4.x应用生成的模型。
6.4.2.层叠样式表的样式
Eclipse发布于2001年,当时还没有出现丰富的互联网应用程序时代,这些应用程序可以通过CSS提供不同的外观和感觉的皮肤。Eclipse 4.0提供了使用样式表来轻松改变Eclipse应用程序的外观和感觉的能力。默认的CSS样式表可以在org.eclipse.platform bundle的css文件夹中找到。
6.4.3.依赖性注入
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中,依赖注入是通过使用一个自定义框架来实现的,该框架使用上下文的概念作为一种通用机制来为消费者定位服务。上下文存在于应用程序和框架之间。上下文是有层次的。如果一个上下文有一个不能满足的请求,它将把这个请求委托给父上下文。Eclipse上下文被称为IEclipseContext,存储可用的服务,并提供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);