`
wzucxd
  • 浏览: 25029 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

OSGi Bundle Convert插件原理

 
阅读更多

本文首发于Infoq中文站(4.2):http://www.infoq.com/cn/articles/osgi-bundle-convert-plugin-principle

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转换为固定版本依赖

alt

(图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转换为非固定版本依赖

alt

(图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来提取,提取方式如下所示:

alt

(图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转换

alt

(图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的:

    alt

    (图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对应的版本。

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

alt

(图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依赖能够更加完整些。

alt

(图7)

alt

(图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

alt

在这里可以看到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)

alt

(图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 OSGihttp://spring.io/blog/2009/01/19/exposing-the-boot-classpath-in-osgi/

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics