OSGi Bundle Convert 插件原理

阅读数:2811 2014 年 4 月 2 日

话题:语言 & 开发架构

1.引言

普通的 web 应用要转换为 OSGi 应用,经常会遇到应用中依赖的 Jar 是非标准的情况,这些 Jar 可能只遵守了部分 OSGi 规范,甚至 Manifest 信息是空的。这种情况在 OSGi 应用中根本无法使用这个非标准的 Jar 做为 Bundle,故必须要将这个非标准的 Jar 转换成遵守 OSGi 规范的 Bundle。另外用 Maven 管理的仓库,由于不同开发者对规范的理解不同,在仓库中也存在了各种规范或者不规范的 Jar,如果我们能很好的将 Maven 仓库中的 Jar 转换成标准的 Bundle,Maven 仓库也就转换成对应的 OSGi Bundle 仓库,对于非 OSGi 的应用而言也就可以很方便的利用 Maven 仓库,普通 web 应用也可以平滑的切换到 OSGi 环境。

2.转换 Bundle 的方式

要将非标准的 Jar 转换成 OSGi Bundle,最核心的也就是如何将 Bundle 中的 Import-Package 和 Export-Package 内容重写,对此转换 Bunlde 的方式可划分为 3 种方式:1. 固定版本方式转换;2. 非固定版本方式转换;3. 固定版本和非固定版本混合使用方式转换;

2.1固定版本方式

主要依据 Jar 对应 pom 依赖树 (mvn dependency:tree) 进行转换,而对于 pom 中无法确定的版本,则需要依靠 Bundle 模块 pom 的依赖仲裁来强制选择一个版本,这种情况最终转换好的 Bundle Import-Package 和 Export-Package 是固定版本的;固定版本方式转换有点类似于 maven 中的 Jar 版本强依赖,这个 Bundle 必须 import 指定的固定版本 Bundle,这样做的目的也在于使得转换后的每个 Bundle 依赖环境是一个独立的集合,而且集合之间没有任何冲突可言。

2.2非固定版本方式

依据 Jar 文件中 *.class 的 package 和 import package 信息转换,最终转换后的 Bundle 不指定 Import-Package 版本和 Export-Package 版本;这种情况也就是目前经常用的不指定版本范围或者无版本 package,特别是无版本 package,在 OSGi 环境中,将会使用 Verison>=0.0.0 的方式选择 Bundle,只要最新版本可用,就会使用最新版本替换新环境。

2.3固定版本和非固定版本混合使用方式

依据 pom 依赖树转换,遇到无法转换的版本不继续处理,最终转换后的 Bunlde 中 Import-Package 和 Export-Package 会存在固定依赖版本、无版本信息或者版本区间。例:

Import-Package: org.apache.commons.collections.comparators;version="[3.2.0,3.2.0]
",org.apache.commons.collections.keyvalue;version="[3.2.0,3.2.0]
",org.apache.commons.collections.list;version="[3.0.0,3.2.0]
",org.apache.commons.collections.set;version="[3.0.0,3.2.0]
",org.apache.commons.logging

3.maven-bundle-plugin插件转换的 Bundle

通常 OSGi 环境的开发相对比较复杂,从上面也可以看出对于 Bundle 的转换更加复杂,于是业界有提供这样的 Maven 插件 maven-bundle-plugin(具体见参考资料 [1])来做这个事情,所采用的转换方式正好是固定版本和非固定版本混合使用方式。

其实 maven-bundle-plugin 是使用 bnd 插件完成对 Bundle 的转换。这个 Maven 插件对于 Bundle 的转换主要使用 asm 解析,读取 Jar 文件中的 class 文件字节码信息,解析 class 文件中的 package 和 import package 信息,最后分析重写 Manifest.MF 的 Import-Package 和 Export-Package。

这种模式在一个独立项目中使用,可能不会存在什么大问题,但是这个 Maven 插件转换后的 Bundle 无法很好处理几种特殊应用场景,下面从 OSGi Bundle 的 2 种依赖情况来详细分析下采用这种转换模式转换后的 Bundle 问题。

