模块化 Java:静态模块化

阅读数:9592 2009 年 12 月 15 日

模块化是大型 Java 系统的一个重要特征。在这些项目中构建脚本和项目通常被划分为多个模块,以便改进构建过程,但是在运行时却很少考虑划分模块的问题。

在“模块化 Java”系列文章的第二篇里,我们将讨论静态模块化(static modularity)。内容包括如何创建 bundle、将其安装到 OSG 引擎以及怎样建立 bundle 之间的版本依赖。在下一篇文章中,我们将讨论动态模块化(dynamic modularity)并展示 bundle 如何对其他 bundle 作出响应。

在上篇文章《模块化Java 简介》 中讲到,Java 在开发时把package 作为模块化单元,部署时把JAR 文件作为模块化单元。可是尽管像Maven 这样的构建工具能够在编译时保证 package 和JAR 的特定组合,但这些依赖在运行时classpath 下仍可能出现不一致的情况。为了解决这一问题,模块可以声明其依赖需求,这样, 在运行时就可以在执行之前进行依赖检查。

OSGi 是一个 Java 的运行时动态模块系统。 OSGi 规范描述了 OSGi 运行时的工作行为方式;当前版本是 OSGi R4.2 Infoq 曾经报导过)。

一个 OSGi 模块(也称为bundle)就是一个普通的 JAR 文件,但在其 MANIFEST.MF 中带有附加信息。一个 bundle 的 manifest 必须至少包含如下内容:

  • Bundle-ManifestVersion:对 OSGi R4 bundle 来说必须是 2(OSGi R3 bundle 则默认为 1)
  • Bundle-SymbolicName:bundle 的文本标识符,通常以反向域名的形式出现,如 com.infoq,并且往往对应了包含其中的 package
  • Bundle-Versionmajor.minor.micro.qualifier形式的版本号,前三个元素是数字(缺省是 0),qualifier则是文本(缺省是空字符串)

创建一个 bundle

最简单的 bundle 必须在 manifest 文件中包含如下内容:

Bundle-ManifestVersion: 2
Bundle-SymbolicName:  com.infoq.minimal 
Bundle-Version: 1.0.0

创建 bundle 并没有什么可稀奇的,那么让我们创建一个带activator的 bundle 吧。下面是 OSGi 特定的代码片段,在 bundle 启动时被调用,有点像是 bundle 的 main 方法。

package com.infoq;
import org.osgi.framework.*;
public class ExampleActivator implements BundleActivator {
  public void start(BundleContext context) {
    System.out.println("Started");
  }
  public void stop(BundleContext context) {
    System.out.println("Stopped");
  }
}

为了让 OSGi 知道哪个类是 activator,我们需要在 manifest 中加入额外的信息项:

Bundle-Activator: com.infoq.ExampleActivator
Import-Package: org.osgi.framework

Bundle-Activator 声明了在 bundle 启动时要实例化并调用其 start() 方法的类;类似的,在 bundle 停止时将调用该类的 stop() 方法。

那么 Import-Package 又是干什么的?每个 bundle 都需要在 manifest 中定义其依赖,以便在运行时判断所有必需代码是否可用。在本例 中,ExampleActivator 依赖于 org.osgi.framework 包中的 BundleContext;如果我们不在 manifext 中声 明该依赖,在运行时就会碰到 NoClassDefFoundError 错误。

下载 OSGi 引擎

要编译并测试我们的 bundle,需要一个 OSGi 引擎。对 OSGi R4.2,下面罗列了几个可用的开源引擎。你也可以下载 Reference API 来编译(这样可以确保没有用到任何平台特定特性);可是,要运行 bundle,还是需要一个 OSGi 引擎。以下引擎都可供选择:

另外还有更小的定位于嵌入设备的 OSGi R3 运行时可用(比如 Concierge ),但本系列文章只关注 OSGi R4。

编译并运行 bundle

获得framework.jar之后,把 OSGi 加入 classpath 并编译上面的例子,然后将其打包成 JAR:

