Oracle Java平台组首席架构师Mark
Reinhold发表了一份关于模块化系统的情况的报告,强调了模块化的目标是什么。由于和已经存在的框架存在明显的重复,特别是OSGi,报告引起发了人们的讨论。

Java 9
中一个重要的新特性就是模块化。它的实现机制是什么那?它和已有的模块框架OSGi有什么差异那?为了回答这些问题,本人在网上找到了一篇比较好的介绍文章,为了加深理解,对文章进行了翻译。由于原文分为2个部分,所以翻译对应也分为2篇:

本文是对《OSGi入门教程》课程的笔记。

正如报告中所解释的,以及在JSR-376和模块化系统项目主页中完整的详细说明,模块化系统是为了解决当前Java访问模型中的两个疏漏:

1)《Java
9,OSGi和模块化的未来(1)》是对《Java
9, OSGi and the Future of Modularity (Part
1)》的翻译,文章日期为2016年9月22日。介绍的内容包括:背景、高层次比较、复杂性、依赖粒度对比、模块导出对比、模块导入对比、反射和服务。

OSGi基础概念

  • 可靠的配置:当前一个组件通过类路径访问另一个组件的类时相当容易出错,特别是尝试使用不在类路径里面或者存在多个版本的类的时候。

  • 强封装:没有办法去限制特定组件暴露给其他组件的类,外部能访问所有的公共类。

2)《Java
9,OSGi和模块化的未来(2)》是对《Java
9, OSGi and the Future of Modularity (Part
2)》的翻译,文章日期为2016年10月4日。介绍的内容包括:动态性、二者协同工作、未来发展、结论。

基本概念

完整的细节能够在报告和InfoQ之前的文章中找到,总的来说,每一个组件通常(但不一定)作为一个jar文件,其中包含了如下结构的模块描述文件module-info.java:

本文是对原文第一部分的翻译。

OSGi的一些定义

  • Open Services Gateway initative 开发服务网关协议
  • 是指Java的动态模块化系统的一系列规范。(OSGi联盟,osgi.org)
  • OSGi Alliance 组织
    以及该组织指制定的一个基于Java语言的服务规范。(Wiki)
  • Java 平台的模块层。(《OSGi in Action》)
module com.foo.bar {

   requires com.foo.baz;

   exports com.foo.bar.alpha;

   exports com.foo.bar.beta;

}

引言

关键点

  • Java 9
    在2017年发布,其中一个重要的特性就是新的模块化系统,被称作Java平台模块系统(Java
    Platform Module
    System,JPMS)。本文将介绍它与Java已有的模块标准化机制OSGi的关系,以及对OSGi的影响。
  • 自1.0版本以来Java平台已经增长了20倍,整个平台存在着模块化的需求。为了解决这个问题,进行了很多不成功的尝试。与之相对的是,OSGi已经提供了16年的应用模块化服务。
  • OSGi和JPMS在实现细节上差别很大。如果将JPMS作为模块化的一般解决方案,会存在一些重大的缺陷,并且缺少OSGi的一些特性。
  • JMPS的目标是比OSGi更简单和更容易,但是对一个非模块化的产品进行模块化设计本身就是一件很复杂的事情,JMPS看起来好像还没有实现这个目标。
  • JMPS在对Java平台本身的模块化方面做得非常好,这意味着我们可以在运行时只加载和任务相关的Java平台组件。对于应用模块化,OSGi有很多的优点。我们已经证明这两者可以很好地结合在一起。

这篇文章是“Java 9, OSGi and the Future of
Modularity”文章系列的第一部分。第二部分请查看《Java 9, OSGi and the
Future of Modularity (Part
2)》。

当Java 9发布的时候,其中一个重要的特性就是模块化:Java平台模块系统(Java
Platform Module
System,JPMS)。虽然JPMS的具体细节还不是很清楚,但我们已经对其基本内容有了一定的了解。

从2000年开始,Java中的模块系统就以各种形式存在了。其中一个比较著名的模块化系统叫做OSGi,这是一个独立于供应商的行业标准,由OSGi联盟制定。OSGi联盟由领先的软件供应商、电信公司和其他组织组成,包括:Adobe、Bosch、Huawei、IBM、Liferay、NTT、Oracle、Paremus、Software
AG。它几乎能够支持所有的Java
EE应用服务器、最流行的IDE、web应用(eBay、Salesforce.com、Liferay),并且它也被政府和军方使用(the
U.S. Air Force、Federal Aviation Administration)。