3.1转换为固定版本依赖

(图 1)

如图 1 所示,假如是多人多部门协作开发的环境,那么就很有可能不同 Bundle 是由不同开发角色开发的,这时候 BundleA1.0.0 要求说必须使用 BundleA1 1.0.1,BundleB 1.0.0 要求说必须使用 BundleA1 1.0.0。但是,他们所依赖的 BundleA1 的是冲突的,所以这时需要指定 BundleA 和 BundleB 依赖各自对应版本的 BundleA1 来解决,此时 2 个 BundleA1 同时在一个应用环境中存在并同时提供服务,固定版本方式的 Bundle 依赖很好解决了冲突问题。不过这种情况下,BundleA 和 BundleB 同时存在会造成 2 个 Bundle 之间通信出现问题,这是由于同一个类(BundleA1 中的类)进行类型转换时会因为 classLoader 不同而造成 ClassCastException 异常。在第 4 节“插件的改进”中将会详细描述如何解决固定版本模式出现的问题。

3.2 转换为非固定版本依赖

(图 2)

在多人协作开发或者分布式应用环境中,如果 BundleA 和 BundleB 不指定依赖的 BundleA1 版本,那么在 OSGi 应用环境中,应用会使用 verison>=0.0.0 的方式选择 Bundle,只要最新版本的 Bundle 可用,就会使用最新版本更新环境。如图 2 所示,BundleA 1.0.0 和 BundleB 1.0.0 均选择了最新版本的 BundleA1 1.0.1 安装,假如 BundleA1 提供了 1.0.0 和 1.0.1,而且这 2 个版本是冲突的。BundleB 1.0.0 会要求必须使用 BundleA1 1.0.0,这时候的应用环境就成了图 2 这种情况,会造成最后安装的 BundleA1 环境不稳定,导致 BundleB 1.0.0 功能出问题。反之,使用 BundleA1 1.0.0,那么 BundleA 将会出现问题。 maven-bundle-plugin 插件对于非遵守 OSGi 规范的 Jar 转换大多都会这种形式,因此在 OSGi 应用环境开发过程中尽量使用固定版本方式,虽然这种情况的开发相对困难些,但是改进好插件还是能够很好的帮助我们规避掉以后维护固定版本的工作量,而且能辅助我们很好的定位 Bundle 依赖出现的问题。

3.3插件的功能问题

如果要转换的 Jar 本身是 OSGi Bundle,而且 MANIFEST.MF 的 Import-Package 中存在内容,原先 Maven 的 maven-bundle-plugin 插件则会继续使用该 Import-Package。当然 Import-Package 的内容会遇到一些不规范的版本,特别是从未接触的开发人员,模仿着写,Import-Package 写的有些问题,依赖信息错了,甚至由于开发人员疏忽写了些错误版本,这时候需要我们去修正为正确的信息,同理 Export-Package 也存在同样的问题。

另外 MANIFEST.MF 中没有 Import-Package 内容时:maven-bundle-plugin 利用 asm 来解析 *.class 文件里 import package 来提取,提取方式如下所示:

(图 3)

图 3 BunldeA 中只有这个类,那么 BundleA 的 Import-Package 为 java.lang.reflect,org.objectweb.asm,com.wzucxd.classloader。

可以看出这种由 maven-bundle-plugin 插件转换的版本信息丢失很明显,再则由于 bnd 根据 maven pom 依赖进行转换,如果 pom 中有 exclusions 写法,那么当转换 Bundle 过程中,认为这个 Bundle package 依赖是不需要的,Import-Package 会去除这个 package,对于所依赖的 Bundle 来说可能会造成 deploy 失败。如 BundleA1.0.0 依赖的 BundleA1 1.0.1 需要 com.wzucxd.classloader 这个 package,但是由于在 BundleA1.0.0 的 pom 中把 BundleA1 1.0.1 对应的 com.wzucxd.classloader package exclusion 出去了,而 BundleA1.0.0 的 package 中 import 进来,这种情况就有可能造成 BundleA1 1.0.1 deploy 失败了。

