领先一步
VMware 提供培训和认证,助您快速提升。
了解更多我不时遇到的一个相当常见的问题是如何在 OSGi 环境中使用 JDK 特定的类。在某种程度上,这相当于无需将其打包即可从 OSGi 访问引导类路径。为了表达包依赖关系,捆绑包(bundle)在其 manifest 中使用 OSGi 指令 - 主要有Export-Package和Import-Package分别用于提供和要求类包依赖关系。定义捆绑包连线是创建模块化应用程序的关键一步;然而,在某些情况下,正如上述问题所示,所需的包无法从捆绑包中获取。
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 规范):"从给定捆绑包的类加载器可达的所有类",或者用通俗的话说,是捆绑包能看到的内容,即捆绑包的世界观。这些网络可以相交,因为同一个包可以被多个捆绑包加载;但是,每个空间必须一致,这是平台在每个捆绑包的解析阶段强制执行的要求。网络模型的一个副作用(或目标)是类型隔离或类版本控制:同一类的多个版本可以在同一个 VM 中良好地共存,因为每个版本都加载到自己的网络,自己的空间中。
然而,有些类需要以不同的方式加载,例如java.*包。这些类是 Java 运行时本身的组成部分,因此被隐式要求。例如,每个 Java 对象都是 java.lang.Object 的子类,这实际上意味着每个捆绑包都至少使用一个 Java 包 (java.lang)。虽然这种依赖关系可以通过捆绑包 manifest 中的指令来表达,但由于其强制性,这样做变得不受欢迎。这就是为什么 java.* 包被视为隐式导入,即使没有声明,每个捆绑包也可以加载它们。事实上,OSGi 规范禁止捆绑包指定对以下包的导入:java.*因为类连线总是意味着版本控制,这意味着在同一个 VM 中运行多个 Java 版本,这是不可能的(至少目前不可能)。
为了加载这些基本类型,OSGi 平台使用父级委托而不是网络模型;也就是说,它使用启动 OSGi 框架的类加载器来加载类,而不是使用 OSGi 类空间。虽然这看起来可能比实际复杂,但我已经使用 dot 语言创建了一个图表
如上所示,这种加载模型与传统的 Java 约定(依赖父级委托来解析所有包的类,而不仅仅是)有很大不同。java.*。捆绑包之间根据其连线进行通信,同时将特殊类型的加载委托给父类加载器(图中绿色箭头所示)。细心的读者可能已经注意到,只有java.*包被提及了 - JDK 中可用的其他公共包,例如javax.net或javax.xml是通过父级委托加载的,这意味着它们必须在类空间内解析。也就是说,由于它们不是隐式导入的,捆绑包需要导入这些包(这意味着需要有一个提供者)。OSGi 规范允许框架(通过其系统捆绑包)使用以下属性,将其父类加载器中的任何相关包作为系统包导出:org.osgi.framework.system.packages属性。由于将宿主 JDK 打包成一个捆绑包并不可行,可以使用此设置让系统捆绑包(或 ID 为 0 的捆绑包)自行导出这些包。大多数 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
使用此属性,可以添加额外的包,这些包将由框架加载和提供,并且可以与其他捆绑包连线。
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]>
另一个可能的选项是通过扩展捆绑包来增强系统捆绑包。这些扩展捆绑包充当片段;它们本身不是捆绑包,而是附加到宿主上。一旦附加,片段内容(包括任何允许的头信息)将被视为宿主的一部分。扩展捆绑包是一种特殊类型的片段,它们只附加到系统捆绑包上,以便提供框架的可选部分(例如 Start Level 服务)。可以使用这种机制创建一个空的扩展,只声明所需的包,将加载留给其宿主捆绑包(在本例中为框架)。
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头信息中的特殊宿主符号名称和额外属性。这告诉框架,该捆绑包不仅仅是一个普通片段,而是一个扩展捆绑包。一旦附加,相关的扩展 manifest 指令会与系统捆绑包(其宿主)的指令合并。
osgi> packages special.parent.package
special.parent.package; version="0.0.0"<org.eclipse.osgi_3.5.0.v20081201-1815 [0]>
解决方案 A' 基本上是解决方案 A 的变体(因此得名)——无需使用系统属性,可以使用片段捆绑包来扩展系统捆绑包,这在某些情况下可能更方便。值得指出的是,扩展捆绑包可能会使用 Java 引导类路径执行加载,这是规范定义的一种可选机制,并非兼容实现所必需。然而,目前我尝试过的 OSGi 框架中,没有一个实现了此功能。
这两种解决方案的主要优点是包在 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 的章节。
OSGi 支持的另一个选项是引导委托,您已经在java.*包中看到了。这允许用户创建“隐式”包,这些包将始终由框架的父类加载器加载,即使捆绑包未提供适当的导入。
此选项主要用于解决各种边缘情况,尤其是在 JDK 类中,这些类期望始终发生父类加载委托,或者假定系统上的每个类加载器都可以完全访问整个引导路径。sun.*包和 com.sun.* 是最常见的两个例子(如前所述),因此某些 OSGi 实现(例如 Equinox)默认启用它们:
org.osgi.framework.bootdelegation=sun.*,com.sun.*
顺带一提,Spring DM 在其集成测试框架中也默认使用相同的设置(AbstractConfigurableOsgiTests#getBootDelegationPackages())
上述每种解决方案都应适用于大多数情况;但是,我强烈推荐 A/A' 方法:它们清楚地表达了捆绑包的连线并允许扩展。连线易于控制、检测和诊断。解决方案 B 有点像黑魔法,因为捆绑包无法控制其加载并选择特定版本或提供者,因为没有类连线。此外,该设置会影响所有捆绑包,这可能并非总是您想要的。尽管如此,在某些情况下,引导委托非常方便;一个很好的例子是 instrumentation,例如性能分析或代码覆盖。大多数工具使用字节码织入来添加各种计数器或拦截执行流程。由于新添加的代码引用了捆绑包未知的类,因此“被 instrumentation”的捆绑包无法在 OSGi 内部加载,除非更新其 manifest。将自定义包添加到引导委托列表提供了一种非常快速的方法来 instrumentation OSGi 应用程序,而无需更改打包或部署过程。
在本文中,我按照 OSGi 规范的术语,将父类加载器称为加载和启动(或引导)OSGi 框架的实体。值得注意的是,某些 OSGi 实现(特别是 Equinox)允许将父类加载器自定义为不同的值(例如应用程序、引导或扩展类加载器)。
附注:本文没有代码列表,但代码爱好者可以此处获取图表定义。