Java 9,OSGi和模块化的未来(2)

Java 9 中一个重要的新特性就是模块化。它的实现机制是什么那?它和已有的模块框架OSGi有什么差异那?为了回答这些问题,本人在网上找到了一篇比较好的介绍文章,为了加深理解,对文章进行了翻译。由于原文分为2个部分,所以翻译对应也分为2篇:

1)《Java 9,OSGi和模块化的未来(1)》是对《Java 9, OSGi and the Future of Modularity (Part 1)》的翻译,文章日期为2016年9月22日。介绍的内容包括:背景、高层次比较、复杂性、依赖粒度对比、模块导出对比、模块导入对比、反射和服务。

2)《Java 9,OSGi和模块化的未来(2)》是对《Java 9, OSGi and the Future of Modularity (Part 2)》的翻译,文章日期为2016年10月4日。介绍的内容包括:动态性、二者协同工作、未来发展、结论。

本文是对原文第二部分的翻译。

引言

关键点

  • Java 9 在2017年发布,其中一个重要的特性就是新的模块化系统,被称作Java平台模块系统(Java Platform Module System,JPMS)。本文将介绍它与Java已有的模块标准化机制OSGi的关系,以及对OSGi的影响。
  • 自1.0版本以来Java平台已经增长了20倍,整个平台存在着模块化的需求。为了解决这个问题,进行了很多不成功的尝试。与之相对的是,OSGi已经提供了16年的应用模块化服务。
  • OSGi和JPMS在实现细节上差别很大。如果将JPMS作为模块化的一般解决方案,会存在一些重大的缺陷,并且缺少OSGi的一些特性。
  • JMPS的目标是比OSGi更简单和更容易,但是对一个非模块化的产品进行模块化设计本身就是一件很复杂的事情,JMPS看起来好像还没有实现这个目标。
  • JMPS在对Java平台本身的模块化方面做得非常好,这意味着我们可以在运行时只加载和任务相关的Java平台组件。对于应用模块化,OSGi有很多的优点。我们已经证明这两者可以很好地结合在一起。

这篇文章是“Java 9, OSGi and the Future of Modularity”文章系列的第二部分。第一部分请查看《Java 9, OSGi and the Future of Modularity (Part 1)》

本文将继续深入了解OSGi和JPMS(Java Platform Module System),其中JPMS会作为Java 9的一个组成部分。在上篇文章中,我们在一个较高的层次比较了这两个模块化系统,讨论了它们是如何解决模块间的隔离性的。我们研究了依赖关系是如何建立的,并且我们探讨了一些关于反射的问题。本文作为第二个部分,将探讨版本管理、模块动态加载以及二者潜在的协同工作可能性。

版本管理

版本管理是所有软件交付的一个关键点。API和实现都会改变,所以无论何时我们依赖它们,我们都隐含地依赖于它们在某个时间点的存在。任何模块系统必须能够处理这个现实,通常用显式的版本来指明依赖关系。

然而并非所有的改变都具有同样的破坏性。如果我们使用了版本为1.0.0的模块的来构建和测试我们的软件,那么当我们部署版本1.0.1或1.0.5时,我们有可能可以继续工作,但是如果部署的是版本2.0.0或版本5.2.10,那么不能工作的可能性较大。这表明模块系统需要了解并支持兼容性范围。

OSGi一直支持这些概念。Bundle在导出包时标注了版本号。在导入包的时候指明了版本范围,通常使用闭开区间来表示这个范围,例如“[1.0.0, 2.0.0)” 表示版本介于1.0.0和2.0.0之间,不包括2.0.0。OSGi使用语义版本控制,与流行的语义版本规范完全一致(尽管OSGi早于那些规范)。大致来说,版本号的第一段是主版本号,表示功能和API的重大变化,第二段是次版本号,表示功能的增强,第三段表示增加了一些补丁。

OSGi的开发者不需要推理或显式地声明这些版本范围。和 import 一样,版本范围可以在构建时期根据依赖情况自动构建。例如,如果我们仅需要使用一个API包,那么我们可以提供一个比较宽的区间,如“[1.0.0, 2.0.0)”,这个区间包含了最小和最大版本号之间的所有版本。但是如果我们是提供一个服务接口的实现,那么我们需要使用较窄的区间来导入依赖,例如“[1.0.0, 1.1.0)”,意味着包含1.0.0和这个区间内的,但是不包含1.1.0。这里的不同点在于,一个支持1.0.0的服务提供者无法支持1.1.0,因为增加的版本号表明有新的功能被添加了进来,而提供者无法自动提供新的功能。另一方面,一个服务消费者,可以方便地使用1.1.0和1.2.0等版本号,因为它可以忽略新增加的功能。