OSGi适用于IoT(物联网)。一开始,OSGi是为嵌入式系统设计的。在几年前,这些系统的内存和CPU资源都是很有限的。现在,这些设备具备更多的资源,这为构建复杂的应用系统提供了机会,并且催生一大批企业和个人对软件和硬件进行着贡献,形成了一个生态。这样的生态系统存在于多个市场,包括:智能家居联网、车联网、智慧城市、工业4.0(IIoT)。此外,网关常用于传感器与设备、设备与后端系统的互联。应用和服务可以在本地网关运行,也可以在云端运行。

OSGi提供了很多规范,从而使得构建开放物联网生态的基本特性能够实现。这些特性包括:设备管理、软件配置、对设备的底层通信协议进行抽象。一些公司,包括:AT&T、Bosch、NTT、Deutsche
Telekom、General Electric、Hitachi、Miele、Schneider
Electric等,在构建IoT解决方案的时候,可以通过使用OSGi技术获得更好的收益,而这也有段时间了。通过OSGi和IoT,现在已经是万物互联的社会了。

当然,OSGi使用者比较关注的是:Java
9中的模块化系统会对OSGi造成什么影响,包括短期和长期。

由于技术、政治、经济等原因,Java生态中存在了两个模块系统。在这篇文章中,我们避开了政治的原因,从技术的角度进行了比较。我们描绘了一个JPMS和OSGi一起工作的场景,在这个场景中,它们有各自擅长的领域和机会。

需要注意的是,本文使用了截至2016年8月能够获得的公开信息,有些细节可能在规范最终出来之前还会更改。

OSGi生态

图片 1

OSGi生态

文件结构中包含一行或多行exports用于指出能够被其他组件访问的包,零行或多行requires用于指出自身需要访问的其他模块。该系统提供
了方法用于在编译时评估访问类型是否具有正确的可见性(例如声明为公共类并被所需组件导出),并在运行时评估必需的模块是否可用,而不需要去检查完整的类
路径。类似于OSGi中的清单文件。

背景

Java平台从1990年代开始迅速发展。从下载大小来看,JDK 1.1的大小在 10Mb
之内,而 Mac OS X 平台下载的JDK 8u77
达到了227M。安装空间和内存需求也相应需要增加。Java平台扩大是由增加新特性驱动的,这些特性的大部分都是受欢迎和有用的。但是,平台中的每个新功能都会给不需要它的用户造成膨胀,并且没有人会使用到全部的特性。此外,为了使Java具备向后的兼容性,其中一些特征在废弃之后仍会被保留。

很多年来,Java平台变大并不是一个大问题。Java是最受欢迎的企业级平台,并且它的竞争对手(微软的
.NET)也和Java有着相似的发展轨迹。但是,现在Java面临了不同的挑战。IoT的发展使得空间占用被重新关注了起来,并且一些新的灵活的平台和语言(如Node.js、Go)也是非常厉害的竞争对手。

此外,安全同样是一个大问题:为了避免来自Java的攻击,有安全意识的组织会从用户桌面完全删除它。如果JVM内部和用户空间的应用代码有更好的隔离性,那么很多攻击可能就不会发生。

很长一段时间来,大家都很明确的是:需要对Java平台进行一些模块化工作。在2000年代中期,有过一些努力,但是都没被采用,包括JSR
294和它的“superpackages”、JSR 277的“Java Module System”。最终,一个称为
Jigsaw 的原型项目出现了。这本来打算在2011年的Java
7中发布的,但是后来推迟到了Java 8,接着又被推迟到了Java
9。作为一个原型工程,Jigsaw 为 JPMS 规范提供了实践参考。

与此同时,OSGi已经发展和改进了16年。OSGi是应用模块化的标准,由于它不是Java平台本身的一部分,所以它不能影响平台本身的模块化。但是另一方面,许多应用程序都得益于它在JVM级别之上提供的模块化模型。

模块化

将大型系统分解为多个模块,通过设置模块的边界来改善系统的可维护性和封装性。

OSGi的背景

高层次比较

JPMS和OSGi之间在细节上有很多不同点,但是它们之间最大的区别在于:实现隔离的方法。

对于任何一个模块化系统,隔离性都是最基本的特性。必须有一些方法来保护每个模块不受同一应用程序中其它运行模块的干扰。隔离并不是一个二元概念(隔离或不隔离),OSGi和JPMS都不能阻止一个模块干这些“坏事”:耗尽JVM中的可用内存、启动数千个线程、通过忙循环挂起CPU。将模块运行在OS独立的处理器中可以提供一些隔离保护,但是即使这样也不是万无一失的,一些人仍旧可以使OS奔溃或者擦除磁盘中的数据。

