核心要点
- Java 9 会在 2017 年发布,一个标志性的特性就是新的模块化系统,名为 Java 平台模块化系统(JPMS)。本文探讨了它与现有的 Java 模块标准即 OSGi 会产生什么样的关联,又会对其产生什么样的影响。
- 自 1.0 版本以来,Java 已经增长了 20 倍,对这个平台进行模块化是非常必要的。为了解决这个问题,也曾有过很多失败的尝试。而与此同时,OSGi 为开发人员提供应用程序模块化的功能已经有 16 年之久了。
- OSGi 和 JPMS 在实现细节上有本质上的区别。如果将 JPMS 作为模块化的通用解决方案,似乎会有严重的缺陷和缺失的功能。
- JPMS 的目标是使用起来比 OSGi 更简单、更容易。但是,让现有的非模块化产品模块化是非常复杂的,而且 JPMS 在这个目标上似乎并没有成功。
- JPMS 在 Java 平台自身模块化方面做得很好,这意味着我们可以为特定的工作构建一个小的运行时环境,它只包含 Java 平台相关的部分。在应用程序模块化方面 OSGi 有很多优势。我们已经证明了两者可以结合起来,这看起来是一个成功的方式。
本文是“Java 9、OSGi 以及模块化的未来”系列的第二篇文章。本文的第一部分请参阅“Java 9、OSGi 以及模块化的未来”。
我们将会继续深入讨论OSGi 与Java 平台模块化系统(JPMS),后者计划将作为Java 9 的一部分而发布。在第一部分中,我们在整体上对比了这两个模块化系统,描述了它们各自是如何解决在模块间进行隔离这一问题的。我们深入研究了依赖功能是如何运行的,并了解了一些反射方面的问题。在第二部分中,我们将会讨论版本化、动态模块加载以及未来OSGi 和JPMS 实现互操作的可能性。
版本化
版本化在软件交付中是很重要的一个方面。API 和实现都会发生变化,所以,当我们依赖它们的时候,实际上我们所依赖的是它们在某个时间点上的状态。所有的模块化系统都必须要解决这一现实问题……在引用制件(artifact)或依赖项时,这一般会通过在这两者上面明确声明版本来实现。
但是,并非所有的变更都具有相同的破坏性。如果我们基于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 使用语义化版本(semantic versioning),完全遵循流行的语义化版本规范(尽管OSGi 本身要比这个文档更早)。大致来讲,版本号中的第一部分是主版本(major),代表了在功能和API 方面的破坏性变更,第二部分是次版本号(minor),表明了这是非破坏性的功能增强,而第三部分是微版本号(micro),代表了已有功能的补丁。
OSGi 开发人员不需要关心或明确声明这些版本范围。就像导入功能本身一样,版本范围也是在构建的时候通过分析依赖关系自动生成的。例如,如果我们只是作为消费者来使用 API 包,那么我们有可能使用一个很宽泛的范围,比如 [1.0.0, 2.0.0),所有的次版本和微版本都包含在内。但是,如果我们要作为提供者实现服务接口的话,那么我们必须要以更狭窄的范围来导入包,比如 [1.0.0, 1.1.0),意味着所有从 1.0.0 到 1.1.0 的版本,但是不包含 1.1.0 版本本身。这里的区别在于支持 1.0.0 版本功能的提供者将不会支持 1.1.0 版本,因为次版本号增加的数字表明服务提供者无法自动提供新的功能。而另一方面,对于服务的消费者来说,可以很容易地使用 1.1.0 或 1.2.0 版本,它们只需忽略掉新增的功能就可以了。
除了在导入时生成版本范围,OSGi 构建工具( bnd )还能帮助我们保证导出包的正确性。版本是包的一个属性,可以使用@Version
注解直接写入到包的package-info.java
文件中。在这里很重要的一点就是当包的内容发生变化的时候,要及时更新这个版本号:例如,如果我们为服务接口新增了一个方法,那么我们需要将版本号从 1.0.0 增加到 1.1.0。构建工具会检查版本号,使其能够精确地反应变更的实际情况。举例来说,如果我们新增了方法,但是忘记了变更版本号,或者只是对版本号做了很小的变更,比如只增加到 1.0.1,那么构建将会失败。
最后,OSGi 的灵活性还在于它允许将某个模块的多个版本同时部署到一个应用之中。如果我们的一些依赖项具有对某些通用库的传递性依赖的话,比如 slf4j 或 Guava,就有可能发生这种情况。这里也有一些限制,我们不能在一个模块中直接导入某个包的多个版本,但是当我们真正需要这项特性的时候,它还是非常有价值的。
这些都意味着 OSGi 提供了一种综合性的方案,允许我们由单独的团队或组织来构建模块,稍后再将这些模块组合成一个应用。这些工具能够让我们充分相信我们所选择的模块能够在一起正常运行起来。
与此形成对比的是,JPMS 在版本化基本上没有提供任何的支持。
在 module-info.java 文件中,我们没有办法指定版本(在编译后的 module-info.class 文件中会有一个 Version 属性,但是它并不是由 Java 源码生成的,它的实际用处尚不明确)。依赖无法进行版本化:JPMS 模块只能通过名称来声明对其他模块的依赖,不能使用版本,当然就更不能指定版本范围了。这些特性需要通过外部工具来提供,但是在这方面的努力也是困难重重,因为 module-info.java 源文件无法进行扩展,在这个文件中无法使用 Java 注解。
JPMS 的需求文档表明,选择运行期版本兼容的组件并不在它们的功能范围之内。这意味着必须要由其他的工具来完成这项任务,但是如果没有合适的元数据的话,这些工具也是无法实现的。其实,如果能将版本元数据和基本的模块元数据放到同一个描述文件中,这是非常自然合理的,但是目前来看,这将无法实现。
同时,正如我们前文所述,在 JPMS 中,并不允许同一个模块的多个版本共存。另外,它还不允许多个模块导出相同的包,甚至不允许私有包出现重叠。所以,用来构建合法模块集的工具必须要找到一种解决传递性依赖的方式。在很多场景下,所谓的“解决方案”不过是让特定的模块不与其他的模块一起使用。
动态化
OSGi 基于类加载器实现了隔离,这种机制有一个很好的副作用,那就是能够支持运行时动态加载、更新和卸载模块。在企业级的环境中,这似乎并不是那么重要,大多数企业级部署的 OSGi 应用其实并没有使用动态更新的功能。OSGi 也没有说我们必须要使用动态更新!
但是,动态部署在其他的环境中就非常有价值了,比如说在 IoT 领域。如果软件部署到了成千上万,甚至百万级数量的设备上,通过缓慢且断断续续的网络对软件进行更新是一件非常令人头痛的事情。OSGi 是为数不多的能够在任意平台上直接支持运行期更新的技术,它所使用的数据量绝对是最少的:我们只需要发送真正发生变更的模块即可。
最初在 2000 年,电信运营商都愿意借助 OSGi 在家庭网关和路由器上构建智能家居解决方案,采用这种方案一个主要原因就在于它能够在无需固件升级的情况下管理软件。固件升级并不是一个很有吸引力的解决方案,主要的原因在于:下载——固件升级一般需要下载 MB 大小级别的软件,而且可能需要下载到上百万台的设备上。固件是与设备相关的,所以最终可能需要创建很多不同的更新包,并要管理它们的部署;测试——固件升级需要广泛、耗时、成本高昂且压力重重的测试,因为每次都需要在所有的设备上进行测试。OSGi 能够非常显著地简化这一过程,更新可以应用到模块中,在运行中的网关和路由器上进行安装,不需要重启。相同的模块可以应用到所有的设备上(它通常会从底层的设备硬件层抽象出来),另外很重要的一点,单元测试可以只针对更小的一个软件集来执行,从而节省大量的时间、精力和金钱。一个很具体的例子就是 Qivicon,它是由德国电信所创建的行业联盟。Qivicon 所提供的家庭网关包含了基于 OSGi 的软件技术栈、后端基础设施、针对应用开发人员的工具,并且还会提供维护和支持。通过采用 OSGi 来支撑其生态系统,Qivicon 合作伙伴能够更加快速地将他们的智能家居产品推向市场。
Qivicon 的合作伙伴会持续地集成新设备,开发具有创新性的增值服务。这需要复杂的设备管理和软件供应能力,以确保特定设备平台中软件组件的依赖和兼容性管理能够正常运行。在 OSGi 中,这些功能已经借助已有的工业标准进行了标准化,比如 TR-069 和 OMA-DM 。
除此之外,动态化行为相关的功能并不仅仅局限于软件更新方面。
OSGi 服务注册中心(OSGi Service Registry)本质上也是动态的。服务可以来去自如,绑定服务的组件能够实时感知。借助服务,我们能够表述和报告持续变化的外部现实世界。即便是在相对稳定的企业级应用中,这也是相当有意义的。例如,OSGi 服务可以反映外部数据 feed 的可用性或者具备负载均衡功能的 REST 服务的 IP 地址,甚至是证券市场的开放时间。消费服务的每个组件能够决定当服务不可用的时候要采取什么样的反应动作:它可以继续运行,也可以将自己所拥有的服务解除注册。这样的话,低层级的状态变更能够非常可靠地传递到它们所影响的地方。
互操作性与未来
2017 年,JPMS 会随着 Java 9 的发布,在 Java 的主版本中释放。目前已经有大量使用 OSGi 所编写的应用,并且还有很多正处于编写之中。它们是安全的吗,这些应用必须要针对新的 JPMS 模块系统进行重写吗?
需要阐明的第一点就是OSGi 应用不经任何变更就能运行在 Java 9 上,只要应用编写的时候没有用到不支持的、内部 Java API 即可。这同时也是针对所有 Java 代码的通用建议。OSGi 只用到了受支持的 Java API,Oracle 给出了一个郑重的承诺,不会破坏这样的应用。在使用 Java 9 中所遇到的问题很可能是因为你所用到的库使用了 JDK 的内部类型,在 Java 9 中只有借助特定的配置标记才能继续使用它们。OSGi 的用户对这种变更准备得会更加充分,因为他们的导出会进行明确的声明。普通的应用在声明依赖的时候只是将 JAR 文件堆砌到类路径下,与之对比,在 OSGi 平台上构建的应用对它的依赖范围会更加清晰。
在这种最基本的兼容模式下,OSGi 框架和 bundle 将会存在于一个“未命名”的 JPMS 模块中。OSGi 将会继续提供已有的隔离特性,还有它强大的服务注册和动态加载功能。你在 OSGi 方面的投资依然是安全的,并且对于新项目来说 OSGi 依然是一个很棒的选择。
但是,我们希望能够做得更好一些。当 OSGi 运行在模块化的 Java 9 平台中,我们应该能够使用平台中的模块。例如,某个 OSGi bundle 可能会声明它所依赖的平台模块集合——也就是说,我们应该能让 OSGi bundle 直接声明对 JPMS 模块的依赖。OSGi 框架应该能够在运行时遵循这些依赖,工具应该基于这些依赖准备运行时环境。
到这里,所有的事情看起来都非常棒。在 2015 年 11 月份的一篇博客文章中,我描述了一个我所构建的概念验证样例,阐述了OSGi 如何运行在JPMS 上。我详细介绍了OSGi bundle 可以如何声明对特定JPMS 模块的依赖,这些模块运行在基础平台中。我展示了如果bundle 所依赖的JPMS 模块没有位于平台之中,OSGi 将会拒绝这个bundle。我没有构建用于组装运行时的原型工具,但是创建这种工具所需的各项部件都已经存在了。
图三展示了未来互操作性是如何运行的。我们可以看到Bundle A 导入了 javax.activation
包,这个包是 JPMS 中的java.activation
模块所导出的。互操作层将会知道平台包含这个模块,允许 OSGi 解析它。Bundle A 不需要任何变更就能迁移到 Java 9 上。Bundle B 使用了java.httpclient
JPMS 模块的java.net.http 包
,但是这个包不能表达为 OSGi Import-Package,因为它是以“java
”开头的(注意,所有的 bundle 和模块都隐式地依赖java.base
包)。
因此,我们提议了一个新的 OSGi 头信息,名为“Require-PlatformModule”,它表达了所需的 JPMS 模块。这样的话,如果平台不包含 java.httpclient 模块的话,OSGi 框架将会让 Bundle B“快速失败(fail fast)”。它也能让工具为应用构建一个完整的运行时,在这个运行时中只需使用最少的 JPMS 模块和 OSGi bundle。
再次强调,很重要的一点就是这些工作只是一个非官方的概念验证,OSGi 与 JPMS 将会按照什么形式进行互操作还要取决于规范如何制定。
图:OSGi – JPMS 互操作概念原型
结论
从 Jigsaw 原型项目来看,JPMS 在模块化 Java 平台方面做得非常不错。这项工作所带来的一个结果就是我们可以构建一个很小的运行时环境,其中只包含特定负载所需的 Java 平台中的内容。
但是,如果作为应用层级的模块化规范,那 JPMS 就有一些很严重的不足了。缺少版本化这项不足会让人觉得有些惊讶,如果没有外部工具提供元数据,组成并行的系统,我们很难想象该如何构建实际的应用。整个模块的依赖声明将会引入传递性的依赖,这可能会超出实际所需,从而削弱构建更小平台所带来的收益。反射无法访问非导入的包,在使用 Java 生态中已有的框架时,这一点可能会带来不必要的麻烦。
对于 JDK 本身来说,这些设计可能是非常恰当的:它们会增加平台的健壮性和安全性,避免破坏已有应用的向后兼容性。但是,这也有一定的代价,对于应用模块化来说,这不是一个好的选择。
所以,OSGi 的前景看起来依然很光明:通过将 OSGi 与一个裁剪过的模块化 Java 平台组合在一起,我们能够同时得到两者最好的一面。在 16 年的经验中,OSGi 遇到并解决了很多问题,而这些问题是 JPMS 甚至还没有遇到过的。OSGi 生态系统的工具和运行时非常广泛和深入。它保证不会过时,会支持长期维护的、已证明可行的、独立的标准。你还在等待什么呢?
关于作者
Neil Bartlett是首席工程师、顾问、培训师、Paremus 的开发工程师。Neil 从 1998 年开始接触 Java,2003 年开始接触 OSGi,专注于 Java、OSGi、Eclipse 和 Haskell。他是 Eclipse 插件 bndtools 的创始人,bndtools 是 OSGi 方面领先的 IDE。他经常在 Twitter(@nbartlett)上发推 OSGi 有关的内容,并在 Stack Overflow 上回答 OSGi 相关的问题,在这个网站上他是唯一持有 OSGi 领域黄金徽章的人。Neil 定期会在 Paremus 博客上撰写文章,他还编写了他的第二本书《Effective OSGi》,该书为开发人员展示了采用最新的技术和工具如何快速强化OSGi 的生产力。
Kai Hackbarth是博世软件创新(Bosch Software Innovations)的布道师。他曾深入参与 OSGi 联盟的技术标准化活动超过 15 年。Kai 是 OSGi 联盟董事会的成员,自 2008 年以来一直是 OSGi 家居专家组的联合主席。他正在协调 IoT 领域多个不同的研究项目。他重点关注的领域是智能家居、汽车和物联网,并积极推进整个产品组合目前的发展和战略定位。
评论