在 OSGi 中暴露引导类路径

工程 | Costin Leau | 2009年1月19日 | ...

我时不时会遇到一个相当常见的问题,即如何在 OSGi 环境中使用特定于 JDK 的类。在某种程度上,这等同于从 OSGi 获取对引导类路径的访问权限,而无需将其打包。为了表达包依赖关系,bundle 在其清单中使用 OSGi 指令 - 主要Export-PackageImport-Package分别用于提供和请求类包依赖关系。定义 bundle 连接是创建模块化应用程序的关键步骤;但是,在某些情况下,如上述问题,所需的包无法从 bundle 中获得。

NoClassDefFoundError: com.sun...

Notable examples of such packages would be the <tt>sun.*</tt> and <tt>com.sun.*</tt>, present in the JDK jars. Even though these are <a href="http://java.sun.com/products/jdk/faq/faq-sun-packages.html">internal</a> packages and are not guaranteed to be portable, some of them can be found even in non-Sun JDKs, due to their usage. Your application might not use them, but there are various libraries that do (in some cases due to performance, in others because it's the only way to achieve a certain functionality). If the using bundle declares an import on the <tt>com.sun</tt> package, it will fail to resolve since there are no providers for it. If the import is not declared, since the bundle doesn't contain the class definition, the loading process will usually fail. Clearly the packages above are not a corner case; generalizing the example, the packages available in the OSGi framework boot classpath are not visible to the OSGi environment. There are several solutions to this problem but first, let's take a closer look to see why it occurs.

类空间

在 OSGi 中,每个模块都有自己的类加载器用于加载资源和类。根据连接指令,平台在各个模块之间创建了一个委托网络。该网络形成一个类空间,它表示(引用 OSGi 规范):“从给定 bundle 的类加载器可以访问的所有类”,或者用通俗的话来说,bundle 可以看到什么,bundle 的世界观。由于同一个包可以由多个 bundle 加载,因此网络可以交叉;但是每个空间必须保持一致,这是平台在每个 bundle 的解析阶段强制执行的要求。网络模型的副作用(或目标)之一是类型隔离或类版本控制:同一个类的多个版本可以很好地共存在同一个虚拟机中,因为每个版本都加载到自己的网络、自己的空间中。

但是,有些类需要以不同的方式加载,例如java.包。这些类是 Java 运行时本身的一部分,因此隐式地由其所需。例如,每个 Java 对象都是java.lang.Object的子类,这实际上意味着每个bundle 至少使用一个 Java 包(java.lang)。虽然可以通过 bundle 清单中的指令表达这种依赖关系,但由于其强制性,它变得不可取。这就是为什么java.包被视为隐式导入,即使它们未声明,也可以由每个 bundle 加载。事实上,OSGi 规范禁止 bundle 对java.*指定导入,因为类连接始终意味着版本控制,这意味着在同一个虚拟机中运行多个 Java 版本,这是不可能的(至少现在是不可能的)。

为了加载这些基本类型,OSGi 平台使用父委托而不是网络模型;也就是说,它使用启动 OSGi 框架的类加载器来加载类,而不是 OSGi 类空间。由于这可能看起来比实际情况更复杂,因此我使用dot语言创建了一个图表

网络模型 网络模型

如上所示,此加载模型与传统的 Java 约定完全不同,后者依赖于父委托来解析所有包的类,而不仅仅是java.*。bundle 之间根据其连接进行通信,同时将特殊类型的加载委托给父类加载器(图像中的绿色箭头)。

方案 A:系统包

细心的读者可能已经注意到,只有java.*包已被提及 - JDK 中可用的其他公共包(例如javax.netjavax.xml)没有父委托,这意味着它们必须在类空间内解析。也就是说,bundle 需要导入包(这意味着需要有一个提供者),因为它们不是隐式的。OSGi 规范允许框架(通过其系统 bundle)使用org.osgi.framework.system.packages属性从其父类加载器导出任何相关包作为系统包。由于将宿主 JDK 重新打包为 bundle 不是一个可行的选择,因此可以使用此设置让系统 bundle(或 id 为 0 的 bundle)自己导出这些包。大多数 OSGi 实现已经使用此属性导出所有公共 JDK 包(基于检测到的 JDK 版本)。下面是 Java 1.6 的 Equinox 配置文件中的一个片段

org.osgi.framework.system.packages = \   javax.accessibility,\   javax.activity,\   javax.crypto,\   javax.crypto.interfaces,\   ...   org.xml.sax.helpers

使用此属性,可以添加将由框架加载和提供的额外包,这些包可以连接到其他 bundle。

org.osgi.framework.system.packages = \   javax.accessibility,\   javax.activity,\   ...   org.xml.sax.helpers, \   special.parent.package

如通过询问系统 bundle(下面是 Equinox 中 OSGi 控制台中的一个片段)所示

osgi> bundle 0   System Bundle [0]    Id=0, Status=ACTIVE     Registered Services    ...    Exported packages     ...     org.xml.sax.helpers; version="0.0.0"[exported]     special.parent.package; version="0.0.0"[exported]     ...

此设置需要在 OSGi 框架启动之前初始化,因此一个常见模式是将其设置为系统属性。这种方法将覆盖默认配置,因此即将推出的 OSGi 4.2 定义了另一个名为org.osgi.framework.system.packages.extra的属性,它将定义的系统包附加到org.osgi.framework.system.packages配置中,从而更容易扩展 OSGi 实现已定义的配置。添加新包就像向启动平台的虚拟机传递参数一样简单

java -Dorg.osgi.framework.system.packages.extra=special.parent.package;version=1.0 ...

让我们再次从 OSGi 控制台中检查包

osgi> packages special.parent.package   special.parent.package; version="1.0.0" <org.eclipse.osgi_3.5.0.v20081201-1815 [0]>

方案 A':扩展 bundle

另一种可能的选择是通过扩展 bundle来增强系统 bundle。它们充当片段;它们本身不是 bundle,而是附加到主机。一旦附加,片段内容(包括任何允许的标头)将被视为主机的一部分。扩展 bundle 是一种特殊的片段,仅附加到系统 bundle 以提供框架的可选部分(例如启动级别服务)。可以使用此机制创建一个空扩展,该扩展仅声明所需的包,并将加载留给其宿主 bundle(在本例中为框架)

osgi> ss
 
Framework is launched.
 
id     State      Bundle
0    ACTIVE    org.eclipse.osgi_3.5.0.v20081201-1815
        Fragments=1
1    RESOLVED    a.framework.extension_0.0.0
        Master=0
 
osgi> bundle 1
 
a.framework.extension_0.0.0 [1]
    Id=1, Status=RESOLVED    Data Root=...
    No registered services.
    No services in use.
    Exported packages
       <b>special.parent.package; version="0.0.0"[exported]</b>
    No imported packages
    Host bundles
       <b>org.eclipse.osgi_3.5.0.v20081201-1815 [0]</b>
    No named class spaces
    No required bundles
   
osgi> headers 1
 
Bundle headers:
   Bundle-ManifestVersion = 2
   Bundle-SymbolicName = a.framework.extension
   <b>Export-Package = special.parent.package</b>
   <b>Fragment-Host = system.bundle; extension:=framework</b>
   Manifest-Version = 1.0

请注意上面的Fragment-Host标头中的特殊主机符号名称和额外属性。这告诉框架该 bundle 不仅仅是一个普通片段,而是一个扩展 bundle。一旦附加,相关的扩展清单指令将与系统 bundle(其主机)的指令合并。

 
osgi> packages special.parent.package
 
special.parent.package; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> 

方案 A' 基本上是 A 的一个变体(因此得名) - 与使用系统属性相比,可以使用片段 bundle 来扩展系统 bundle,这在某些情况下可能更方便。值得指出的是,扩展 bundle 可能会使用 Java 引导类路径执行加载,这是规范定义的一种可选机制,对于符合规范的实现不是必需的。但是,目前,我尝试过的 OSGi 框架中,没有一个实现了此功能。

这两种方案的主要优点是包在 OSGi 中提供(并因此版本化)。约定是为系统包使用默认版本 (0.0.0),但这并非强制性(如上所示)。一个强大的副作用是能够通过不同的 bundle 提供框架声明的包的不同、更新的版本。我们使用它来解决由 JDK 带有一个不完整的javax.transaction包版本导致的事务数据访问问题,该包由框架在 OSGi 环境中自动导出。

osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]>