OSGi和JPMS提供的都是代码级别的隔离,这意味着一个模块不能随意访问另一个模块内部的类型,除非有另一个模块的显示允许。

OSGi通过类加载器来实现它的隔离性。每个模块(OSGi中的术语是“bundle”)有它自己的类加载器,这些类加载器知道如何加载bundle中的类型。此外,也可将类加载的请求委托给依赖的bundle。OSGi的系统是高度优化的,例如:OSGi直到bundle真正需要被使用的时候才会创建类加载器;并且事实上每个类加载器只需要加载部分类型,这也意味着加载速度可以有所提高。

这个系统有一个很大的优点是,多个bundle可以包含重复的包和类型,并且这些元素不会相互干扰。实际的效果是:可以同时在一个虚拟机上运行包或库的多个版本。当使用构建工具(例如
Maven)时,这个模块化机制有助于处理复杂的传递依赖。在很多企业级Java应用中,几乎不可能实现应用的每个依赖里面使用的都是同一个版本的类库。

例如,我们看一下JitWatch库。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版本,并且如果我们包括测试域的依赖,我们还会依赖另一个1.6版本的slf4j-api。这种混乱状况非常常见,传统的Java并没有真正有效的解决办法,除了在依赖树上逐渐添加“excludes”,直到我们可以奇迹般地让一堆依赖工作。但不幸的是,JPMS对此还没有确切的解决方案,等Java
9发布后我们就可以看到是否如此。

正是这个缺点阻止了OSGi被用来模块化JDK本身。JDK的很多部分都有一个隐含的假设就是:任何一个JDK类型都可以从JDK的其它部分中加载进来。因此,如果使用类似OSGi的模型的话,很多事情都无法完成。为了解决这个问题,同时为了可以轻松地从使用
Class.forName
的代码迁移过来,JPMS并没有全部选用类加载机制来实现模块的隔离。当你启动了一个应用,并且使用了“modulepath”下的一些模块,那么这些模块会被同一个类加载器加载。JPMS引入了一个新的访问机制来实现这个目标。

这个隔离栅栏在OSGi中被称为可见性。在OSGi中,我们不能加载模块内部的类,因为这在外部是不可见的。这也意味着,我模块的类加载器只能看到我自己模块内的类型、以及从其它模块明确引入的类型。如果我尝试从你的模块中加载一个类,我的模块是无法看见那些类型的,看起来好像那些类并不存在一样。如果我无论如何一定要加载那些类的话,会得到
NoClassDefFoundError 或者 ClassNotFoundException

在JPMS中,每个类型可以看到其它任何类型,因为它们都在一个类加载器中。但JPMS增加了额外的检查,从而确保尝试加载的类有正确的访问权限。其它模块中的内部类型实际上是私有的,即使被声明为
public。我们如果强制要加载的话,会得到 IllegalAccessError
IllegalAccessException。加载其它模块中的私有类型和默认类型也会得到同样的错误,并且调用
setAccessible
这样的函数是没有作用的。这改变了Java中public的语义,原先意味着任何地方都可以访问,现在意味着只有在模块内部或者声明了require的地方才可见。

JPMS这种做法的缺陷是:不能实现多个模块都加载重叠的内容。这意味着,如果两个模块都包含一个私有(没有导出)的包
org.example.util,那么这些模块不能在模块路径下被同时加载,这会导致
LayerInstantiationException。虽然我们可以在自己的应用中创建类加载器来绕过这个限制,但是OSGi已经为我们实现了。

这完全是通过设计,来使得JPMS可以对JDK内部进行模块化。但是这样造成的影响是,你不能让内部实现细节冲突的模块一起工作。

Java模块化的局限性

OSGi是一个基于Java的模块化系统和服务化平台,实现了一个完整的动态的组件模型。从1998年在JSR-8第一次提出,在随后的审核中被延
迟发布(最近一次是2014年),OSGi定义了bundle(类似模块),采用包含如下的MANIFEST.MF文件的JAR文件的形式:

复杂性

关于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已经延迟一年发布了。

Jigsaw是以简洁性为目标开始的,但是JPMS规范已经变得越来越复杂:模块与类加载器的相互作用、分层与配置、再次导出依赖、弱模块、静态依赖、规范的导出、动态导出、跨层的读可继承性、多个Jar包的多个模块、自动化模块、未命名模块,等等。这些所有的特性都被添加到规范中去了,因为它们需要被清晰地说明。OSGi也发生了相似的过程,只是领先了16年而已。

