在计算需求日益复杂的当今世界,我们作为软件开发人员,不断寻求最终的、通用的架构,使我们能够高效地开发高质量的应用程序。这种追求促使我们采用了许多新的抽象概念和工具。最近最有希望的进展之一是新的纯插件架构。
最初只是扩展应用程序的回调机制,现在已成为应用程序本身的基础。插件不再仅仅是应用程序的附加组件;今天的应用程序完全由插件构成。在过去几年中,这个领域已经相当成熟,许多成功的项目做出了重大贡献。
本文旨在探讨围绕纯插件架构基础的一些概念,以及在采用插件方法时,这些概念如何影响各种利益相关者。本文的目的是讨论所涉及的一般问题,但也介绍了从 Eclipse (www.eclipse.org) 中吸取的一些经验教训,以便更好地说明某些概念。
客户的需求控制着软件的创建和部署。客户需要更多更好的功能,他们希望功能能够根据自己的需求进行定制,并且他们希望“尽快”获得这些功能。通常,大型企业更喜欢开发自己的内部附加组件,或调整和替换现有功能。没有人想重新发明轮子,而是希望通过编写使其与竞争对手区分开来的专业代码,来集成并构建在现有工作的基础上。较新的企业级应用程序套件由较小的独立产品组成,这些产品必须集成在一起才能产生预期的高级功能,同时提供一致的用户体验。快速响应需求快速变化、可升级性以及随时支持集成其他供应商组件的能力,都为灵活和可扩展的应用程序创造了额外的推动力。
在实际工作中,开发人员必须处理复杂的基础设施、工具和代码。他们最不需要的就是在已经很复杂的代码库上再打补丁,以便营销部门可以面不改色地销售产品。新的插件架构对开发人员非常有吸引力,因为他们可以专注于为用户提供模块化功能。任何人都可以通过混合和匹配他们需要的插件来快速自定义应用程序,或者为缺失的功能编写新的插件。这种灵活性的代价是管理这些插件。我们将在本文后面讨论这个问题和其他问题,您可以自行判断是否值得付出这个代价。
大多数人熟悉传统插件,即可下载的软件包,它们扩展了 Web 浏览器或文本和图形编辑器等宿主应用程序的功能。它们没有被编译到应用程序中,而是通过定义良好的接口和扩展机制链接在一起。通常,构建插件不需要访问应用程序的源代码。插件实现了宿主应用程序可以识别并在需要时激活的功能。
在新的纯插件架构中,一切皆为插件。宿主应用程序的角色被简化为运行插件的运行时引擎,不具有固有的最终用户功能。在没有指令宿主应用程序的情况下,剩下的是一个由联邦插件组成的世界,所有插件都按照框架和/或插件自身定义的约定规则运行。
为了支持组成一个未预先构建的更大系统,或以您无法预见的方式扩展它,该架构必须支持插件通过插件进行扩展。在没有宿主应用程序的情况下,插件本身通过提供定义良好的挂钩点来成为其他插件的宿主,其他插件可以在这些挂钩点添加功能。这些挂钩点通常称为扩展点。当插件为扩展点贡献实现时,我们称之为添加扩展。与定义任何合同义务非常相似,扩展模型为插件提供了一种结构化的方式来描述它们可以被扩展的方式,以及客户端插件描述它们提供的扩展的方式。
简化来说,图 1 说明了传统插件架构和纯插件架构之间的主要结构差异。
如前所述,插件不是独立的程序;它们的执行由运行时引擎协调。插件运行时引擎——我们称之为内核——是用户启动应用程序时启动的核心引擎。内核至少必须为基本插件基础设施提供运行时支持,具体包括:
• 查找、加载和运行正确的插件代码。
• 维护已安装插件及其提供的功能的注册表。
• 管理插件扩展模型和插件间依赖关系。
可选地,内核可以提供其他实用程序服务,例如日志记录、跟踪或安全性。用户还应该能够关闭或删除应用程序的某些部分,或动态安装新功能。出于显而易见的原因,我们希望内核小而简单,但足够健壮,可以在其之上构建工业强度功能,而无需太多(或任何)黑客行为。
为了说明插件环境的基本工作原理,让我们深入了解 Eclipse 插件运行时结构和控制流。虽然 Eclipse 主要以其强大的 Java 集成开发环境而闻名,但实际上它是一种通用的插件架构,用于创建“任何东西,但又不是特定的东西”。运行时引擎本身被实现为许多核心插件,除了少量引导代码。这段代码启动核心插件以初始化插件注册表和扩展模型,并解析插件依赖关系。除了核心插件之外,此时没有运行其他插件代码。所有需要的插件元数据都从插件清单文件(plugin.xml 和/或 manifest.mf)中读取。
一旦初始化完成,运行时内核就可以运行应用程序了。扩展机制(如图 2 所示)开始发挥作用,并且整个应用程序从内向外逐步构建,如下所示:
1. 运行时核心插件定义了一个应用程序扩展点,以便任何插件都可以通过为其贡献扩展来声明自身为应用程序。
2. 应用程序扩展必须为扩展点的回调接口 IPlatformRunnable 提供具体的实现。
3. Eclipse 的默认应用程序扩展由 IDE 插件贡献,通过提供一个具体的实现类,当扩展点处理代码调用该类时,它会创建工作台图形用户界面并运行事件循环直到退出。
这个工作台窗口看起来像任何标准应用程序:它有菜单、工具栏、视图和编辑器,您可以启动向导、打开首选项对话框等等。然而,基本工作台本身只是一个用于显示可视化部件和其他用户界面元素的框架。它的强大之处在于它定义的扩展点(例如,视图、向导、编辑器、操作集、帮助支持),其他插件或工作台插件本身可以为其贡献扩展。
一般来说,定义扩展点的插件负责查找其扩展器(使用扩展注册表),实例化相关的回调类,并调用适当的接口方法。在底层,运行时内核负责提供扩展注册表查找功能,为扩展器插件创建类加载器,加载它并启动它。这确保了扩展器插件在创建和运行其类之前被初始化并处于活动状态。
最后,值得注意的是,某些扩展点可能需要多个回调接口,而另一些则不需要任何回调接口。例如,帮助系统扩展点 toc 用于贡献文档目录,不需要任何回调。当调用帮助时,帮助插件会处理 toc 扩展,方法是从所有贡献插件创建一个更大的、集成的目录。
部署插件系统时的一个重要考虑因素是配置和发现插件。与通常安装在用户机器上的一个文件夹中的单体应用程序不同,基于插件的应用程序在安装布局和插件发现方面可能具有更高的自由度。这种灵活性也可能成为安装程序和插件管理的主要难题。
考虑一个多用户共享安装,整个产品由中央管理员代表用户社区安装。个人用户无需执行任何安装。他们只需获得一个本地“快捷方式”来调用共享安装(快捷方式可以是命令行调用或封装命令行调用的真实桌面快捷方式的形式)。如果您像大多数用户一样,您会想要安装一些其他很酷的新功能,尤其是当第三方插件产品种类繁多(且通常免费)时。显然,新插件(用户私有的)无法安装在只读的共享安装位置,因此产品应允许用户在他们具有更多权限的位置安装和配置额外的插件。
仅根据访问标准(例如用户角色)为特定用户配置插件子集会增加更多挑战。在许多应用程序在同一台机器上运行并共享一组通用插件,或者某些插件未在本地安装而是从远程位置(例如,应用程序服务器提供商)运行时,会出现其他配置问题。
理论上,平台必须自行查找可用的插件,并优雅地处理丢失的插件或动态来来去去的插件。在实践中,许多针对这些问题的解决方案都涉及关键配置文件或插件本身的预定义文件和目录位置。更高级的插件系统提供可插拔的配置和插件发现机制。通常,会运行一些基本的引导代码来启动配置器插件,然后配置器插件将根据其自身的策略发现其他插件。例如,Eclipse 提供了一个 Update Manager 配置器插件,它可以从 eclipse/plugins 文件夹以及从 eclipse/links 文件夹链接的其他本地插件文件夹或用户将新插件安装到他们选择的位置时动态添加的插件文件夹中获取插件。由于配置器是可插拔的,因此任何人都可以插入另一个配置器,该配置器可以从远程服务器配置和配置插件。
在时间和预算的压力下,或者受到花哨的营销说辞的诱惑,您可能会想采用纯插件架构,而没有意识到其潜在的缺陷。或者,您可能真的相信墨菲定律,因此不会使用插件,因为它们注定要失败。我倾向于追随那些走中间道路的人:如果某件事可能会失败,那么首先应该理解它,然后再修复它。本文的其余部分介绍了在您使用插件时可能构成一些挑战的问题,这些问题将帮助您在评估插件架构时提出问题或提供答案。
安装和更新。许多现代产品可以自动检测到它们相对于可用的服务或产品版本而言是否已过时。无论是在启动时,还是作为显式更新操作的结果,产品都会将当前的安装级别与一些基于网络的基线进行比较。然后,产品会自动下载所需的修复程序或升级程序并应用它们,通常作为产品执行的一部分。此外,用户可以而且通常会从各种来源安装额外的插件,以扩展其当前应用程序提供的功能。
对于基于插件的应用程序,安装和更新过程可能是一场真正的噩梦:一方面,存在任何应用程序中都会出现的传统安装问题——回滚更改、迁移现有程序数据和首选项或确保安装未损坏的能力。另一方面,由于插件可能来自彼此无关的各种提供商,因此最终配置可能从未经过测试。这带来了一些有趣的挑战,我们将在下一节讨论的其他问题的背景下解决这些挑战。
安全性。系统永远不会太安全,您需要特别注意保护基于插件的系统。由于可以安装任意插件——例如,通过从 Web 下载它们——并且允许无限制地访问它们插入的系统,因此必须仔细规划插件环境中的安全性。除此之外,某些插件还需要支持在安装期间执行自定义安装代码,以便它们可以控制其安装的某些部分。为了防止软件安全事故或故障,插件框架必须解决从第三方下载以及控制插件对其他代码和数据的访问的问题。支持数字签名的插件或安全连接会有所帮助,但这仍然依赖于信任下载源或插件提供商。某些编程环境(例如 Java)提供了内置的运行时安全机制,可以有效地用于弥合某些安全漏洞。
冷酷的现实是,除非您小心安装的内容,否则几乎不可能确信已安装的插件不是恶意的。当然,纯插件架构也意味着,即使是一个善意的插件,如果存在严重的错误,也可能在安装后造成与恶意插件一样多的损害。
并发插件版本支持。毫无疑问,管理并发插件版本和依赖关系是那些可能让架构师、开发人员和安装人员夜不能寐的问题之一。你们中的大多数人可能在某个时候都经历过“DLL 地狱”,并且会怀疑某些有潜力成为“插件地狱”的东西。
任何严肃的插件模型都定义了一些版本控制方案,但允许并发安装和执行插件的多个版本是一个必须在设计早期考虑的问题,并且必须通过安装和更新过程正确强制执行。
为了说明这个领域的复杂性,让我们首先考虑一个应用程序套件的场景,该套件集成了两个独立的插件产品,每个产品都安装在不同的位置。可能在两个位置安装了相同的插件,版本相同或版本不同。运行时内核必须通过运行这两个插件实例或删除一个插件实例来处理这两个插件实例。当两个版本都在运行时,并且插件贡献了用户界面元素(例如菜单或首选项页面),则结果可能是非常混乱的界面,其中包含重复的菜单项或首选项页面。还有更多麻烦:用户是否需要面对这种异常?您将如何更新或卸载这些插件?
插件版本在此处扮演的另一个重要角色放大了问题的难度:它们是插件依赖关系规范的一部分。通常,插件需要其他插件提供的功能,并且通常对所需版本有严格的标准。例如,插件可能需要另一个插件贡献的特定级别的 XML 解析器,或者它可能需要仅对特定版本的扩展点贡献扩展。版本依赖可以是固定版本号(例如 3.0.1)的形式,也可以是版本范围的形式——例如,3.0.1 或更高版本。在运行时正确解析插件依赖关系对于应用程序的正确运行至关重要。管理插件依赖关系图对于确保安装或更新插件后的一致配置状态,以及正确回滚更改也至关重要。安装/更新过程可能需要中止导致未解析依赖关系的插件的安装,或者搜索并安装缺少的插件。无论哪种方式,都可能最终得到同一插件的多个版本,因此我们又回到了管理并发版本的问题。
对于管理插件的并发版本,没有简单、通用的解决方案。Eclipse 针对并发插件版本采用了一种合理的权衡约定:只有贡献代码库但不贡献插件扩展(没有用户界面贡献,没有文档等等)的插件版本才允许在同一运行时实例中共存。对于所有其他插件,通常会选择最新版本,除非配置文件精确定义了要运行的内容。这种方法的主要优点是,它仍然允许同一代码的各种级别在运行时共存,但对最终用户隐藏,最终用户将获得一致的用户界面。缺点是,这不会涵盖所有用户场景,并且插件在 Eclipse 中的处理方式与其他地方不同。插件的这种特殊处理不仅是一个运行时/安装问题,而且也是开发人员和产品打包人员必须考虑的问题。
可伸缩性,向上和向下。使用插件架构的另一个挑战是可伸缩性。正如与多个版本和依赖关系相关的问题可能会迅速升级一样,交互插件的庞大数量也可能很快成为一个问题。例如,当 Eclipse 首次设计时,人们认为一个产品(一个大型产品)将由数百个插件组成。在几个版本之后,已知某些基于 Eclipse 构建的企业级产品已超过一千个插件的标记,因此平台目标已重新修订为支持在 5,000 到 10,000 个插件之间进行伸缩。
在设计用于可伸缩性的插件系统时,开发人员必须考虑各种机制,这些机制可以加快启动速度并减少内存占用。运行时的一般原则是,最终用户不应为已安装但未使用的插件支付内存或性能代价。插件可以安装并添加到注册表中,但除非根据用户的活动请求插件提供的功能,否则插件不会被激活。一般来说,这需要支持插件声明式功能。这通常通过插件清单文件在实践中实现,因此无需加载任何代码即可获得插件贡献的功能。缓存插件注册表和插件清单/声明可以减少后续应用程序启动期间的处理,从而缩短响应时间。然而,在性能和内存占用方面获得的收益,在代码复杂性方面却会丢失:需要编写更多代码来缓存数据并将缓存与安装配置中的更改同步。
可伸缩性问题几乎总是在插件安装/更新操作期间浮出水面。产品越大,补丁和升级的下载大小就越大。对于产品升级,可以通过仅下载已更改的插件来缩短下载时间。在交互式安装/更新操作期间,可能会出现另一个安装可伸缩性问题。通常,出于开发原因(例如功能重用)建立插件边界,并且在用户将功能单元视为安装单元方面,插件边界呈现了错误的粒度级别。考虑到企业级应用程序可以扩展到数百个甚至数千个插件,并且具有复杂的依赖关系,用户可能会被各种安装选择淹没。一个可能的解决方案是引入一个打包和安装组件,该组件将多个插件组合在一起以提供更高级别的功能。例如,在 Eclipse 中,Update Manager 不直接安装插件;它处理功能。功能是插件的捆绑包,被认为是具有安装/更新语义的部署单元。
可伸缩性的另一方面是向下伸缩,以便产品可以在资源有限的设备(例如手机和 PDA)上运行。这通常意味着重新思考核心框架插件,将其重构为更小的插件,其中一些插件是可选部署的,以便可以使用最少的配置运行。
除非您是插件框架的创建者或计划成为插件框架的创建者,或者您为了乐趣和利润而开发插件,或者您正在使用基于插件的应用程序,否则关于插件架构和设计问题的一般讨论对您或您公司的盈亏可能没有太大意义。使用插件框架并非没有障碍,但可以通过适当的准备来克服这些障碍。
虽然传统插件已经存在一段时间了,但纯插件模型直到最近才作为健壮的企业级质量应用程序开发环境出现。随着许多主要的行业参与者正在迅速采用插件技术来开发其软件产品线,我们可以预期会进一步研究和开发以改进当前的架构。以下是一些将受到更多关注的领域:
• 所有级别的安全性,包括安装、更新和运行时。
• 性能:速度和低资源使用率。
• 改进的插件开发、测试、打包和部署工具。
• 改进的安装和更新,尤其是通过合并更改而不关闭系统。
• 各种流程的远程管理(配置、配置等等)。
• 各种插件框架的融合、兼容性或互操作性。
• 更广泛的部署平台(从台式 PC 和高端服务器到数字移动电话和嵌入式设备)。
越来越多的努力将在开源领域或标准机构中进行。
喜欢它,讨厌它?请告诉我们
[email protected] 或 www.acmqueue.com/forums
DORIAN BIRSAN 在 IBM 多伦多实验室工作了近 10 年,领导了一系列关于应用程序开发工具的技术项目,并在该领域撰写了多项专利。自 Eclipse 成立以来,他一直在其中发挥积极作用,领导用户帮助和更新/安装团队。他是一位数学家出身,后来成为计算机科学家,再后来成为丈夫和父亲,他拥有滑铁卢大学计算机科学与组合数学和优化专业的 B.Math 学位,以及不列颠哥伦比亚大学计算机科学专业的 M.Sc. 学位,在那里他是加拿大国家科学与工程研究委员会的研究员。
© 2005 1542-7730/05/0300 $5.00
最初发表于 Queue vol. 3, no. 2—
在 数字图书馆 中评论本文
David Chisnall - 如何设计 ISA
在过去的十年中,我参与了多个项目,这些项目为各种类型的处理器设计了 ISA(指令集架构)扩展或全新的 ISA(您甚至可以在 RISC-V 规范的致谢中找到我的名字,追溯到第一个公开版本)。当我开始时,我对什么构成好的 ISA 知之甚少,而且,据我所知,这在任何地方都没有正式教授。
Gabriel Falcao, João Dinis Ferreira - PiM 还是非 PiM
随着人工智能成为数十亿边缘 IoT(物联网)设备的普及工具,数据移动瓶颈对这些系统的性能和自主性施加了严格的限制。PiM(内存内处理)正在兴起,成为一种缓解数据移动瓶颈的方法,同时满足依赖 CNN(卷积神经网络)的边缘成像应用程序的严格性能、能效和准确性要求。
Mohamed Zahran - 异构计算:将长期存在
在过去几年中,“异构计算”这个流行语的提及率一直在上升,并且在未来几年将继续被听到,因为异构计算将长期存在。什么是异构计算,为什么它正在成为常态?我们如何从软件方面和硬件方面来处理它?本文提供了对其中一些问题的答案,并对其他问题提出了不同的观点。
David Chisnall - 没有所谓的通用处理器
计算机架构中存在一种日益增长的趋势,即将处理器和加速器归类为“通用”。在本年度国际计算机体系结构研讨会 (ISCA 2014) 上发表的论文中,45 篇中有 9 篇明确提到了通用处理器;另一篇还提到了通用 FPGA(现场可编程门阵列),还有一篇提到了通用 MIMD(多指令多数据)超级计算机,将定义扩展到了崩溃的边缘。本文提出了一个论点,即没有真正的通用处理器,并且对这种设备的信念是有害的。