Osgi Bundle Convert插件原理

 由  陈旭东 发布

[作者]

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

Osgi Bundle Convert插件原理

Osgi环境的开发相对比较麻烦些,对于Bundle的转换更加复杂,业界有提供这样的maven插件maven-bundle-plugin(http://felix.apache.org/site/apache-felix-maven-bundle-plugin-bnd.html)来做这个事情,maven-bundle-plugin也是使用bnd插件完成,但是普通的jar转换为osgi Bundle有很多情况无法处理,需要我们自己来处理。在一个独立项目中使用maven-bundle-plugin插件,可能不会存在什么大问题,但是做分布式组件,共享组件仓库,设计的仓库这个插件就不一定能帮上忙了。Osgi Bundle的依赖情况简要来说主要有2种:
1.固定版本方式依赖
固定版本类似于maven中的jar版本强依赖,这个bundle必须import哪些固定版本的bundle,这样做的好处在于每个bundle使用环境都是一个独立的依赖集合,如图1。集合之间没有任何冲突可言。

http://assets.osgi.com.cn/article/7289507/图片1.jpg

(图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异常。

这种情况又该如何解决呢?可以将这个冲突类放到bootdetegation中,但是这种方式不建议中,这样做把这个类让WebAppClassLoder加载,让应用方去解决maven冲突,所有的bundle中的这种冲突都需要解决,显然这样不是一个好方式。但是我们可以采用固定版本方式结合extra方式,可以很好的解决这个通信问题,将所有固定版本的import-package做到extra中,extra中指定的版本会用加载framework的calssloader加载,具体可以(http://spring.io/blog/2009/01/19/exposing-the-boot-classpath-in-osgi/) ,也避免了大量使用bootdelegation来解决类型转换问题,用maven-bundle-plugin就没法很好解决了,后面将重点讲下如何做bnd convert固定版本的import-packege和export-package插件。

2.非固定版本方式
这种情况也就是目前经常用的不指定版本范围或者无版本package,特别是无版本package,在osgi环境中,只要最新版本可用,就会使用最新版本替换新环境。这里为什么说会有问题呢?因为在多人协作开发或者分布式环境中,如果BundleA和BundleB不指定依赖的BundleA1版本,也就是会造成最后安装的BundleA1环境不稳定,可能为1.0.0或者1.0.1,只要BundleA1之间的2个版本是冲突的,于是无论是那个版本都会造成BundleA 1.0.0或者BundleB 1.0.0功能出问题,所以建议osgi环境开发过程中使用固定版本方式,虽然这种情况开发比较困难。

下面我们继续来看下maven-bundle-plugin,如果要转换的bundle是Osgi Bundle,那么没有任何问题,bnd转换后会继续使用该jar中MANIFEST.MF的Import-Package来处理,可是有很多Osgi Bunlde开发不规范,写了些错误版本,特别是从未接触的开发,模仿着写,import-package写的有些问题,依赖版本错了,这时候我们就需要去修正了。同样Export-Package也有同样的问题;另外一种情况是如果MANIFEST.MF中没有import-package那么maven-bundle-plugin是利用asm来解析*.class文件里import package来提取,如下所示:

http://assets.osgi.com.cn/article/7289507/图片2.jpg

(图2)

如图2 BunldeA中只有这个类,那么import-package为java.lang.reflect,org.objectweb.asm,com.wzucxd.classloader而且这种由bnd插件转换的bundle版本信息丢失很明显。另外bnd由于是根据maven pom依赖来转换的,这里可以看到另外一个问题:如果maven中有exclusions写法,那么当转换bundle过程中,import-package会去除这个package,因为转换时也可认为这个bundle是不需要的,对于所依赖的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失败了。这种非必须的package转换怎么做呢?osgi标准中给我们提供了resolution:=optional用法,这种情况的package不能直接去除,要加上resolution:=optional。

说到这,我们还需要对Bundle转换插件做些改进,让其更加完美,对Import-Package和Export-Package有更强的控制。

这会我们再从Convert插件使用上来看看,大多都会怎么去使用Convert插件:
1.在pom中指定bundle
2.引入plugin

http://assets.osgi.com.cn/article/7289507/图片3.jpg

(图3)

注:Instructions中可选属性有
Private-Package:将需要使用的package内容全部打包到bundle中,私有package的内容由bundle自身的classloader加载,但是不建议使用,容易出现package被指定了在WebAppClassloader加载过
Import-Package:指定bundle的import,import如果写明具体版本,格式为 xxx.xxx.xxx;version=”[1.0.0,1.0.0]”, xxx.xxx.xxx;version=”[xxx,xxx]”,那么将这个Import-Package覆盖bnd转换后的Import-Package
Export-Package:对外暴露的package,可被其他bundle import使用,Export-Package的所有package必须带版本,而且建议和pom的版本一致,版本也可以自己指定
Bundle-SymbolicName:bundle的名字,建议用${project.groupId}+${project.artifactId}的组合

Interface类型的bundle在开发过程一般只写Export-Package就好,如果接口bundle作为二方库方式开发也可以,在bundle.implementatio中引用接口二方库的时候,插件需要支持对接口做bundle convert,为接口中的MANIFEST.MF写入正确的Import-Package和Export-Package等信息。

Bundle.impl作为实现bundle,一般不需要写Export-package/Import-Package,一些特殊的bundle,如这个bundle.impl实现使用了动态代理的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

Convert插件原理:

http://assets.osgi.com.cn/article/7289507/图片4.jpg

(图4)

Export-Package转换原理:
从上面总结下来,对于Export-Packge中的package版本需要修正,对于没有使用版本的package指定成当前转换jar的pom 中指定的version。
方法:将jar所有的package提取出来,再把MANIFEST.MF中的Private-Package的package排除掉,最后再把当前jar对应的pom version覆盖掉原先的version,让所有Export-Package的version对应具体的版本。处理后还存在无版本的package,则去除,因为这种情况的package并不是他自己提供的,是由于原始的Osgi Bundle Jar写的不规范。转换可采用后序遍历方式逐级转换,如图4所示,逐级转换asm-all,org.apache.felix.ipojo.metadata->org.apache.felix.ipojo。

在Bnd中对应下面的代码用来获取当前jar的所有package,但是这个package是包含private-package的:

http://assets.osgi.com.cn/article/7289507/图片5.jpg

最后将这些packages 减去private-package,并带有当前转换jar的version的内容作为export-package。
提取后的Export-Package和MANIFEST.MF文件中原先存在的Export-Package最后再合并,于是最终对应的version值都会改成和该jar对应的版本。
例如griffin.core.module:1.0.5的

http://assets.osgi.com.cn/article/7289507/图片6.jpg

在这里我们看到还有uses的用法(标识这个package要使用的时候,必须先install包含uses中package的bundle;)
uses类型的package生成原因:interface类中使用了其他package的类,但是这个interface类在实现类型的bundle中没有使用到。
如接口bundle中有TemplateEngine interface

http://assets.osgi.com.cn/article/7289507/图片7.jpg

而另外一个依赖的bundle.impl中的类只是import这个接口,但是没有使用这个接口

http://assets.osgi.com.cn/article/7289507/图片8.jpg

这时候bundle.impl生成的export-package为com.wzucxd.griffin.core.module.engine;uses:=com.wzucxd.griffin.core.module.context;version="1.0.5"

Import-Package转换原理:
对于Import-Package的转换要求更为严格,要在Export-Package转换后再转换,因为Import-Package的内容必须是被Export过的package。
另外需要解决之前描述的几个问题:
1. 修正标识为Osgi bundle中不正确的Import-Package的jar
2. 转换不带有任何Import-Package的jar
3. 需要解决exclusions等情况的版本丢失问题
4. 解决非必要版本问题,resolution:=optional正确使用
方法:做2次转换,第1次转换时候先后序遍历转换maven dependency-tree(可以参照maven的DependencyTreeBuilder实现) :转换jar时,读取该jar里所有java文件的字节码,分析class文件中的import内容,将所有import内容提取出来。并记录下所有依赖树里的package和jar version信息。
第2次后序遍历2次转换的时候,将Import-package的版本信息用第1次转换记录下来的版本和之前转换Export-Package时候记录的package version进行重写。这时候正常来说版本信息已经全部为固定版本信息,如net.sf.json-lib_2.2.0的MANIFEST.MF;
第2次转换还会遇到一些异常情况,如果遇到原先没有版本,但是发现这个版本在maven仲裁中有定义,那么使用maven仲裁的版本作为固定版本,并加上resolution:=optional;转换时遇到maven仲裁后也无版本的,也就是说这个package无定义,在这个bundle环境中是独立的,那么这个信息可能是Private-Package或者原先是Osgi Bundle中错误的Import,没有任何意义,这种Package去除掉。

这里为何要做2次转换?原因:由于是根据pom转换bundle时,依赖树有几种情况的配置:optional,exclude jar,exclude java等类型的配置,这种情况下转换bundle的时候是不可能知道正在使用的Import-Package版本,版本信息只有在maven仲裁后才知道(也有可能是开发者最后具体指定使用的版本),当前你也可以不写版本信息,但是这样不建议。 为何第2次转换有些Import-Package需要加resolution:=optional?这个和Export-Package中的use有点类似,有些jar采用spi方式开发,只定义了接口,但是实现由具体的jar来做,实现的jar可以有多个,如common.logging,可以有多个log日志系统,那么这个接口使用必须都import进去,具体使用哪个由应用来决定。resolution:=optional就提供了一样的功能做这个事情。
Ignore-Package:
com.sun.jdmk.comm,javax.swing.text,javax.swing.border,javax.swing.tree,javax.swing,com.ibm.uvm.tools,javax.swing.table,javax.swing.event,这里的Ignore-Package作用:转换的时候过滤掉这些信息,在所有bundle转换之前,已经可以配置了一个bnd.properties文件,里面指定所有Import-Package不需要import的package,里面的内容为jdk里的package,因为jdk刚开始由WebAppClassLoader加载, 当然也可以为每个Bundle写上Ignore-Package。

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

http://assets.osgi.com.cn/article/7289507/图片9.jpg

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

MANIFEST.MF中的其他信息定义:
Bundle-Convert:表示从普通jar转为osgi bundle后的表示
Bundle-Build:表示原先是标准的osgi bundle,如自己开发的标准bundle.impl
Bundle-Sha1:表示这个bundle的唯一版本信息,计算方式:BundleConvertUtil.getSha1(File file);具体sha1或者md5计算方法很多,这里的具体作用还为了以后将转换后的bundle保存到一个组件仓库中避免重复bundle的多次转换,也可以用来区分一个bundle是否被多次编译,因为编译时间不同会生成不同的bundle,但是里面的内容可以没有做任何改动。

Convert插件另外的问题:pom转换为Osgi Bundle时候,出现jar版本冲突时候又该如何处理?目前是使用自己解决的方式,插件转换时候也会用maven仲裁的版本,这里的版本就相当坑爹,maven使用树状最短路径版本,而Osgi Bundle使用的时候图状关联版本,故这个时候多个版本osgi bundle会使用version=”[xxx,xxx]”区间表达,但是在我们这里建议使用maven仲裁选择一个固定版本。当然固定版本选择是为了extra使用,所以在这个插件使用的时候,应用方不建议在pom中依赖的jar与接口Bundle的pom依赖有冲突,冲突需要应用方提前先解决,而这个做法也是合理的,因为应用新引入一个接口jar的时候, pom依赖有冲突那么需要提前解决。在插件中也可以在转换过程中就将有冲突的Package提前抛出异常告知开发者,在编译期就让开发者解决掉。


sswhsz 2014-05-12 10:33

楼主你好! 有没有相关的 BND源码分析 类的文章呢?

顶(0) 踩(0) 回复

salmon 2014-04-16 14:22

源码在哪里可以下

顶(0) 踩(0) 回复
查看评论