可见性控制不够

  • 通过Package来组织和划分代码,可见范围可以分为Private、Package、Protected、Public这几类。
  • 存在的问题:如果是Public的话,任何人都可以访问,希望可以进一步控制(一种方案可以是通过一些模式进行模拟实现,另一种可以是OSGi)。
Bundle-Name: Hello World

Bundle-SymbolicName: org.wikipedia.helloworld

Bundle-Description: A Hello World bundle

Bundle-ManifestVersion: 2

Bundle-Version: 1.0.0

Bundle-Activator: org.wikipedia.Activator

Export-Package: org.wikipedia.helloworld;version="1.0.0"

Import-Package: org.osgi.framework;version="1.3.0"

依赖:包 vs 模块

隔离只是完成了模块化难题的一半,另一半是:模块需要一起协同工作。在构建了模块之间的隔离之后,我们还需要引进一个可以控制的交流机制。这个可以在类型级别静态实现,也可以通过对象动态实现。

静态依赖是指那些在构建时期就能被知道和控制的部分。如果需要穿过模块的边界访问一个类型,那么模块需要提供一种提供类型可见性和访问控制的方法。这包括两个方面:模块需要选择性地暴露一些它们封装的类型,并且它们需要明确指出需要使用其它模块中的哪些类型。

Jar包灾难

  • ClassPath中的Jar包在运行时没有明确地模块边界。
  • 多个版本的Jar同时存在时,加载具有不缺定性。Jar Hell(Jar包灾难)。

(例子来源于Wikipedia)

导出:Exports

在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;

这是一个有用的模式,因为导出包的意图可以直接在该包中表示。版本号也可以在这里声明,而且当包中内容发生变动时,也很容易在附件进行修改。

在JPMS中,包导出声明是在 module-info.java 文件中,举例如下:

    module A {
        exports org.example.foo;
        exports org.example.bar;
    }

需要注意的是:在JPMS中,模块和包都无法标注版本号。这部分内容我们之后会再次讨论。

部署和管理缺少支持

  • 模块的动态更、系统动态演化。
  • 插件化开发。

显而易见的,尽管格式不一样,但是目的表现的和Java模块系统很相似。的确,Java平台模块系统和OSGi之间的相似性,在2005年提出的
JSR-277,“Java模块系统”中,初次尝试模块化Java时就已经被注意到了。最初瞄准的Java
7,JSR-277专注简化分发和执行Java程序包。尽管有和JSR-376几乎一样的名字,但两者最初的目标有着细微差别;虽然它的任务是修复“可靠
的配置”问题,但是它并不试图解决“强封装”的问题。相对于JSR-376,它也尝试增加一个版本模型到Java程序包中。这些目标和OSGi提供的功能
之间的相似性是完全足够让作者在最初考虑把OSGi作为一个解决方案,放弃的原因考虑为OSGi的版本控制太弱了。

导入:Imports/Requires

OSGi和JPMS在模块导出的部分很相似,但是它们在导入或者依赖其它模块的部分差异很大。

在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存在并行机制来导入所有的模块,被称作
Require-Bundle,但是这个在OSGi规范中已经废弃了,存在也只是为了支持一些非常稀少的边缘案例。Import-Package
机制的最大优点是:可以在不影响下游模块的情况下,实现模块的重构和重命名。图1和图2对此进行了说明。

在图1中,模块A被重构为两个模块,A和A’,但是模块B没有受到这个操作的影响,因为它依赖的是包。在图2中,我们对A进行了同样的操作,但是现在B奔溃了,因为它可能使用的包在A中不再存在了(这里说“可能”是因为我们无法知道B使用了A中哪些部分,因为我们只说明了依赖的模块,这就是问题所在)。

图片 2

图1 Import机制中重构模块

图片 3

图2 Require机制中重构模块

Import-Package
声明手动书写很麻烦,所以我们并没有这么做。OSGi工具可以通过检查bundle中编译类型的依赖来生成它。这是非常可靠的,比开发人员自己声明运行时依赖要可靠得多。当然,开发者还是需要管理它们的构建依赖,这通常可以通过Maven进行管理(或者你可以自己选择构建工具)。在构建期间,如果你在类路径上放置了很多依赖,这并不会造成特别大的问题:最糟糕的情况也只是编译失败,而这仅会对开发者造成影响,并且很容易被修复。另一方面,在运行时如果有太多依赖的话会降低模块的可移植性,因为所有的这些依赖都需要被移植,这可能会与移植环境中其它模块的依赖形成冲突。