除了生成导入范围外,OSGi构建工具(bnd)还可以帮助获得导出包的正确版本。版本号是包的一个属性,它可以直接写在包中(通过在 package-info.java 文件中添加 @Version 注解)。当包中的内容改变的时候,一件很重要的事就是修改这个版本号,例如:当我们在服务接口中增加了一个方法时,我们需要将版本号从1.0.0增加到1.1.0。构建工具检查版本号是否准确地反映了所做更改的性质。例如,当我们添加了方法而忘记修改版本号的时候,构建将会失败,或者我们只是将版本号增加为1.0.1的时候也会如此。

最终,OSGi具有这样的灵活性:可以在单个应用程序中同时部署模块的多个版本。这种情况会在这样的场景出现:通过传递依赖我们引用了一些通用库的不同版本,如 slf4j 或 Guava。还有一些限制是:我们不能直接在单个模块中导入包的多个版本,但是在真正需要的时候,具备这样的能力还是很有用的。

与之相对的是,JPMS对版本控制基本没有任何支持。

在 module-info.java 中没有办法为一个模块指明版本。在 module-info.class 文件中存在一个版本属性,但是它并不是来自于 Java 代码,目前还不清楚它在实际中该如何使用。依赖声明同样没有版本:JPMS模块只能通过名字来 require 其它模块,无法指明版本,当然更无法指明版本的范围。这些特性需要由外部工具添加,但这种方法是受限的,因为 module-info.java 源文件是不可扩展的,并且在该文件内也无法使用 Java 注解。

JPMS的需求规定:在运行时选择兼容的版本不在支持范围内。这意味着其它工具将不得不做这项工作,但没有合适的元数据它们无法完成这项工作。将版本元数据存储在与基本模块元数据相同的描述符中是很自然的,但这是不可能的。

正如我们提到的一样,JPMS中禁止在运行时同时包含一个模块的多个版本。此外,它禁止多个模块导出相同的包,甚至禁止重复的私有包。因此,无论其它工具使用什么方法来构建一个有效的模块集合,都需要找到一个解决转递依赖冲突的方法。在很多案例中,一个简单的“方法”是:在多个模块共同存在的场景下,禁止一些模块的使用。

动态性

OSGi基于类加载的隔离实现方案有一个不错的用处:可以支持模块在运行时被动态加载、更新和卸载。这在企业环境中似乎并不重要,事实上大多数OSGi企业在部署中都不会使用动态更新。的确,OSGi没有要求你一定要使用动态更新!

但是动态更新在其它环境中是非常有用的,比如物联网。在数千台甚至数百万台设备上,通过缓慢或断断续续的网络更新软件是一件让人头痛的事情。OSGi是少数技术之一,可以在任何平台上直接支持使用最少数据量进行即时更新:我们只需要发送实际更改的模块。

最初在2000年,电信运营商在家庭网关和路由器上使用OSGi构建智能家居解决方案的主要原因之一是:能够在不进行固件更新的情况下管理软件。固件更新不吸引人的原因有很多:下载固件更新通常需要下载兆字节的软件到潜在的数百万设备上;固件是针对设备设计的,因此你可能会有许多不同的更新来创建和管理部署;测试固件更新需要大量的、耗时的、昂贵的压力测试,每次都要对每个设备执行这个测试。OSGi显著简化了这个过程;更新可以应用在模块中,并快速安装在运行的网关和路由器上,不需要重启;同样的模块可以在所有设备上使用(通常是底层硬件设备的抽象),重要的是,单元测试可以在更小的软件集上执行,节省大量的时间、精力和金钱。一个具体的例子是 Qivicon,它是一个由德国电信公司成立的行业联盟。Qivicon 提供家庭网关,包括:基于OSGi的软件栈,后端基础设施、应用程序开发工具以及维护和支持。通过使用OSGi来搭建基础生态系统的方法,使得Qivicon的合作伙伴能更快地将智能家居产品推向市场。

Qivicon 合作伙伴不断整合新设备和开发新的创新增值服务。这需要复杂的设备管理和软件供应能力,以确保针对特定设备平台的软件组件的依赖性和兼容性管理。通过利用现有的工业标准(如 TR-069 和 OMA-DM),这些功能已经被写入OSGi标准规范中了。

此外,动态行为不仅仅体现在软件更新上。

OSGi服务注册表本质上是动态的。服务可以注册和卸载,与之绑定的相关组件也会在收到事件通知。服务允许真实世界不断变化的状态被表示和通知。即使在相对稳定的企业应用领域,这也是相关的。例如,OSGi服务可以表示外部数据输入是否可用,或者是REST服务的负载均衡IP地址,甚至是金融市场的开放时间。每个消费服务的组件可以决定服务不可用时的反应行为:可以继续,或者注销自身提供的服务。因此,状态的变化被可靠地传播到任何有影响的地方。