解决方案是安装一个bundle,其中包含完整的javax.transactionAPI 并具有更高的版本: osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> javax.transaction; version="1.1.0"<com.springsource.javax.transaction_1.1.0 [1]>

以便使用 bundle 可以使用它而不是 JDK 中打包的版本。

osgi> ss   Framework is launched.   id     State      Bundle 0    ACTIVE    org.eclipse.osgi_3.5.0.v20081201-1815 1    ACTIVE    com.springsource.javax.transaction_1.1.0 2    ACTIVE    user.bundle_0.0.0   osgi> headers 2   Bundle headers:    Bundle-ManifestVersion = 2    Bundle-SymbolicName = user.bundle    Import-Package = javax.transaction;version=1.0    Manifest-Version = 1.0   osgi> packages javax.transaction   javax.transaction; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]> javax.transaction; version="1.1.0"<com.springsource.javax.transaction_1.1.0 [1]>    user.bundle_0.0.0 [2] imports

有关更多信息,请参阅 Spring DM 常见问题解答部分

方案 B:引导委托

OSGi 支持的另一个选项是引导委托,您已经看到过java.*包的示例。这允许用户创建“隐式”包,这些包将始终由框架父类加载器加载,即使 bundle 未提供正确的导入。

类加载委托 类加载委托

