技术开发 频道

OSGi Java模块化框架的另类进化

  版本控制

  我们的模块系统现在看起来非常不错,但随着时间的推移,模块不可避免地会发生方便,对称我们还不能处理。所以,我们还需要支持“版本控制”。

  如何进行版本控制?手洗,导出方可可进行声明,为其导出的包提供一些有用的信息:“这个是 API 版本 1.0.0”。导入方现在能够只导入与其预期匹配并且经过编译/测试的版本,并且解决接受某个版本,比如版本 3.0.0。但是如果导入方想要版本 1.0.0 而只有版本 1.0.1 可用时,应该如何处理呢?一个稍高一点的版本看起来不会保护巨大的更改,所以导入方应该可以接受版本 1.0.1。事实上,导入方应为其可接受版本指定一个范围,比如类似这样的一个范围:“版本 1.0.0 到 2.0.0 但不含 2.0.0”。对包进行连线的流程可以支持这种范围,如果导出的导出版本位于导入指定的范围内,就将导入与该导出进行连线。为了让这个机制能够正常使用,版本编号应该是有顺序的并且能够进行比较。

  我们如何确定版本 1.0.1 相对于 1.0.0 没有包含巨大的更改呢?很遗憾,我们无法确认这种事情。对于版本编号,OSGi 强烈建议而不是强制使用以下语法规则:

  1. 对于非向后兼容的更改,对主要(第一)部分进行递增。

  2. 对于向后兼容的功能改善,对次要(中间)部分进行递增。

  3. 对于未造成可见的功能更改的故障修复,对最后部分进行递增。

  如果所有人都遵守这些语法规则,那么指定导入范围将是一件轻松简单的事情。但现实世界并不是这么简单,因此在试用如何外部库时,我们必须小心地处理兼容问题。

  对模块和元数据进行打包

  我们这个模块系统需要一种方法来对模块的内容以及描述导入和导出的元数据进行打包,将其包括到一个可部署的单元中。

  Java 已经有了标准的部署单元:JAR 文件。JAR 文件可能并不算一种非常成熟的模块,但对于移动大块的编译代码还是不错的,所以我们并不需要创建新的东西。那么现在的唯一问题是,将元数据(即导入和导出列表、版本等等)放在哪里?

  看起来配置格式强烈地受到一时潮流的影响;如果我们是在 2000 年到 2006 年期间设计这个模块系统,我们很可能会选择将元数据放到 JAR 文件下的某个 XML 文件中这种方式能够工作,但会遇到许多问题:对于流程,XML 文件并不是特别有效率,尤其是我们必须在 JAR 文件的某个地方才能找到它,而且在进行语法分析之前还要对其进行解压。JAR 文件是一个 ZIP 压缩包,所以要找到某个特定文件,意味着必须读取末端,找到用于跟踪记录的中央目录,然后再跳转到该目录指定的分支上。换句话说,通常不得不读取整个 JAR 文件,对于需扫描大型目录的工具,如果这个目录下有很多模块,这个过程将变得非常痛苦。比如,搜索某个可用的模块,以满足某个依赖关系。

  另外 XML 几乎不能人工编辑。为了正确的编辑这种文件,我们需要使用特定的编辑根据。

  另一方面,如果是在 2006年之后设计这个模块系统,我们的第一个想法会是使用 Java 注释(annotation)。如果使用适当,我非常喜欢注释,将类似 @Export(version="1.0.0") 的东西放到 Java 源文件中的包声明上,很明显比在单独文件中对其进行维护要更有吸引力。不过,等一下……在包的每个源文件中,包声明都会重复一次;难道我们也必须在所有源文件中加入注释?

  为了解决这个问题,Java 语言规范(JLS)建议使用一个名为“package-info.java” 特定源文件。但对于不属于任何特定包的元数据怎么处理呢?比如导入包的列表或模块本身的名称和版本。Java 语言规范建议我们需要使用另一个特定源文件,使用类似“module-info.java”名称。

  到目前一切顺利,现在让我们看看如何对模块进行处理。

  这些特定的源文件将在 package-info.class 和 module-info.class 中被编译为字节码,这样就不需要打开 ZIP 压缩的 JAR 文件来查看元数据了。所有模块扫描工具都必须对整个模块系统进行读取,而且也必须能够处理字节码。运行时模块系统自身也必须立即为模块常见一个类加载器,用于读取它的元数据;结果是,如果我们能够将类加载器的创建推迟到真正从模块中加载某个类那个时刻,就可以消除大量的优化工作。

  已经发生的事实是,OSGi 的设计的确是在 2000 年之前,所以它的确选择了这些方案中的其中之一。回头看看 JAR 文件规范,答案自动浮现:META-INF/MANIFEST.MF 是应用程序专用元数据的标准位置。在规范中这样写道:“忽略不可理解的属性。这类属性可能包含应用程序所用的特定部署新型。”

  MANIFEST.MF 专为提高流程的效率而设计,而且它至少比 XML 更快。某种长度上,它是可读的;至少与 XML 一样可读,很明显比编译的 Java 字节码更具有可读性。此外,标准的 jar 命令行工具通常将 MANIFEST.MF 放到 JAR 文件的第一项中,所以为了获取元数据,工具只需扫描文件中的前几百个字节。

  令人遗憾的是 MANIFEST.MF 并不完美。其一,由于规则要求每行不超过 72 个字节,手工编写相对困难,考虑到单个 UTF-8 字符为 1-6 个字节,这种规则会导致一些问题。一个更好的方式是利用另一格式的模板来生成 MANIFEST.MF。Bnd 工具是这样的,Maven 的 Bundle Pulin 和 SpringSource 的 Bundlor 也是如此。

  事实上,Bnd 甚至包括对于处理注释的实验式的支持,比如 @Exporton 源代码注释。这样我们将能够获得来自2个方面的好处:注释的便利性,以及 MANIFEST.MF 的效率和运行时可读性/工具性。

 

0
相关文章