java 模块化osgi_Java 9,OSGi和模块化的未来(第1部分)
java 模块化osgi
重要要点
|
这是“ Java 9,OSGi和模块化的未来”系列的第一篇文章,另请参阅第2部分: Java 9,OSGi和模块化的未来(第2部分) 。
明年发布Java 9时,旗舰功能将是新的模块系统:Java平台模块系统(JPMS)。 尽管JPMS的细节并没有完全固定,但我们已经对它的形状有了很多了解。
Java确实有一个预先存在的模块系统,该模块系统自2000年以来就以各种形式出现。这就是称为OSGi的模块系统,它是独立于供应商的行业标准。 它由OSGi联盟出版,OSGi联盟是领先软件供应商,电信公司和其他组织(包括Adobe,Bosch,华为,IBM,Liferay,NTT,Oracle,Paremus和Software AG)的联合体。 它为几乎所有Java EE应用程序服务器,最流行的IDE,eBay,Salesforce.com和Liferay等Web应用程序提供动力,并被美国空军和联邦航空管理局等*和军队使用。
OSGi专为物联网而设计-从一开始,OSGi就专为嵌入式设备而设计,几年前内存和CPU资源受到严重限制。 如今的设备功能更强大。 这为构建复杂的应用程序和解决方案提供了机会,并催生了蓬勃发展的组织和个人生态系统,它们为整个解决方案贡献了软件和硬件元素。 这样的生态系统可以在互联家庭,互联汽车,智慧城市和工业4.0(IIoT)等市场中找到。 网关通常用于将传感器和设备彼此连接以及与后端系统连接。 应用程序和服务可以在网关和/或云中本地运行。
OSGi还提供了许多规范,以启用用于构建开放式IoT生态系统的基本功能。 这些功能包括设备管理,软件配置和设备抽象,可根据其底层通信协议对设备进行概括。 AT&T,博世,NTT,德国电信,通用电气,日立,美诺,施耐德电气等公司如今都受益于使用OSGi构建物联网解决方案,并且已经做了很多年。 借助OSGi和IoT,如今已连接了数百万台设备。
自然,OSGi用户很好奇Java 9中的新模块系统将在短期和长期内影响OSGi。
有技术,政治和商业原因,使得Java生态系统中的两个模块系统很快就会存在。 在本文中,我们避开了政治,并从技术角度比较了两者。 我们以JPMS和OSGi如何协同工作,我们认为它们各自的领域是什么以及在新世界中将存在什么机会的愿景作为结束。
请注意,在本文中,我们使用了2016年8月公开提供的信息。在规范成为最终定稿之前,某些细节可能会发生变化。
背景
自1990年代末诞生以来,Java平台已经有了长足的发展。 从下载大小来看,JDK 1.1的大小不足10Mb,而针对JDK 8u77的Mac OS X下载则是巨大的227Mb。 安装的占用空间和内存需求也相应增加。 增加的原因是增加了新功能,其中大多数功能受到欢迎和有用。 但是,平台上的每个新功能都会为不需要它的用户带来膨胀……而且没有人使用所有平台。 而且,即使它们已经过时,所有现有功能也会保留下来,因为Java的管家对向后兼容性表现出了令人敬佩的奉献。
多年来,Java的重量增加并不是一个大问题。 它最受企业平台欢迎,其主要竞争对手是Microsoft的.NET,其发展轨迹大致相似。 在现代世界中,Java面临着不同的挑战。 物联网正将人们的注意力重新集中到占地面积上,而像Node.js和Go这样的新型敏捷平台和语言则是重要的竞争对手。
安全性也是一个大问题:对Java的攻击促使注重安全性的组织将其从用户桌面中完全删除。 如果在JVM内部和用户空间应用程序代码之间有更好的隔离,那么许多此类攻击将是不可能的。
长期以来,很明显需要采取一些措施来模块化平台。 在2000年代中期,JSR 294及其“超级包”,JSR 277的“ Java模块系统”经历了一系列失败的努力,然后最终出现了一个名为Jigsaw的原型项目。 它最初打算在2011年与Java 7一起交付,但后来推迟到Java 8,然后又推迟到Java9。作为一个原型项目,Jigsaw告知并提供JPMS规范的参考实现。
同时,OSGi一直在发展和改进16年。 OSGi是应用程序模块化的标准:由于它不是Java平台的直接组成部分,因此它不会影响平台本身的模块化。 但是许多应用程序都受益于它提供的高于JVM级别的模块化模型。
高层比较
JPMS和OSGi之间有许多细微的差异,但是有一个很大的差异,那就是隔离的实现。
隔离是任何模块系统的最基本特征。 必须有某种方法来保护每个模块不受同一应用程序中运行的其他模块的干扰。 隔离是一个连续的概念,而不是二进制的概念:OSGi和JPMS都无法采取任何措施来防止行为不当的模块占用模块的性能,该模块会夺取JVM中的所有可用内存,启动数千个线程,或者使CPU陷入繁忙的循环。 在OS上将模块作为单独的进程运行可以提供这种保护,但是即使那样也不是完美的。 仍有人可能会崩溃操作系统或擦除磁盘。
OSGi和JPMS都提供代码级隔离,这意味着一个模块除非得到该模块的明确同意,否则不能访问另一个模块的内部类型。
OSGi使用类加载器实现其隔离。 每个模块(或OSGi术语中的“捆绑包”)都有一个类加载器,它知道如何加载捆绑包中的类型。 它还可以将类加载请求委托给它依赖的其他捆绑软件的加载器。 该系统经过高度优化,例如OSGi直到最后一刻才为捆绑软件创建类加载器,并且每个加载器看到的类型集要少得多的事实意味着每种类型的加载速度都可以略微提高。
该系统的最大优点是捆绑包可以包含重叠的包和类型,并且它们不会相互干扰。 实际的结果是,某些包和库的多个版本可能在同一JVM中同时运行。 当处理复杂的传递依存关系图时,这是一个福音。 在许多企业Java应用程序中,几乎不可能提出一组仅包含每个库一个版本的依赖项。
例如,让我们看一下JitWatch库1 。 JitWatch取决于slf4j-api 1.7.7和logback-classic 1.1.2 ...,但logback-classic 1.1.2取决于slf4j-api 1.7.6,这与JitWatch的直接要求相抵触。 JitWatch也传递地依赖于1.6和版本两种版本1.9的jansi ,如果我们有测试范围的依赖,我们还没有得到SLF4J的API,1.6的另一个版本。 这种混乱非常普遍,传统的Java没有真正的解决方案,只能将“ excludes”逐渐添加到我们的依赖树中,直到我们神奇地获得了一组有效的库为止。 不幸的是,JPMS对此问题也没有答案,我们很快就会看到。
使用类装入器进行隔离确实有一个缺点:它打破了每个类型最多可以在一个位置找到的假设。 这是模块化的自然结果。 如果一个模块可以使用自己的类型而不受其他模块的干扰,那么不可避免的是在多个模块中都可以找到一个类型名称。 可悲的是,这给很多遗留Java代码带来了一个问题,这些代码并未考虑模块化。 特别是,调用Class.forName(String)
从名称中查找类型在真正的模块化环境中将始终无法正常工作,因为它可以返回多种类型。
正是这一缺陷阻止了OSGi用来模块化JDK本身。 JDK的许多部分都有一个隐含的假设,即可以从JDK的任何其他部分加载任何JDK类型,因此在类似OSGi的模型下会有很多事情发生。 为了解决此问题-并简化使用Class.forName
代码的迁移,JPMS选择完全不使用类装入器进行隔离。 当您使用“模块路径”上的一组模块启动应用程序时,所有这些模块将由相同的类加载器加载。 相反,JPMS引入了新的可访问性规则以实现隔离。
OSGi中的隔离障碍是可见性 。 在OSGi中,我们无法加载模块的内部类,因为从外部看不到它们。 也就是说,模块的类加载器只能看到模块内部的类型,以及从其他模块显式导入的类型。 如果我尝试从您的模块中加载内部类,则我的类加载器将看不到该类型。 好像该类型根本不存在。 如果我们尝试继续加载该类,则将收到NoClassDefFoundError
或ClassNotFoundException
。
在JPMS中,每种类型都具有其他每种类型的可见性,因为它们存在于同一类加载器中。 但是JPMS添加了辅助检查,以确保加载类有权访问它尝试加载的类型。 其他模块的内部类型实际上是私有的,即使它们被声明为公共的也是如此。 如果我们尝试继续加载它们,则将收到IllegalAccessError
或IllegalAccessException
。 如果尝试从另一个程序包加载私有或默认访问类型,则将得到相同的错误,并且在该类型上调用setAccessible
也无济于事。 这改变了Java中public
修饰符的语义,它以前是指可以通用访问,现在意味着只能在模块及其需求者中访问。
JPMS方法的缺点是不可能有内容重叠的模块。 也就是说,如果两个模块都包含一个名为org.example.util
的私有(未导出)包,则这些模块不能同时在模块路径上加载–这将导致LayerInstantiationException
。 可以通过在应用程序中实例化类加载器来解决此限制,但这正是OSGi已经为我们完成的工作!
同样,这完全是设计使JPMS能够模块化JDK的内部。 但是结果是,您可能会因为纯粹的内部实现细节冲突而导致模块无法一起工作。
复杂
关于OSGi的最常见的抱怨之一是它可能增加开发人员的复杂性。 这里有一个事实,但是提出这一抱怨的人正在误将药物用于该疾病。
模块化不是在发布之前就可以撒在应用程序上的魔尘。 在设计和开发的所有阶段都必须遵循这门学科。 早期采用OSGi并在编写一行代码之前应用模块化思维的开发人员会获得巨大的收益,他们发现OSGi实际上非常简单,尤其是当使用可自动生成元数据并进行大量一致性检查的现代OSGi工具链时。在错误到达运行时之前捕获它们。
另一方面,试图将OSGi引入现有的大型代码库的开发人员会遭受苦难,因为该代码很少模块化,不足以使迁移变得容易。 没有强制性模块化的约束,采用破坏封装的捷径太容易了。 正如BEA WebLogic的开发人员告诉我的那样,在BEA被Oracle收购之前:“我们一直以为是模块化的,直到我们开始使用OSGi。”
除了非模块化应用程序之外,非模块化库还阻碍了OSGi的采用。 一些最受欢迎的Java库充满了关于类加载和全局可见性的假设,这些假设在模块化体系结构中被打破。 OSGi为使使用此类库成为可能而做了很多工作,这是OSGi规范明显复杂的源头。 我们需要一定程度的复杂性来处理混乱,复杂的现实世界。
在JPMS中,同样的问题也会出现-可能还会更多,正如我们将很快看到的那样。 如果您的组织以前曾尝试采用OSGi并由于涉及大量迁移工作而放弃了,那么如果您选择迁移到JPMS,则应该至少期望有同样多的工作。 看看Oracle在JDK模块化方面的经验:太多的工作导致Jigsaw从Java 7迁移到Java 8,然后又迁移到Java 9,甚至Java 9都推迟了一年(到目前为止)。 。
拼图计划最初的目标是简单,但是JPMS规范的复杂性已大大提高:模块与类加载器的相互作用; 分层层和配置; 再出口要求; 弱模块 静态要求; 合格出口; 动态出口; 跨层继承的可读性; 多模块JAR; 自动模块; 未命名的模块…所有这些功能都已添加,因为对它们的需求变得清晰起来。 在OSGi中也发生了类似的过程,仅领先16年。
依赖关系:程序包与整个模块
隔离只是模块化难题的一半:模块仍然需要协同工作并进行通信。 在模块之间筑起墙之后,我们需要重新引入连接,但要以受控的方式。 模块系统必须定义模块从其他模块访问功能的方式。 可以静态地,在类型级别上进行此操作,也可以使用对象动态地进行此操作。
静态依赖关系是在构建时可以知道和控制的依赖关系。 如果一种类型是指跨越模块边界的另一种类型,则模块系统需要一种使该类型可见和可访问的方法。 这有两个方面:模块需要有选择地公开其某些封闭的类型,并且需要指定其他模块可以使用的类型。
出口产品
在OSGi和JPMS中,公开类型都是在Java包级别完成的。 在OSGi中,我们使用Export-Package语句,该语句声明某些命名的程序包可以使其他包可见。 看起来像这样:
Export-Package: org.example.foo; version=1.0.1,
org.example.bar; version=2.1.0
该语句出现在META-INF / MANIFEST.MF文件中。 在OSGi成立之初,大多数开发人员都会手动指定这样的语句。 但越来越多的我们更喜欢使用构建工具来生成它们。 现在最流行的模式是在Java源代码上使用批注,并且在Java 5中引入了package-info.java文件以允许包级批注和文档,因此在OSGi中,我们可以执行以下操作:
@org.osgi.annotation.versioning.Version("1.0.1")
package org.example.foo;
这是一个有用的模式,因为可以直接在该软件包中指示导出软件包的意图。 版本也可以在此处指出,并且在包装内容更改2时就可以随时更新。
在JPMS中,程序包按如下所示在module-info.java中导出:
module A {
exports org.example.foo;
exports org.example.bar;
}
注意缺少版本,因为模块和软件包都无法在JPMS中进行版本控制。 我们将稍后再讨论这一点。
进口/需求
尽管OSGi和JPMS在导出方面相似,但在导入方面或取决于其他模块方面却有很大差异。
在OSGi中,导出软件包的补充是导入软件包。 我们使用Import-Package语句导入软件包,例如:
Import-Package: org.example.foo; version='[1,2)',
org.example.bar; version='[2.0,2.1)'
规则是,除以java。*开头的软件包(例如java.util)外,OSGi捆绑软件必须导入其依赖的每个软件包。 例如,如果捆绑软件中的代码取决于org.slf4j.Logger
类型(并且捆绑软件中实际上并不包含org.slf4j
软件包),则该软件包必须列为导入文件。 同样,如果您依赖org.w3c.dom
.Element,则必须导入org.w3c.dom
。 但是,如果您依赖于java.math.BigInteger
,则不会导入java.math
因为java。*包是由JVM的引导类加载器加载的。
OSGi还具有一种用于要求整个捆绑包的并行机制,称为Require-Bundle,但是OSGi规范中已弃用了该机制,并且仅存在于支持非常薄的边缘情况下。 Import-Package的最大优点是,它允许重构和重命名模块,而不会影响下游模块。 这在图1和2中进行了说明。
在图1中,模块A被重构为两个新模块A和A',但是模块B不受此操作的影响,因为它取决于所提供的软件包。 在图2中,我们对A执行了完全相同的重构,但是现在B可能已经损坏了……因为它可能使用了A中不再存在的包(我们在这里不得不说“可能”,因为我们无法分辨B在使用什么来自A ...这就是问题所在!)。
图1:具有导入包的重构模块
图2:具有需求的重构模块
Import-Package语句要手动编写会很麻烦...因此我们不这样做。 OSGi工具通过检查依赖包中已编译类型的依赖关系来生成它。 这非常可靠–比开发人员自己声明运行时依赖关系要可靠得多。 当然,开发人员仍然需要管理其构建依赖关系,这是使用Maven(或您选择的构建工具)以常规方式完成的。 在构建时,在类路径上放置太多依赖关系并不重要:可能发生的最坏情况是编译失败,编译失败仅影响原始开发人员并且可以轻松修复。 另一方面,过多的运行时依赖项可能会破坏模块的可移植性,因为所有这些依赖项都必须拖延,并且可能与另一个模块的依赖项发生冲突。
这导致OSGi和JPMS之间的另一个关键的哲学差异。 在OSGi中,我们始终认识到构建依赖项和运行时依赖项可以而且经常会有所不同。 例如,标准实践是根据API构建并针对这些API的实现运行。 此外,开发人员通常根据我们可能会兼容的API的最旧版本进行构建,但是在运行时,我们选择可以找到的最新版本的实现。 甚至非OSGi开发人员也熟悉这种方法:您通常以准备支持的最低版本的JDK进行构建,同时鼓励用户使用最高版本的JDK(包括其所有安全补丁和性能增强功能)运行。
另一方面,JPMS则采取不同的策略。 JPMS旨在实现“所有阶段的保真度”,以便“模块系统……在编译时,运行时以及在开发或部署的每个其他阶段都应以完全相同的方式工作”(根据JPMS要求 )。 因此,依赖项是在运行时根据整个模块定义的,因为这是在编译时定义它们的方式。 例如:
module B {
require A;
}
此require语句与OSGi不建议使用的Require-Bundle效果相同:模块A可以访问模块A的所有导出包。因此,它与Require-Bundle具有相同的问题:无法从模块声明中确定是否重构模块A的内容是安全的,因此通常这样做绝对不安全。
我们已经发现,使用需求而不是导入的依赖树具有更高的扇出度:每个模块所承载的依赖关系远远超过其实际需要。 这些问题是真实而重大的。 Eclipse插件作者尤其受其困扰,因为出于历史原因,Eclipse软件包倾向于使用需求而不是导入。 我们认为JPMS遵循了这条路线是非常不幸的。
有趣的是,尽管保真/运行时保真度是JPMS的基本目标,但最近的变化已大大削弱了保真度。 当前的Early Access版本允许使用static
修饰符声明要求,这意味着依赖关系在编译时是必需的,而在运行时是可选的。 相反,可以使用dynamic修饰符声明导出,这使得导出的包在编译时不可访问,但在运行时3可以访问(使用反射)。 使用这些新功能,可以创建成功编译和链接但在运行时抛出IllegalAccessError/Exception
的模块。
反思与服务
Java生态系统非常庞大,并且包含用于各种目的的各种框架:从依赖注入到模拟,远程调用,O / R映射等。 这些框架中的许多框架都使用反射从用户提供的代码中实例化和管理对象。 例如,Java持久性体系结构(JPA)是Java EE规范套件的一部分:作为O / R映射器,它必须从用户代码中加载和实例化域类,以便将它们映射到从数据库加载的记录。 在另一个示例中,Spring框架加载并实例化“ bean”类作为接口的实现。
这会给包括OSGi和JPMS在内的模块系统带来问题。 域或bean类是理想情况下应该隐藏在模块内部的东西:如果将其导出,则它将成为公共API,如果消费者依赖于它,则可能导致使用者中断,但我们希望灵活地随意更改我们的内部类。 另一方面,如上所述,通过反射访问非导出类型以支持框架可能很有用。
由于OSGi基于ClassLoader的设计,模块可以获取未导出软件包和其他模块类型的可见性……只要它们知道类型的完全限定名称以及要询问的模块(请记住,几个模块可以包含任何给定的类型名称)。 按照Java长期使用反射的精神,这是实用主义的减少,通过调用setAccessible
方法,甚至可以打开所谓的私有字段。
使用此功能,OSGi中的惯例是提供完全不导出的实现模块! 相反,它们可能包含声明,这些声明引用框架可能加载的内部类型。 例如,使用JPA进行持久化的模块可以引用persistence.xml文件中的域类型,并且JPA实现模块将在需要时加载引用的类型。
最大的用例是实现服务组件。 OSGi规范包括称为声明性服务(DS)的一章,该章定义了模块如何声明组件:其生命周期由框架管理的类。 组件可以绑定到OSGi服务注册表中的服务,并可以选择提供自己的服务。 例如:
@Component
public class CartMgrComponent implements CartManager {
@Reference
UserAdmin users;
@Override
public Cart getOrCreateCart(String user) {
// ...
}
}
在此示例中, CartMgrComponent
是提供服务CartManager
的组件。 它引用服务UserAdmin
,并且该类的生命周期由DS框架管理。 当UserAdmin
服务可用时,将创建CartMgrComponent
并发布一个CartManager
服务,该服务可以类似地由其他模块中的其他组件引用。
该框架之所以有效,是因为它可以加载CartMgrComponent
类,该类已使用@Component批注标记为组件。 定义组件和服务是设计和编程OSGi应用程序的主要方法。
在JPMS中,即使使用反射,也只能访问导出包中的类型。 尽管非导出包中的类型可见(您可以调用Class.forName并获取Class对象),但无法从模块外部对其进行访问。 一旦框架尝试调用newInstance实例化一个对象,就会引发IllegalAccessException
。 这似乎切断了框架的许多可能性,但是还有一些前进的方法。
一种方法是提供单个类型作为服务,可以通过java.util.ServiceLoader
进行加载。 自Java 6起,ServiceLoader已成为标准平台的一部分,并且已在Java 9中对其进行了更新以跨模块工作。 只要提供模块中包含了provides
语句,ServiceLoader便可以访问非导出包中的类型。 不幸的是,ServiceLoader是原始的,不能提供DS或Spring等现代框架所需的灵活性。
第二种可能性是使用包装的“合格”出口。 这是一个导出,仅允许单个命名模块访问,而不是通常所有模块都可以访问。 例如,您可以将bean包导出到Spring Framework模块。 但是,对于JPA这样的操作将失败,因为JPA是规范而非单个模块,并且可以由Hibernate,EclipseLink等不同模块实现。
第三种可能性是“动态”导出,任何人都可以访问该程序包,但只能通过反射,而不是在编译时访问。 这是JPMS的一项非常新功能,在邮件列表中仍然存在争议。 它最接近OSGi的允许方法,但是它仍然需要模块作者为可能包含需要反射加载的类型的每个包显式添加dynamic
导出。 作为OSGi用户,这感觉像是不必要的麻烦。
直到下一期
这就是我们文章的第1部分。 请注意接下来两周内的第2部分 ,其中我们研究了版本控制,动态加载以及OSGi和JPMS未来互操作性的潜力。
关于作者
是Paremus的首席工程师,顾问,培训师和开发人员。 Neil自1998年以来一直从事Java的工作,自2003年以来一直从事OSGi的工作,专门研究Java,OSGi,Eclipse和Haskell。 他是Bndtools eclipse插件(OSGi的领先IDE)的创始人。 他经常在推特(@nbartlett)上发布,涉及所有#OSGi推文,并在Stack Overflow上回答问题,在那里他是OSGi金牌的唯一持有者。 Neil定期为Paremus Blogs撰稿,并撰写第二本书“ Effective OSGi ”,该书将向开发人员展示如何使用最新技术和工具通过OSGi快速提高生产力。
是Bosch Software Innovations的布道者。 他从事OSGi联盟的技术标准化活动已有15年以上。 Kai是OSGi联盟董事会成员,自2008年以来一直是OSGi住宅专家组的联合主席。Kai正在协调各个IoT领域的多项研究项目活动。 他主要关注的领域是智能家居,汽车和一般的物联网,在那里他积极支持产品组合的当前发展和战略定位。
参考资料
1感谢Alex Blewitt的分析 。
2 @Version批注用于表示导出,因为只有导出的软件包需要版本。 在下一个OSGi版本中,计划使用更明确的@Export批注。
3在本文付印之时,此领域于2016年9月12日再次发生变化。 现在,动态导出已替换为“弱模块”概念。 我们仍在评估此根本性更改的影响,请注意,它在Java 9的发布时间表中又造成了四个月的延误。
java 模块化osgi