javac -cp framework.jar com/infoq/*/*.java

jar cfm example.jar MANIFEST.MF com

每种引擎都有 shell,命令也相似(但不相同)。为了便于练习,让我们看看如何获得这些引擎并使之运行、安装和启 / 停 bundle。

一旦引擎启动并运行起来,你就可以安装 bundle(由 file:// URL 来定位)了,然后可以使用安装 bundle 所返回的 bundle id,启动或停止该 bundle。

  Equinox Felix Knopflerfish
启动应用 java -jar org.eclipse.osgi_*.jar -console java -jar bin/felix.jar java -jar framework.jar -xargs minimal.xargs
帮助 help
列表 ss ps bundles
安装 install file:///path/to/example.jar
启动 start id
更新 update id
停止 stop id
卸载 uninstall id
退出 exit shutdown

尽管所有的 shell 工作起来大都一样,但命令之间还是有容易混淆的细微差别。有两个统一 console 的项目( Pax Shell Posh )和运行器( Pax Runner )可以利用;OSGi RFC 132 则是一个正在进行中的提案,试图标准化 command shell。另外, Apache Karaf 可以运行在 Equinox 或 Felix 之上,提供统一的 shell 以及其他特性。尽管使用这些项目或工具进行实际部署是可取的,但本系列文章还是关注于普通的 OSGi 框架实现。

如果你启动了 OSGi 框架,你应该能够安装上面所讲的 com.infoq.minimal-1.0.0.jar (你还可以用链接地址及 install 命令直接从网站上进行安装)。每次安装 bundle,引擎都会打印出该 bundle 的数字 ID。

在安装好 bundle 之前不可能知道 bundle 的 ID 是多少,这取决于系统中其它 bundle 的 ID 分配情况;但是你可以使用适当的命令罗列出已安装的 bundle 将其找出来。

依赖

迄今为止,我们只有一个 bundle。模块化的一个好处是可以把系统分解为多个小模块,在此过程中,减小应用的复杂性。从某种程度上,Java 的 package 已经做到了这一点:例如,一个 common 包有独立的 client 和 server 包,他们都依赖于该 common 包。但是 Jetty 最近的例子(client 意外地依赖于 server)表明做到这一点并不总是很容易。实际上,有些由 OSGi 给项目带来的好处纯粹就是强制模块间的模块化约束。

模块化的另一个好处是把'public'包从非 public 包中分离出来。Java 的编译时系统允许隐藏非 public 类(在特定包中是可见的),但是不支持更大程度的灵活性。然而在 OSGi 模块中,你可以选择哪些包是exported(输出)的,这就意味着没有输出的包对其他模块是不可见的。

让我们继续开发一个功能,用来初始化 URI Templates (与 Restlet 中 使用的一样)。因为该功能可重用,我们想将其放在一个单独模块中,让使用它的客户端依赖于它。(通常,bundle 不适合这么细粒度的用法,但是其可用于 说明工作原理)。该功能将根据一个模板(比如 http://www.amazon.{tld}/dp/{isbn}/)和一个包含有 tld=com,isbn=1411609255 的 Map,产生出 URL http://www.amazon.com/dp/1411609255/ (这么做的一个原因是,如果 Amazon URL 模式发生了变化,我们能够改变该模板,尽管好的URI 是不会改变的)。

为了提供一个在不同实现之间切换的简单方法,我们将提供一个接口和一个工厂。这会让我们看到在提供功能的同时实现是怎样对 client 隐藏的。代码(对应几个源文件)如下:

package com.infoq.templater.api;
import java.util.*;
public interface ITemplater {
  public String template(String uri, Map data);
}
// ---
package com.infoq.templater.api;
import com.infoq.templater.internal.*;
public class TemplaterFactory {
  public static ITemplater getTemplater() {
    return new Templater();
  }
}
// ---
package com.infoq.templater.internal;
import com.infoq.templater.api.*;
import java.util.*;
public class Templater implements ITemplater {
  public String template(String uri, Map data) {
    String[] elements = uri.split("\\{|\\}");
    StringBuffer buf = new StringBuffer();
    for(int i=0;i

该实现隐藏在 com.infoq.templater.internal 包中,而 public API 则位于 com.infoq.templater.api 包中。这就给了我们巨大的灵活性,如果需要,以后可以修改实现以提供更加有效的手 段。(internal 包名是约定俗成,你可以起其他名字)。

为了让其他 bundle 能够访问该 public API,我们需要将其export(输出)。我们的 manifest 如下:

Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.infoq.templater
Bundle-Version: 1.0.0
Export-Package: com.infoq.templater.api

创建一个 client bundle

我们现在可以创建一个使用 templater 的 client。利用上面的例子创建一个 activator,其 start() 方法如下:

package com.infoq.amazon;
import org.osgi.framework.*;
import com.infoq.templater.api.*;
import java.util.*;
public class Client implements BundleActivator {
  public void start(BundleContext context) {
    Map data = new HashMap();
    data.put("tld", "co.uk"); // or "com" or "de" or ...
    data.put("isbn", "1411609255"); // or "1586033115" or ...
    System.out.println( "Starting\n" + 
        TemplaterFactory.getTemplater().template(
        "http://www.amazon.{tld}/dp/{isbn}/", data));
  }
  public void stop(BundleContext context) {
  }
}

我们需要在 manifest 中显式输入 templater API,否则我们的 bundle 无法编译。用 Import-Package 或 Require-Bundle 都可指定依赖。前者可以让我们单独输入包;后者 则将隐式输入该 bundle 中所有输出包。(多个包及 bundles 可以用逗号分开)。

Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.infoq.amazon
Bundle-Version: 1.0.0
Bundle-Activator: com.infoq.amazon.Client
Import-Package: org.osgi.framework
Require-Bundle: com.infoq.templater

注意在前面的例子中,我们已经使用了 Import-Package 来输入 org.osgi.framework。在这个例子中,我们将演示 Require-Bundle 的用法,其使用了 Bundle-SymbolicName。当然,用 Import-Package: org.osgi.framework, com.infoq.templater.api 可以达到相同的效果。

不管如何声明依赖于 templater 的 bundle,我们都只能访问单一的输出包 com.infoq.templater。尽管 client 可以通过 TemplaterFactory.getTemplater() 来访问 templater,但是我们不能直接从 internal 包中访问该类。这样我们 在未来就可以在不影响 client 的情况下改变 templater 类的实现。

测试该系统

任何 OSGi 应用实际上都是一组 bundle。在本例中,我们需要编译并把 bundle 打包为 JAR(如前面所述),启动 OSGi 引擎,安装两个 bundle。下面是在 Equinox 中进行操作的例子:

java -jar org.eclipse.osgi_* -console
osgi> install  file:///tmp/com.infoq.templater-1.0.0.jar 
Bundle id is 1
osgi> install  file:///tmp/com.infoq.amazon-1.0.0.jar 
Bundle id is 2
osgi> start 2
Starting
http://www.amazon.co.uk/dp/1411609255

Amazon client bundle 现在已经启动了;当其启动时,它先用(硬编码的)给定值为我们初始化 URI template。然后在该 bundle 启动过程中打印出来以确定其已正常工作。当然,一个真正的系统不会这么死板;但是 Templater 服务可以用于 任何其他应用(例如,产生一个基于 Web 的应用中的链接)。将来,我们将能够在 OSGi 环境中查看 Web 应用。

带版本的依赖

本文最后要指出的是目前我们所谈的依赖都没有版本;更确切的说,我们可以使用任意版本。两个 bundle 整体及各个包都可以有版本,增大 minor 号通常 意味着增加了新特性(但保持向后兼容)。以 org.osgi.framework 包为例,OSGi R4.1 中是 1.4.0,OSGi R4.2 中是 1.5.0。(顺便提一句,这是 bundle 版本和销售版本保持分离的很好理由, Scala 语言已经这么做了)。

要声明依赖处于一个特定版本,我们必须在 Import-Package 或 Require-Bundle 中来表达。比如,我们可以指定 Require- Bundle: com.infoq.templater;bundle-version="1.0.0"以表示工作所需的最低版本为 1.0.0。类似的,我们可以用 Import-Package: com.infoq.templater.api;version="1.0.0"做相同的事情 —— 但是要记住packagebundle版本是完全分开的。如果你不指定版本,缺省为 0.0.0,因此,除非指定了相应的 Export-Package: com.infoq.templater.api;version="1.0.0"否则该输入不会被解析。

还可以指定一个版本范围。例如,按惯例 OSGi 版本的 major 号增大表示向后兼容发生改变,因此我们可能想将其限制在 1.x 的范围内。我们可以通过 (bundle-)version="[1.0,2.0)"的方式来表达这一依赖约束。该例中,[表示‘包含’,而) 表示‘不包含’。换句话说,‘从 1.0 到 2.0 但不包括 2.0’。实际上,将一个依赖约束表达为‘1.0’与“[1.0,∞)”意思是一样的——换句话说,比 1.0 版高都可以。

尽管这些内容超出了本文的范围,但是在一个 OSGi 系统中,一个 bundle 同时有两个版本也是可能的。如果你有一个老 client 依赖于 1.0 版本 API,同时又一个新 client 依赖于 2.0 版本 API,这就非常有用了。只要每个 bundle 的依赖是一致的(换句话说,一个 bundle 不能直接或 间接同时输入 1.0 和 2.0)那么应用程序将工作良好。作为读者练习,你可以创建一个使用泛型的 2.0 版的 Templater API,然后让一个 client 依赖于 1.x,而另一个依赖于 2.x。

总结

在本文中,我们探讨了开源 OSGi 引擎 Equinox、Felix 和 Knopflerfish,并且创建了两个有依赖关系的 bundle。我们还谈到了带版本的依赖。截止目前,模块化还是静态的;我们还没有涉及到 OSGi 的动态本质。在下一篇文章中我们将涉及这一内容。

本文所讲例子的可安装 bundle 罗列如下(包含源代码):

查看英文原文 Modular Java: Static Modularity


感谢崔康对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论