总之,maven-bundle-plugin 插件转换后的 Bundle,在应用运行过程中会存在的上述问题,故可以针对这几个改进措施来弥补插件的不足:

  1. 允许为所有 package 强制指定固定版本
  2. 修正 OSGi Bundle Jar 中不正确的 Import-Package 和 Export-Package
  3. 解决 exclusions 等情况的版本丢失问题
  4. 解决可选版本问题,正确使用 resolution:=optional

4.插件的改进

从 Maven 插件的不足中,我们也看到了核心问题在于该如何解决全部采用固定版本 Bundle 依赖时的转换问题。针对图 1 固定版本依赖这个问题,我们可以将冲突类放到 bootdetegation 中,但是这种解决方式不建议,这样做也就把这个类让 WebAppClassLoder 加载了,让应用方去解决 maven 冲突,所有 Bundle 中的这种冲突都会需要解决,显然这样不是一个好方式,这样就回到了原始社会。但是我们可以采用固定版本方式结合 extra 模式,这样可以很好的解决这个 Bundle 交互问题,将所有 Bunlde 做成固定版本的 Import-Package 到 extra 中,extra 中指定的版本会用加载 OSGi Framework 的 classloader 加载,具体可以看下参考资料 [2] 里的内容,这种方式避免了大量使用 bootdelegation 来解决类型转换问题。

因此下面将具体介绍如何转换固定版本 Import-Package 和 Export-Package 的方法。

4.1 Export-Package转换

(图 4)

从上面总结下来,对于 Export-Package 中的 package 版本修正,遇到没有使用版本或者错误信息的 package 时可以选择指定成当前转换 Jar 的 pom version。

处理方法

转换过程可采用后序遍历方式逐级转换 Jar,如图 4 所示,逐级转换 asm-all,org.apache.felix.ipojo.metadata->org.apache.felix.ipojo。

  1. 将 Jar 中所有的 package 提取出来
  2. 继续把 MANIFEST.MF 中的 Private-Package 的 package 信息收集起来。

    另外在 Bnd 中,对应下面的代码可用来获取当前 Jar 的所有 package,但是这个 package 是包含 Private-Package 的:

    (图 5)

    最后将这些 packages 减去 Private-Package,并带有当前转换 Jar 的 version 的内容作为 Export-Package。

  3. 解析当前 Jar 文件对应的 pom 文件,用 pom version 覆盖掉原先的 version 值,让所有 Export-Package 的 version 对应具体实际使用的版本。

    重新生成的 Export-Package 和 MANIFEST.MF 文件中原先的 Export-Package 数据这这个时候进行合并,于是最终对应的 version 值都会改成和该 Jar 对应的版本。

  4. 如 griffin.core.module:1.0.5 的 Export-Package:

    (图 6)

    处理后还存在无版本的 package,则从 Export-Package 中去除,因为这种情况的 package 并不是他自己提供的,是由于原始的 OSGi Bundle Jar 写的不规范

uses语法的使用

转换后的 Bundle 在使用过程中,有时会遇到一个问题:interface 类中使用了其他 package 的类,而这个 interface 类在实现类型的 Bundle 中却没有使用到。

如下图 7 和图 8 中的 TemplateEngine interface(接口 Bundle 内)和 HelloImpl 类(实现 Bundle 内),在 HelloImpl 类中 import 了 com.wzucxd.griffin.core.module.engine.TemplateEngine 接口,但是不使用 TemplateEngine 接口。这时候实现的 Bundle 将会生成 Export-Package:com.wzucxd.griffin.core.module.engine;uses:=com.wzucxd.griffin.core.module.context;version="1.0.5"

表示 com.wzucxd.griffin.core.module.engine 这个 package 要使用的时候,必须先 install 包含 com.wzucxd.griffin.core.module.context package 的 Bundle; 这样做的目的也是为了让 Bundle 依赖能够更加完整些。

(图 7)

(图 8)

4.2 Import-Package转换

