模块化 Java:静态模块化

阅读数:9416 2009 年 12 月 15 日

话题:Java语言 & 开发架构

模块化是大型 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.2Infoq 曾经报导过)。

一个 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 ShellPosh)和运行器(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 中文站用户讨论组中与我们的编辑和其他读者朋友交流。