BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

Java 9、OSGi以及模块化的未来(第二部分)

| 作者 Neil Bartlett 关注 0 他的粉丝 , Kai Hackbarth 关注 0 他的粉丝 ,译者 张卫滨 关注 13 他的粉丝 发布于 2017年1月18日. 估计阅读时间: 19 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

核心要点

  • 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-069OMA-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领域多个不同的研究项目。他重点关注的领域是智能家居、汽车和物联网,并积极推进整个产品组合目前的发展和战略定位。

查看英文原文:Java 9, OSGi and the Future of Modularity (Part 2)

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT