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

  而与此同时,OSGi用了16年时间不断发展和完善。OSGi是应用程序模块化的标准:由于它不是Java平台的一部分,它不能影响平台本身的模块化。但是,许多应用已经受益于它提供的高于JVM的模块化模型。

  高层比较

  JPMS和OSGi之间有很多小的差异,但是有一个很大的不同,就是隔离的实现。

  隔离是模块化系统最基本的特征。每个模块必须有一些保护措施防止运行在同一应用程序中其他模块的干扰。隔离是一个连续的而不是二进制的概念:无论OSGi还是JPMS都需要做一些事情来避免那些表现不好的模块的影响,这些模块占用了JVM中所有可用的内存,运行了数千个线程或者让CPU处于繁忙的循环。如果一个模块可以作为操作系统上独立的进程运行,是可以提供这类保护的,但即使是这样,它也是不完美的;有人仍然可以使操作系统崩溃或者擦除磁盘。

  OSGi和JPMS都提供了代码级隔离,这意味着一个模块不能访问另一个模块的内部类型,除非该模块有明确的许可。

  OSGi通过类加载器实现隔离。每个模块(或者在OSGi术语中称为“bundle”)有一个类加载器,它知道如何在bundle中加载类型。它也可以将类加载请求委托给它所依赖的其他bundle的加载器。该系统是高度优化的,例如,OSGi不会为一个bundle创建一个类加载器直到最后一刻,而且事实上每个加载器会处理一个更小的类型,这样每个类型可以加载得更快。

  这个系统最大的优势是,bundle可以包含重叠的包和类型,而且不会相互干扰。实际的结果是,可能某些包和库有多个版本同时运行在相同的JVM。在处理像Maven这样的构建工具带来的复杂的传递依赖图时,这是个福音。在许多企业,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也传递地依赖于 jansi 1.6和1.9版本,如果包含测试范围的依赖,我们会有另一个slf4j-api的版本1.6。这种混乱是很常见的,传统的Java中没有真正的解决方案,只能逐步在依赖树中添加“excludes”直到奇迹般地得到一套可以运行的依赖库。不幸的是对于这个问题JPMS也没有答案,我们很快就会看到。

  使用类加载器进行隔离确实有一个缺点:它打破了每一个类型最多可以在一个位置找到的假设。这是模块化的一个自然结果。如果一个模块可以不受其他模块的干扰使用自己的类型,那么不可避免地一个单一类型的名称可能会在多个模块中发现。遗憾的是,这造成了一个问题,因为很多保留的Java代码不是用模块化的思想编写的。特别是,调用Class.forName(String)通过名字查询类型时,在真正模块化的环境中不是总能得到正确的结果,因为有多个可能的返回类型。

  正是由于这个缺点,不能使用OSGi模块化JDK本身。JDK的许多地方都有一个隐含的假设,任何JDK类型可以从JDK的任何其他部分加载,所以很多事情在OSGi下会被打破,比如模型。为了解决这个问题,也为了减少使用Class.forName代码的迁移,JPMS选择在隔离时不使用类加载器。当你在“modulepath”使用一组模块来启动应用程序时,所有这些模块将由相同的类加载器加载。相反,JPMS引入新的访问规则实现隔离。

  OSGi的隔离屏障是 可见的 。在OSGi,我们不能加载一个模块的内部类,因为它们是不可见的。也就是说,自己模块的类加载器只能看到自己模块内部的类型以及从其他模块明确导入的类型。如果我试图从其他的模块中加载一个内部类,我的类加载器是看不到该类型的。就好像是根本不存在的类型。如果试图继续加载该类,就会得到NoClassDefFoundError或者ClassNotFoundException的异常。

  在JPMS,每一个类型对于任何其他类型都是可见的,因为他们存在于同一个类加载器。但是,JPMS增加了辅助检查以确定加载类有权访问它试图加载的类型。其他模块的内部类型实际上是private的,即使它们被声明为public。如果我们试图继续加载它,那么我们会得到IllegalAccessError或者IllegalAccessException的异常。如果我们试图加载private的或者另一个包的默认访问类型也会得到相同的错误,而且在这个类型上调用setAccessible也是无用的。这改变了Java中public修饰符的语义,以前它是普遍可访问的,现在只可在一个模块和它的require对象中访问。