Java 9,OSGi以及模块化的未来

  在OSGi,导入包是对导出包的补充。使用Import-Package声明导入包,例如:

  Import-Package: org.example.foo; version='[1,2)',

  org.example.bar; version='[2.0,2.1)'

  OSGi中bundle必须导入它所依赖的所有包,除了java.*开头的包,如java.util。例如,如果你的bundle中的代码依赖org.slf4j.Logger(并且你的bundle中实际上并不包含org.slf4j包),那么这个包必须被导入。同样,如果你依赖于org.w3c.dom.Element,那么你必须导入org.w3c.dom。但是,如果你依赖java.math.BigInteger,你不需要导入java.math,因为Java.*包是由JVM的bootstrap类加载器加载的。

  OSGi对于引用所有的bundle还有一个并行机制,称为Require-Bundle,但在OSGi规范中它已经过时了,现在它的存在只是为了支持很小的边缘的案例。Import-Package最大的优势是它允许模块在不影响下游模块的前提下被重构或被重命名。如图1和2所示。

  在图1中,模块A被重构为两个新的模块,A和A',但模块B不受该操作影响,因为它依赖于提供的软件包。在图2中,我们对模块A执行完全相同的重构,但现在B可能是坏的,因为它引用的包有可能不再存在于模块A(在这里我们必须说“可能”,因为我们不知道模块B使用模块A哪些包——这正是问题所在!)。

  图1:通过 Imported Packages 重构模块

  图2:通过 Requires 重构模块

  Import-Package语句手动写是很繁琐的,所以我们不这样做。通过OSGi工具检查依赖生成该语句,并将编译类型内置到bundle中。这是非常可靠的,比开发人员自己声明运行时依赖更可靠。当然,开发人员仍然需要管理自己的编译依赖,按照Maven正常的方式去做(或者你选择的构建工具)。如果编译时把太多的依赖放在classpath下并不会有影响:可能发生的最坏情况是编译失败,这只会影响源头的开发人员并且很容易修复。另一方面,太多的运行时依赖会降低模块的可移植性,因为移植时所有这些依赖关系必须一起移植,而且可能与另一个模块的依赖发生冲突。

  这导致了OSGi和JPMS之间另一个关键的理论差异。在OSGi我们始终认为,编译时依赖和运行时依赖可以并且经常会不同。例如,它的标准做法是,有一套编译时API和一套运行时API。此外,开发人员通常在我们所能兼容的最老的API版本上编译,但会选择可以找到的最新的版本来运行。甚至非OSGi开发人员也很熟悉这种方法:你通常会在准备支持的最低版本的JDK上编译,却鼓励用户在最高的版本(包含所有的安全补丁和增强性能)上运行。

  另一方面JPMS采取了不同的策略。JPMS旨在实现“跨越所有阶段的保真度”,这样“模块化系统应该…在编译时、运行时以及在开发或部署的各个阶段可以以完全相同的方式工作”(来自 JPMS需求 )。因此,依赖关系是在整个模块运行时定义的,因为这就是它们在编译时定义的方式。例如:

  module B {

  require A;

  }

  require语句和OSGi过时的Require-Bundle有相同的效果:模块B可以访问所有模块A的导出包。因此,它也存在Require-Bundle同样的问题:从模块的声明无法确定重构模块A的内容是否是安全的,所以这样做一般是不安全的。

  我们发现,依赖树使用requirements而不是imports有更高程度的扇出:每个模块携带比它真正需要的更多的依赖。这些问题是真实和重要的。尤其是Eclipse插件作者深受其害,因为历史原因Eclipse bundle 倾向于使用requires而不是imports。非常不幸地,JPMS也遵循了这条路线。

  有趣的是,虽然编译/运行时的保真度是JPMS的根本目标,但最近的变化明显减弱了保真度。目前的早期试用版本允许用static修饰符声明requirement,这意味着在编译时依赖是强制性的,但在运行时是可选的。相反,可以用dynamic修饰符声明导出,这可以使导出包在编译时无法访问,但在运行时可以访问(使用反射)。有了这些新特性可能会创建出成功编译和链接,但在运行时抛出IllegalAccessError/Exception异常的模块。

  反射和服务

  Java生态系统是巨大的,包含了用于各种目的的各种各样的框架:从依赖注入到mocking框架、远程调用、O/R映射等。从用户提供的代码来看,许多框架使用反射来实例化和管理对象。例如,Java持久化架构(JPA),它是Java EE套件规范的一部分:作为对象关系映射,为了将domain类与从数据库加载的记录一一映射,必须从用户代码加载和实例化domain类。另一个例子是,Spring框架加载和实例化“bean”类实现接口。