在 OSGi 中暴露引导类路径

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

我时常会收到一个相当普遍的问题,那就是如何在 OSGi 环境中使用 JDK 特定的类。在某种程度上,这相当于在不捆绑引导类路径的情况下,从 OSGi 中访问它。为了表达包依赖关系,bundle 在其 manifest 中使用 OSGi 指令 - 主要Export-Package还是Import-Package为了提供和要求,分别是一个类包依赖。定义一个捆绑包连接是创建模块化应用程序的关键步骤;然而,在某些情况下,如上述问题,所需的包在捆绑包中不可用。

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规范):“一个给定捆绑包的类加载器可达到的所有类”,或者通俗地说,捆绑包可以看到的东西,捆绑包的世界视图。网络可以交叉,因为同一个包可以被多个捆绑包加载;然而,每个空间必须是一致的,这是平台在每个捆绑包的解析阶段强制执行的要求。网络模型的一个副作用(或目标)是类型隔离或类版本控制:同一类的多个版本可以在同一个虚拟机中很好地共存,因为每个版本都加载到其自己的网络、其自己的空间中。

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

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

网络模型 网络模型

如上所示,这种加载模型与传统的Java约定有很大的不同,传统的Java约定依赖于父委托来解析所有包(而不仅仅是java.*)。捆绑包根据它们的连接进行通信,同时将特殊类型的加载委托给父类加载器(图中绿色的箭头)。

解决方案 A:系统包

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

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

使用此属性,可以添加额外的包,这些包将被框架加载和提供,并可以与其他捆绑包连接。

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

如通过查询系统捆绑包(下面是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实现已经定义的配置。添加新包可以像传递一个参数给启动平台的VM一样简单。

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':扩展捆绑包

另一个可能的选项是通过扩展捆绑包增强系统捆绑包。它们不是独立的捆绑包,而是作为片段附加到宿主捆绑包。一旦附加,片段的内容(包括任何允许的头)就被视为宿主的一部分。扩展捆绑包是一种特殊的片段,它附加到系统捆绑包,以提供框架的可选部分(例如启动级别服务)。可以使用此机制创建一个空的扩展,仅声明所需的包,将加载委托给宿主捆绑包(在本例中是框架)。

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头中的特殊宿主符号名称和额外属性。这告诉框架该捆绑包不仅仅是一个普通的片段,而是一个扩展捆绑包。一旦附加,相关的扩展清单指令就会与系统捆绑包(其宿主)的指令合并。

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

解决方案 A' basically is a variant of A (hence the name) - instead of using system properties, one can use fragment bundles to extend the system bundle which can be more convenient in some cases. It's worth pointing out that the extension bundles might perform loading using the Java boot class path, an optional mechanism defined by the specification which is not required for compliant implementations. However, at the moment, none of the OSGi frameworks that I have tried, implement this feature.

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

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

解决方案是安装一个包含完整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]>

这样,消费捆绑包就可以使用它,而不是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 FAQ 部分

解决方案 B:启动委托

OSGi支持的另一种选择是启动委托,您已经为java.*包看到了这一点。这允许用户创建“隐含”包,这些包将始终由框架父类加载器加载,即使捆绑包没有提供正确的导入。

类加载委托 类加载委托

此选项主要用于适应各种角落情况,尤其是在JDK类中,这些类期望父类加载委托始终发生,或者假设系统中的每个类加载器都拥有对整个启动路径的完全访问权限。包sun.*com.sun.是两个最常见的例子(正如我已经提到的),因此一些OSGi实现(例如Equinox)默认启用它们。

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

顺便说一句,Spring DM在其集成测试框架(AbstractConfigurableOsgiTests#getBootDelegationPackages())

哪个解决方案更好?

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

关于父类加载器的说明

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

链接

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

P.S. 本条目没有代码列表,但代码爱好者可以从此处获取图定义。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

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

查看所有