协同工作与未来发展

JPMS会在Java 9中发布。目前有非常多的应用程序是用OSGi写的,同时还有很多代码正在被书写。这些代码是安全的吗?它们是否需要在JPMS平台上重新被改写?

首先需要明确地是:OSGi应用可以不用修改代码继续在Java 9上运行,因为它没有使用不被支持和内部的Java API。对于其他的Java应用代码也是如此。OSGi只使用了被支持的Java API,并且Oracle承诺Java 9不会使这些应用奔溃。你在使用Java 9时遇到的问题可能是来自一些使用了JDK内部类型的类库,因为这些类型在Java 9中无法被访问,除非通过特殊的配置标志进行访问。OSGi的用户将能更好地应对这个改变,因为它们的模块具有显式的依赖列表。通常,Java应用会在类路径上放置多个Jar包,与它们相比,基于OSGi的应用对于平台依赖的范围会更加清晰。

一个最基本兼容模式是,OSGi框架和bundle会存在于JPMS的“未命名”模块中。OSGi还会继续提供所有已经存在的隔离性特性,包括它功能强大的服务注册和动态加载能力。你对OSGi的投资是安全的,而且OSGi仍然是新项目的一个好选择。

但我们希望能比这做得更好。当OSGi运行在已经模块了的Java 9平台上时,我们应该能够充分利用平台中的模块。例如,可以为一个OSGi bundle声明它依赖的平台模块,这意味着一个OSGi bundle可以直接依赖一个JPMS模块。OSGi框架应该关注那些运行时的依赖,并且工具应该能够根据这些依赖准备运行时环境。

此时事情已经看起来很不错了。在我2015年11月的一篇博客中,我对这个概念进行了验证性描述,在JPMS上构建并运行了OSGi程序。我详细介绍了如何让OSGi bundle在基础平台的JPMS模块中声明依赖。我展示了OSGi是如何拒绝这样的一个bundle:它依赖的JPMS模块不在平台中。我没有在运行时提供一个工具原型,但是通过所有描述的部分已经可以构建这样的一个工具了。

图3描绘了未来两个机制可以如何一起工作。我们可以看到:Bundle A 导入了包 javax.activation,这个包来自JPMS中导出的 javax.activation 模块。交互层知道平台中包含了这个模块,会运行OSGi来处理它。当Bundle A迁移到Java 9上时,并不需要做任何改变。Bundle B使用了 java.net.http 包,这个包来自JPMS的 java.httpclient 模块,但是它无法在OSGi中Import-Package部分进行声明,因为它是以 java. 开头的(需要注意的是,所有的 bundle 和 moudle 都隐含地依赖 java.base)。

因此,我们提出了一个新的OSGi头部,称作“Require-PlatformModule”,它用来表示对JPMS中模块的依赖。这样当Bundle B没有包含 java.httpclient module 模块的时候,OSGi框架能够在Bundle B中“快速失败”。这同时还可以使得工具能够为应用构建一个完整的运行时环境,这个环境是JPMS中的 modules 和OSGi中的 bundles 的最小集合。

再次声明,这个工作是一个非官方的概念证明。最终,OSGi如何和JPMS进行交互将由规范来处理。

图3

结论

JPMS,通过Jigsaw原型项目,对Java平台本身的模块化工作是非常出色的。通过这个工作,可以构建更小的运行时环境,只需要包括Java平台中任务依赖的部分。

然而作为应用的模块化规范,JPMS存在严重的缺陷。缺少对版本控制的支持是一个令人震惊的遗漏,而且在没有外部工具提供额外元数据的情况下,很难在构建应用程序的过程中实现版本管理。整个模块(whole-module)依赖声明形式将会导致获得更多的传递依赖项,这将削弱它们迁移到更小平台的能力。无法通过反射来获得未导出模块中的类型,这样将会无法使用Java生态系统中已有的一些框架。

这些设计对于JDK本身来说可能是合适的:这增加了平台的健壮性和安全性,同时没有破坏所有Java应用的向后兼容性。但是它们的做法只是一种折中,对于应用的模块化来说,这个设计很不友好。

所以OSGi的未来看起来是光明的:通过将OSGi和整理过的模块化Java平台相结合,我们可以使这两个生态都获得更好的发展。OSGi已经具有了16年的经验,并且遇到和解决了JPMS还没有考虑过的问题。OSGi的开发工具和运行时生态是广泛和深入的。在一个长期有效、独立的标准机构的支持下,它的未来是得到保障的。你还在等什么那?

-------------本文结束感谢您的阅读-------------