此选项主要是为了适应各种特殊情况,尤其是在 JDK 类中,这些类期望始终发生父类加载委托,或者假设系统上的每个类加载器都可以完全访问整个引导路径。包sun.com.sun.是最常见的两个示例(正如我之前提到的),因此某些 OSGi 实现(即 Equinox)默认启用它们

org.osgi.framework.bootdelegation=sun.*,com.sun.*

顺便说一句,Spring DM 在其集成测试框架中默认也使用相同的设置(AbstractConfigurableOsgiTests#getBootDelegationPackages())

哪个解决方案更好?

以上每个解决方案在大多数情况下都应该有效;但是,我强烈推荐 A/A' 方法:它们清晰地表达了 bundle 的连接方式并允许扩展。连接方式易于控制、检测和诊断。解决方案 B 就像一点魔法,因为 bundle 无法控制其加载并选择特定版本或提供程序,因为没有类连接机制。此外,该设置会影响所有 bundle,这可能并不总是你想要的。尽管如此,在某些情况下,启动委托非常方便;一个很好的例子是检测,例如性能分析或代码覆盖率。大多数工具使用字节码编织来添加各种计数器或拦截执行流程。由于新添加的代码引用了 bundle 未知的类,因此“已检测”的 bundle 无法在 OSGi 中加载,除非更新其清单。将自定义包添加到启动委托列表提供了一种非常快速的方法来检测 OSGi 应用程序,而无需更改打包或部署流程。

关于父类加载器

在本条目中,我将父类加载器称为加载并启动(或引导)OSGi 框架的实体,遵循 OSGi 规范的术语。值得注意的是,一些 OSGi 实现(特别是 Equinox)允许将父类加载器自定义为不同的值(例如应用程序、启动或扩展类加载器)。

链接

有关 OSGi 类加载的更多信息,请参阅以下链接
  • OSGi 核心规范,第 3.8、3.14 和 3.15 节
  • ClassLoader API
  • Eclipse 运行时选项(特别是osgi.parentClassLoader)

附注:此条目没有代码清单,但代码爱好者可以此处获取图形定义。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部