这引出了OSGi和JPMS之间的另一个重要哲学差异。在OSGi中,我们总是意识到构建时期的依赖和运行时的依赖常常会不同。例如,通常我们需要构建API和运行API的具体实现。进一步,开发者通常会构建老版本的API来保持兼容,而我们在运行时会选择最新的实现版本。即使是非OSGi的开发者也会对此非常熟悉:通常构建的时候会支持低版本的JDK,但还是会鼓励使用者运行高版本的JDK,因为高版本中会有一些安全补丁,并对运行能力有所增强。

另一方面,JPMS则采取了不同的做法。JPMS的目标是实现“所有阶段的保真度”,
所以“模块系统应该在编译时、运行时以及在开发或部署的每个阶段中,都以完全相同的方式工作”(摘自
JPMS
Requirements)。因此运行时依赖是以整个模块为粒度定义的,因为这样能够和它们编译时期的定义保持一致。示例如下:

    module B {
        require A;
    }

这个 require 声明和OSGi中被废弃的 Require-Bundle
有着相同的效果:模块A中所有被导出的包都可以在模块B中使用。这也导致了它和
Require-Bundle
有着相同的问题,从模块依赖声明当中没有办法得知:对A中内容进行重构是否安全。所以,通常来说这样做并不安全。

我们发现在依赖关系的树形结构中,使用 require 的声明会比使用 imports
的声明具有更大的出度(节点出去的边数):每个模块需要关注比实际需要的更多的依赖。这些问题是真实并且重要的。Eclipse的插件开发者尤其受到了它们的影响,因为历史原因,Eclipse
bundles 往往使用 require 而不是
import。我们觉得JPMS采用这种方案是很不幸的一件事情。有趣的是,尽管编译/运行时保真是JPMS的基本目标,但是JPMS最近的一些改变显著地降低了保真度。当前早期的一个版本允许require声明使用静态修饰符,这意味着依赖在编译时强制性的,但是在运行时是可选的。相反的是,export
声明可以被动态修饰符修饰,这意味着导出的包在编译时期是无法访问的,但是在运行时期是可以访问的(通过反射)。这些特性会导致这样的情况:在编译时期可以成功地创建编译和链接模块,但是在运行时期会抛出
IllegalAccessError / Exception

OSGi三层架构

不久之后创建的JSR-294的目标是实现“改进Java程序语言的模块性支持”。同时针对Java
7,JSR-294新增了模块概念(所谓的“超级包”)来修复强封装的问题;这个概念匹配了当前的Java平台模块系统项目。当Java
7的目标被放弃时,JSR-277和JSR-294从2012年开始被冻结,由JSR-376代替。

反射和服务

Java的生态系统非常巨大,包含一系列用于各种目的的框架,如依赖注入、模拟对象、远程调用、对象映射等等。这些框架中的大部分都使用了反射来实例化和管理用户代码中的对象。例如,Java
持久化框架(JPA),它是Java EE规范中的一部分:作为一个 O/R
映射器,它需要从用户的代码中加载和实例化领域类,从而使得这些实例能够匹配从数据库中加载到的记录。另一个例子是,Spring
框架中会加载和实例化一些接口的实现类 “bean”。

这会对模块化系统带来问题,包括OSGi和JPMS。理想情况下,一个领域或对象类应该被隐藏在模块中:如果它被暴露了,那么它成了一个公共的API,如果有消费者依赖它的话,可能会对它造成影响,而我们希望能够按照意愿灵活地改变我们的内部类。另一方面,可以通过反射来访问一些没有被模块导出的类型,这对于一些框架是非常有用的。

由于OSGi是基于类加载机制设计的,模块可以获得那些没有导出包和类型的模块的可见性,只要它知道类型的完全限定名和需要访问的模块(注意,多个模块可以包含任意的类型名)。Java开发中,反射思想的长期运用会破坏隔离性,因为即使是私有的域也可以通过
setAccessible 方法来修改可见性。

通过这种功能,OSGi通常可以提供模块的实现,但是不需要导出。此外,它们可能包含一些内部类型的声明,而这些类型可能是由框架加载。例如,一个使用JPA来说进行持久化的模块,可以通过
persistence.xml
文件来引用领域类型,JPA实现模块会在需要使用的时候加载引用的类型。