对于 Import-Package 的转换要求更为严格,必须要在 Export-Package 转换后再进行转换,因为 Import-Package 的内容必须是被 Export 过的 package。

转换方法:

使用 4.1 的方式转换好 Export-Package

  1. 第 1 次后序遍历,转换时候先后序遍历转换所有的 Jar,(maven dependency:tree 可以参照 maven 的 DependencyTreeBuilder 实现);
  2. 转换 Jar 时,读取该 Jar 里所有 java 文件的字节码,分析 class 文件中的 import 内容,将所有 import 内容提取出来。并记录下所有依赖树里的 package 和 Jar version 信息,为 V1<package,version> map 集合。
  3. 在所有 Jar 转换完成后,将转换完成后的 V1<package,version> 与第 1 步 Export-Package 转换完成后记录的 package version 合并为 V2<package,version>
  4. 第 2 次后序遍历,将 Import-package 的版本信息用第 2 步合并的 V2<package,version> 数据进行重写。这时候正常来说版本信息已经全部为固定版本信息。
resolution:=optional的使用:

第 2 次转换中会遇到一些异常情况,例如遇到原先没有版本,但是这个版本在 maven 仲裁中有定义,那么可使用 maven 仲裁的版本作为固定版本,这种情况需要加上 resolution:=optional; 转换时遇到 maven 仲裁后也无版本的,也就是说这个 package 无定义,在这个 Bundle 环境中是独立的,那么这个信息可能是 Private-Package 或者原先是 OSGi Bundle 中错误的 Import,没有任何意义,这种 Package 去除掉, 当然也可以使用 resolution:=optional; 表示这个 package 在 install 的时候是可选的,应用真正需要的时候再去 install。

另外由于是根据 pom 转换 Bundle,故遇到 pom 中含有这几种配置时也要用 resolution:=optional:optional、exclude jar、exclude java 类型的配置。因为在这几种情况下转换 Bundle,是不可能知道使用的 Import-Package 版本,版本信息只有在 maven 仲裁后才知道(也有可能是开发者最后具体指定使用的版本),当前你也可以不写版本信息,但是这样不建议。resolution:=optional 使用和 Export-Package 中的 use 有点类似,有些 jar 采用 spi 方式开发,只定义了接口,但是实现由具体的 jar 来做,实现的 jar 可以有多个,如 common.logging, 可以有多个 log 日志系统,那么这个接口使用必须都 import 进去,具体使用哪个由应用来决定。

例 net.sf.json-lib_2.2.0 的 MANIFEST.MF

在这里可以看到 org.apache.oro.text.regex;resolution:=optional;version="[0.0.0,0.0.0]",这里的 resolution:=optional; 用法表示这个 package 是非必须的依赖,可有也无,只有需要使用的时候才 install,当应用中使用有 export 这个 package 的 Bundle 时,那么会提前 install 这个依赖 Bundle,而且少这个 package 的时候也可以 install net.sf.json-lib。

4.3 OSGi Bundle转换插件使用设计

这次我们从 Convert 插件使用上来看看,大多都会以什么习惯去使用 Convert 插件:

1. 在 pom 中指定 <packaging>bundle</packaging>

2. 引入 plugin(如图 9)

(图 9)

