OSGi,Java模块化框架的另类进化(2)

OSGi,Java模块化框架的另类进化(2)

在我们这个模块系统中,我们选择的解决方式是允许模块仅“导出”其内容的一部分。如果模块中某些部分是非导出的,那么对于其他模块就是不可见的。但默认导出哪些内容?除了某些明显需要隐藏的部分,我们应该导出所有内容吗?或者除了那些明显需导出的部分,我们应该隐藏所有其他内容?选择后者看起来能够到来更好的透明度:我们可以很方便查看导出列表,确定那些可见的部分,即模块的“表面部分”。

请注意,我目前还没指定具体导出什么内容,这是一个需要仔细考虑的问题。

导出的反面是什么?当然是导入。一个模块想要使用其他模块中代码可以从后者进行导入。现在我们有了另一个选择……我们应该导入另一个模块导出的所有内容吗?或者只导入我们所需的那部分?同样我们还是选择后者,因为它会带来更好的透明度:重要的是我们导入了什么而不是从哪里导入。

与购物行为进行类比

关于导入的话题非常重要,所以这里次岔开一下话题,让我们看一个有点搞笑又有点夸张的购物行为。

我妻子和我的购物方式是不同的。我认为购物是一件麻烦的琐事。每当不得不去买东西时,我就找到一家商店(或者一组商店),那里有我需要的东西,我只买我需要的商品,买到之后回家。只要能买到我需要的东西,我不关心是从哪家商店买到的。

而我妻子去了一家商店,那家商店买什么她就买什么。

很明显我觉得我的购物方式更好,因为我妻子无法控制她能买到什么东西。如果她常去的一家商店更换了货柜上的商品,那她买回来的将是另外一些东西。当然很多东西并不是她需要的,而且她真正需要的又没有买到。

更糟糕的是,有时她买回来的东西并不能独自使用,因为还需要其他东西,比如电池。所以她不得不再次去商店里买电池,同样这次她会买下电池商店里出售的各种电池。再进一步假设,从电池商店里买到的某样东西还依靠其他东西才能使用,所以她又跑去另一家商店,仅仅是为了让某些商品能够正常工作,而这些商品从最初就不是我们所需要的。这个问题被称为“扇出”(fan-out)。

通过这个购物类比,相信你对模块系统将有一个更清晰的概念。这种非理智的购物行为等同于这样一个系统:我们申明了对某个模块的依靠性,而这个系统强制我们从该模块导入所有内容。当进行导入时,应导入所有我们实际需要的内容,而不管它来自哪里,同时忽略其他所有内容,可能内容只是碰巧位于它的包内。使用 Maven 构建工具时我们遇到这个尖锐的“扇出”问题,这个工具仅提供整体模块的依赖性(即“买下整个商店”方式)。其结果是,在编译 200 个字节的源文件之前,必须下载整个互联网的内容。

导入和导出的粒度(granularity)

从模块导入和导出内容的粒度应该是怎样的?由于存在各种嵌入等级,Java 中有多种等级的粒度。方法和域嵌入到类中,类又嵌入到包中,包嵌入到模块或 JAR 文件中。

不难看出共享等级不应是方法和域。导入一个类的某些方法而排除例外一些,这种方式很明显是荒唐的。不仅仅这种方式是如此。我们可以为某个模块中类写一些方法/域,在另一个模块中再写一些方法/域,这种方式也同样是不可行的。想象一下,为在模块中的每一个共享方法写一些导入和导出列表,运行时对这些列表进行检查以及诊断为题的复杂度将是非常恐怖的,会出现许多错误,因为类并不是设计用来在运行时进行分割的。

现在看看另一个极端,共享等级也不应是整个模块,因为这样模块就不能隐藏实施细节的部分,导入方将经常性地遇到“买下整个商店”的问题。

所以唯一合理的选择是类和包。老实说,选择类也不是那么合理。虽然没有方法/域那么糟糕,但类的数量非常多,由于它太过于依赖同一个包中的其他类,无论是将类列出作为我们的导入和导出,还是将包中的一些类划分到某个模块同时将同一个包中另一些类划分到了另一个模块中,都是不合理的。

最终的结果,OSGi 选择了包。Java 包的内容通常具有某种程度的一致性,但列出导入和导出的包并不是那么麻烦,而且在某个模块加入一些包而在另一个模块在加入另一些包,并不会对如何东西造成损坏。应该属于模块内部的代码可以放到一个或多个非导出的包中。

我们的损失的无法干净地处理那些所谓的“分裂包”(split-package)。在 OSGi 中,包是进行共享的最基本单元:当导入个包时,你获得一个模块导出包的所有内容而不包括其他内容。一些传统的包,一直坚持在许多模块中共享包内容,对于这些包也存在一些方法进行处理,但这好过对每个包进行调整以便让它作为整体只能由某个模块导出。

包连线(wiring)

既然对于模块如何自我分离然后再连接有了一个模型,我们现在可以想象创建一个框架,这个框架将为这些模块构造实际的运行时实例。它将负责安装模块以及构造类加载器(这些类加载器知道相应模块的内容)。

然后它将查看新安装的模块的导入,并试图找到匹配的导出。假设模块 A 导出包 com.foo,模块 B 要导入这个包。该框架将通知 B,它可以从模块 A 获得 com.foo 的类,这个称为连线(wiring)。如果 B 的类加载器要加载类 com.foo.Bar,它将委派 A 的类加载器来做。对整个模块的导入进行连线的过程成为解析(resolution),当所有导入都成功进行连线后,那么这个组件(bundle)就被解析(resolved)了,这将令它完全可用。

一个预料之外的好处是我们可以动态地安装、更新和卸载模块。对于已经解析的模块,安装新模块对它们没有影响,虽然这可能导致某些之前不可解析的模块变得可解析。当进行卸载或更新时,该框架非常清楚那些模块受到影响,并且如果需要它将更改它们的状态。为了能够顺利地进行,还有一些额外的细节需要处理,比如,一个模块在卸载或者取消解析之前正在做非常的事情,那么需要向它发送通知,以便让它干净利落地关闭。所以,OSGi 中的动态模块并不是凭空出现的,这里并没有什么神奇的功能,但 OSGi 至少让它成为可能。

某些 OSGi 用户更喜欢避免动态加载,这样做没有问题。这不是 OSGi 最重要的功能,但由于对于 OSGi 它是独一无二的,英尺获得了过多的关注。无论如何,没有人强迫你使用它,即使从来不去利用动态性的优势,你仍然能够从 OSGi 获得许多好处。

版本控制

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

如何进行版本控制?手洗,导出方可可进行声明,为其导出的包提供一些有用的信息:“这个是 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. 对于未造成可见的功能更改的故障修复,对最后部分进行递增。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值