最大的用例在于实现服务组件。OSGi规范中有一章内容是声明性服务(Declarative
Services,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
来获得一个类对象),但是它们还是还是无法在模块外部被访问。当框架调用
newInstance 来实例化对象的时候,将会抛出一个
IllegalAccessException。这视乎削减了框架的很多可能性,但是还是有一些可以采用的方法。

第一种方法是提供个人类型来作为服务,这可以通过 java.util.ServiceLoader
来加载。ServiceLoader 从Java
6开始就已经成为平台标准的一部分了,并且在Java
9中会被更新来支持进行模块间的工作。ServiceLoader
可以访问没有导出的包,只要模块提供者包含一个提供声明。不幸的是,ServiceLoader
是原生的,而且无法为当前的一些框架(比如,DS 或 Spring)提供灵活性。

第二种方法是通过使用“合格”的包导出。这是只对单个命名模块访问的导出,而不是所有模块都能访问。例如,你可以导出类所在的包给Spring
框架。但是,这样的方式会在JPA中的某些方面遇到失败,因为JPA是一个规范,而不是一个单一的模块,它可以由多个不同的模块实现,比如:Hibernate、EclipseLink、等等。

第三种方法是“动态”导出,这样包可以被任何人访问,但是只能通过反射的方式,在编译时期是无法访问的。这是JPMS很新的一个特性,而且它还有所争议。这是最接近OSGi允许策略的方法,但是对于那些可能会被反射加载的类型,还需要为它们所在的每个包都添加
dynamic
导出来获得访问权限。对于OSGi用户而言,这就像是一种不必要的并发症。

模块层:定义了OSGi中的模块Bundle。

  • Bundle是一个具有额外元数据的Jar包。
  • Bundle定义了包含的包的可见性(对外暴露哪些模块)。
  • Bundle定义了所依赖的包。

图片 4

Bundle所包含内容

OSGi和模块化Java的另一个关联能在JSR-291中找到,“Java
SE的动态组件支持”(本质上是OSGi服务平台发布的第四版)。JSR-291参考了JSR-277,作为最早的Java模块系统,来主动区分两者之间
的不同作用:JSR-277关注Java中使用的静态模块定义,而JSR-291关注运行时动态组件的加载和卸载。

下一篇

本文是文章的第一部分。下一篇将介绍OSGi和JPMS中的这些主题:版本、动态加载、未来二者协同工作的可能性。

生命周期层:提供运行时管理和框架访问接口。

  • 提供对模块生命周期的操作(install,update,start,stop…),使得程序外部可以动态管理和演化系统。
  • 定义了运行时上下文的接口,bundle本身可以和OSGi框架进行交互,从而实现内部自我管理。

最后,JSR-376也参考了OSGi,放弃它作为有效解决方案的主要原因是因为它的范围远远大于Java框架模块系统规范。

服务层:关注模块间的交互。

  • 服务层是JVM中的SOA(面向服务的架构)。
  • 服务是动态的,促使你使用基于接口编程。
  • OSGi服务是Java Interface,服务调用是Java对象方法调用(Lightweight
    Tech,轻量级技术)。

综上所述,多数人难以区分新的模块系统和OSGi是正常的。但是,模块系统和OSGi相互补充,用以服务于不同的目标,OSGi作为一个构建于
Java之上的框架,在一个持续运行的应用中,创建了一个用以动态管理bundle的环境,而模块系统作为Java本身的新能力能够更紧密更简单的控制静
态模块。

OSGi介绍补充

Java平台模块系统和OSGi的区别

为了更好的理解这一点,InfoQ请教了Holly Cummins,《Enterprise OSGi in
Action》的作者之一。不过以下内容并不会彻底说清楚Java平台模块系统和OSGi之间的差异,只是为读者提供一个基本的理解关于两者目标的区别。

一方面,Java新的模块系统将会提供更简单的方式在编译时检查包和类的可见性,但是当我们询问OSGi的bundle是否能用相同的方式使用时,Holly表示“答案是相当的复杂”。

OSGi通过bundle的清单文件来描述依赖,有两种基本方式来创建清单文件:“代码优先”和“清单文件优先”。代码优先方式(使用bnd工
具,和maven
bundle插件),并不会在编译时检查依赖列表,实际上它是在编译时生成的。在通常方式的编译过程中,工具会根据编译时的依赖统计出运行时的依赖。另一
种方式是清单文件优先,通过Eclipse
PDE工具来使用。在这种方式中,依赖被声明在清单文件中,Eclipse
IDE会使用这份清单文件统计出你的代码能访问的类,并高亮缺失的依赖。PDE命令行构建和一个叫做Tycho的Maven插件,都会在编译时检查依赖关
系。但是,请注意OSGi自己并不会检查可见性,而是通过PDE自带的工具;因为不是所有使用PDE的团队都会使用其中的工具,有时候可能会在编译时造成
缺少依赖。