注:Instructions 中可选属性有

  • Private-Package:将需要使用的 package 内容全部打包到 Bundle 中,私有 package 的内容由 Bundle 自身的 classloader 加载,但是不建议使用,容易出现被指定的 package 在 WebAppClassloader 加载过
  • Import-Package:指定 bundle 的 import,import 如果写明具体版本,格式为 <Import-Package> xxx.xxx.xxx;version="[1.0.0,1.0.0]", xxx.xxx.xxx;version="[xxx,xxx]"</Import-Package>,将这个 Import-Package 信息覆盖 bnd 插件转换后的 Import-Package
  • Export-Package: 对外暴露的 package,可被其他 bundle import 使用,Export-Package 的所有 package 必须带版本,而且建议和 pom 的版本一致,版本也可以自己指定
  • Ignore-Package: 这里的 Ignore-Package 作用:转换的时候过滤掉这些 package 信息,在所有 Bundle 转换之前,可以提前配置了一个 bnd.properties 文件,里面指定所有 Import-Package 不需要 import 的 package,里面的内容基本为 jdk 里的 package,因为 jdk 刚开始由 WebAppClassLoader 加载, 当然可以为每个 Bundle 默认写上 Ignore-Package。
  • Bundle-SymbolicName:Bundle 的名字,建议用 ${project.groupId}+${project.artifactId}的组合
  • Interface 类型的 Bundle 在开发过程一般只写 Export-Package 就好,如果接口 Bundle 作为二方库方式开发也可以,在 Bundle.implement 中引用接口 pom 的时候,插件在转换 Bundle.implement 之前转换对接口做 Bundle Convert,为接口 Bundle 中的 MANIFEST.MF 写入正确的 Import-Package 和 Export-Package 信息。
  • Bundle.implement 作为实现 Bundle,一般不需要写 Export-package/Import-Package,一些特殊的 Bundle,如这个 Bundle.implement 实现使用了动态代理的 service,那么大多可以在 Import-Package 中指定这个 service interface 的 package 来解决。

插件编译参数设计,为了提升效率,必要的编译参数也是需要的:

插件编译参数设计,为了提升效率,必要的编译参数也是需要的:
// 编译前是否清理缓存目录 
maven install -Dbundle.clean=true/(false)
// 是否编译 snapshot
maven install -Dsnapshot.rebuild=(true)/false
// 只编译指定的 bundle
maven install -D buildBundle=com.common.util

这样对 Bundle 的 Import-Package 和 Export-Package 可以进行一定程度的自定义了,但是我们还是更希望尽量一次性完整的转换好 Bundle,用插件来完全负责转换 Import-Package 和 Export-Package。

4.4 MANIFEST.MF中其他信息的定义

Bundle-Convert:表示从普通 Jar 转为 OSGi Bundle 后的表示

Bundle-Build:表示原先是标准的 OSGi Bundle,如自己开发的标准 Bundle.implement

Bundle-Sha1:表示这个 Bundle 的唯一版本信息,计算方式:BundleConvertUtil.getSha1(File file);具体 sha1 或者 md5 计算方法很多,这里的具体作用还为了以后将转换后的 Bundle 保存到一个组件仓库中避免重复 Bundle 的多次转换,也可以用来区分一个 Bundle 是否被多次编译。

5.总结

改进后的 Convert 插件也还存在一个问题,pom 转换为 OSGi Bundle 时出现的 Jar 版本冲突,允许 2 个版本都可以使用,这里的方法是使用自己解决冲突的方式,插件转换时用了 maven 仲裁的版本。这里的版本就相当坑,maven 使用树状最短路径版本,而 OSGi Bundle 使用的是图状关联版本,故这个时候使用多个版本的 OSGi Bundle 会使用 version="[xxx,xxx]"区间值表达,但是在我们这里还是建议使用 maven 仲裁选择一个固定版本。当然固定版本选择是为了支持 extra 使用,所以在这个插件使用的时候,不建议应用方在 pom 中的 Jar 依赖与接口 Bundle 的 pom 依赖有冲突,冲突需要应用方提前先解决,当然这个做法也是合理的,因为应用新引入一个接口 Jar 的时候,pom 依赖有冲突那么需要提前解决。在插件中也可以在转换过程中就将有冲突的 Package 提前抛出异常告知开发者,在编译期就让开发者解决掉这个问题。

6.参考资料

[1] apache-felix-maven-bundle-plugin-bnd 插件 http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html

[2]Exposing the boot classpath in OSGi http://spring.io/blog/2009/01/19/exposing-the-boot-classpath-in-osgi/

作者简介

陈旭东,阿里巴巴资深研发工程师,主导过阿里巴巴地图的架构设计,对搜索规则和旺铺 SEO 优化有非常全面资深的了解。目前主导搜索应用的架构设计和搜索应用 Osgi 框架研发。


感谢张龙对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。