新的模块系统的另一个重要方面是能够限定指定包对模块的导出,这是很有用的当一批相互关联的模块需要互相访问,但却不能访问其他内容时。正如Mark
Reinhold在报告中指出的,这能够用以下语法来实现:

module java.base {
   ...
   exports sun.reflect to
       java.corba,
       java.logging,
       java.sql,
       java.sql.rowset,
       jdk.scripting.nashorn;
}

OSGi最初并没有这个能力,但增加之后能够让它比模块系统的目标走的更远。Holly解释道,“所有的bundle都能够注册成一个解析拦截器,
用以过滤匹配,因此包只会暴露给指定的bundle。当导出声明了确定的元数据的包给bundle时,你能够使用相同的机制来预测,或者只是在每周二疯狂
的导出包”。

查看英文原文:Oracle Publishes Report on the State of Java’s Module
System

转载自:infoq.com/cn

OSGi严格的模块化特性的最大优势

  • 基于接口编程,完全隐藏实现(促进你从架构上思考)。
  • 动态新(对扩展开发,即使是运行时)。

OSGi传播的阻力

  • 最初面向的嵌入式领域,容易被误解为只是嵌入式技术。
  • 有些人觉得OSGi复杂,人为属于重量级。
  • Lib支持不够:很多Lib不能再OSGi环境下运行。

企业中的OSGi

  • 企业应用特点:持久化数据、数据量大、访问并发等。
  • 需要模块化:前端/事务/持久化分离、运行多个服务器上、需要协作化开发。
  • 存在问题:企业开发中其它框架会使用TCCL(SPI模式)、反射,这些技术在OSGi类加载机制下会有问题。

小节

  1. OSGi提供了更粗粒度的模块化特征,可以解决Java模块化的局限性。
  2. OSGi中声明式和基于元数据的方法是非侵入式的。
  3. 生命周期层定义了模块动态且可控的生命周期模型,简化了系统管理。
  4. 服务层鼓励采用基于接口编程的方法,从而将接口与实现进行分类。

OSGi模块层

模块化

与面向对象的关系

  • 都是“关注点分离”(分治)思想的体现,但关注的粒度不一样。
  • 在实现特定功能时,需要设计类以及类之间的关系,此时需要面向对象的原则和模式。
  • 当把相关的类在逻辑上组织在一起的时候,需要关注系统模块和模块间的关系。
![](https://upload-images.jianshu.io/upload_images/8118972-9f42924463d918e9.png)

模块化设计

模块化的意义

  • 解决Java模块化的局限:可见性控制不够、Jar包灾难、部署和管理缺少支持。
  • 通过显示定义能力(Export-Package)和依赖(Import-Package),可以优化设计,让系统更加“高内聚低耦合”(软件开发的终极目标)。
  • 促进协同开发,提高开发效率。

Bundle

基本概念

  • Bundle是一个包含代码、资源、元数据,以Jar的形式存在的模块化单元。
  • Jar文件的边界也是模块的边界。Jar包是bundle中代码的物理容器。
  • Mainifest.mf文件保存了模块的元数据。

元数据定义

  • 元数据信息定义在/META-INF/MANIFEST.MF中,OSGi R5规范定义了28个标记。
  • 元数据有三类标记:可读信息、Bundle标识(Identification)、代码可见性。

依赖解析

OSGi类查找顺序

  • 如果类所在包以“java.”开头,委托给父类加载器。
  • 如果类所在的包在导入包中,委托给导出该包的Bundle。
  • 在Bundle自身的类路径上查找。

多个提供者的选取规则

  • 已解析的(resolved)的bundle优先级高,未解析的(installed)bundle优先级低。
  • 相同优先级多个匹配时,版本高者优先,版本相同则先安装的优先。
![](https://upload-images.jianshu.io/upload_images/8118972-3b722ddb9cf3f842.png)

解析问题

uses子句使用

  1. 使用uses子句来解决类空间不一致的问题。
  2. uses约束是可传递的。
  3. 谨慎使用uses,会大大限制解析的灵活性。
  4. 使用场景如下。
  • 导出包中的类,其方法签名中包含了其Import-Package中的类。
  • 导出包中的类,继承了其Import-Packeage中的类。

图片 5

use子句使用

OSGi生命周期层

基本概念

生命周期管理

  • 通过外部或内部对应用进行操作,完成对应用的“生命周期管理”过程。
  • 对于非模块应用,这些操作是以整个应用为对象。
  • 对于模块化应用,可以有更细粒度(针对某个模块)的生命周期管理。

图片 6

生命周期

生命周期层的作用

  • 在应用外部,该层精确定义了对bundle生命周期的相关操作。
  • 在应用内部,该层定义了bundle访问其执行上下文的方式,为bundle提供了一种与OSGi框架交互的途径。
  • 对生命周期的操作允许你动态地改变运行于框架汇中的bundle组成,并以此来管理和演化应用程序。

生命周期层状态转移

图片 7

生命周期层状态转移

使用生命周期层

  • BundleActivator是生命周期层的基础设施,如下所示。

public interface BundleActivator{
    public void start(BundleContext context) throws Exception;
    public void stop(BundleContext context) throws Exception;
}
  • bundle属于active时,BundleContext才有意义,即start方法被调用和stop方法被调用的两个时间点之间。
  • BundleContext包含部署和生命周期管理相关接口、与bundle间服务交互相关的接口。
  • Bundle定义了一系列API,用于管理已安装的bundle的生命周期。
![](https://upload-images.jianshu.io/upload_images/8118972-d3f20218ec2866b7.png)

BundleContext接口

图片 8

Bundle接口

Bundle的更新

两阶段更新

  • 两阶段更新:先update,再显示refresh。
  • 为什么两阶段?对外输出后,其它模块会使用旧版本,需要刷新,可参考《bundle动态更新》。

图片 9

Bundle更新

刷新流程

  1. 从bundle开始计算受影响的bundle有向图。
  2. 处于ACTIVE状态的bundle被停止并被切换至RESOLVED状态。
  3. 处于RESOLVED状态的bundle,切换至INSTALLED状态,这些bundle的依赖关系不再被解析。
  4. 处于UNINSTALLED状态的bundle会从图中移除,同时也会被彻底地从框架中移除(GC)。
  5. 其它bundle如果在框架重启前处于ACTIVE状态,重启框架会对这些bundle及其依赖的bundle进行解析。
  6. 所有工作完成后,框架会触发一个FrameworkEvent.PACKAGES_REFRESHED事件。

小节

  1. BundleActivator是bundle的入口,与Java应用中的main函数类似。
  2. BundleContext为应用提供执行时操作OSGi框架的方法。
  3. Bundle代表了一个已安装到框架中的bundle,允许对其执行状态进行操作。

OSGi服务层

基本概念

什么是服务

  • 为别人完成的工作(经典定义)。
  • 指提供者及其使用者之间的一个契约。服务可以被替代,能够发布和查找。
  • 使用者不关心具体实现,只关心约定的契约。

面向服务的设计

  • 降低服务提供者和使用者之间的耦合,更容易重用组件。
  • 更强调抽象接口而不是具体实现。
  • 清晰描述依赖关系(可以通过附加元数据描述)。
  • 支持服务的多个实现方案以及动态替换。

OSGi服务

OSGi服务模型

  • 拥有一个集中的服务注册中心,遵循发布-查询-绑定模型。
  • 提供者bundle可以将POJOs(Plain Ordinary Java Object)发布为服务。
  • 使用者bundle可以找到并绑定服务。

图片 10

OSGi服务

OSGi服务注册、更新与注销

  • 服务注册对象是私有的,不能被别的bundle共享,它们与发布服务的bundle的生命周期是绑定的。
  • 不推荐使用具体类名进行服务注册。
  • 当一个bundle停止时,任何没有被移除的服务都会被框架自动移除。
  • 服务排序:先按service.ranking由大到小排序,然后再按service.id由小到大排序。
  • 按属性查询:使用LDAP过滤字符串(LDAP,Lightweight Directory Access
    Protocol,轻量目录访问协议)。

服务使用

// 注册中心会增加一个使用计数。
A serviceA = (A)bundleContext.getService(reference); 
// 完成服务时应该通知注册中心。
bundleContext.ungetService(reference);  

服务监听

服务可以监听的事件包括:REGISTERED(注册)、MODIFIED(更改)、UNREGISTERING(注销)。

图片 11

服务监听事件

OSGi开发环境

略,点击可查看原文。