bootdelegation vs. org.osgi.framework.system.packages.extra

osgi alliance support
osgi alliance support

在用osgi的过程中,如果需要osgi容器内外互通,则一般需要osgi容器内外共享类,否则会抛出各种ClassNotFoundException或者ClassCastException。常见的解决方法有两种,即把需要共享的类所在包名配置在这两个osgi启动参数上:org.osgi.framework.bootdelegation和org.osgi.framework.system.packages.extra。

下面分别介绍这两个参数的特点(以下解释都是基于osgi-core-4.3.0):

org.osgi.framework.bootdelegation

配置这个参数时往往需要一起配置另一个参数:org.osgi.framework.bundle.parent(详见osgi.core-4.3.0.pdf的9.1.15.47节),用来指定配置在org.osgi.framework.bootdelegation上的包中的类由什么类加载器加载,这个参数的值有如下几个:boot(默认)、ext、app、framework四个。

  1. boot:父加载器默认是用的这个,引导类加载器(bootstrap class loader),它用来加载 Java 的核心库,是用原生代码来实现的。
  2. ext:扩展类加载器(extensions class loader),它用来加载 Java 的扩展库。
  3. app:系统类加载器(system class loader,下文中出现的app类加载器均指的是系统类加载器),它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. framework:osgi框架容器的类加载器。

由于通常我们的程序都是系统类加载器来加载的,而默认的osgi容器的父加载器是boot(引导类加载器),这两个加载器加载同一个类得到的实例和引用是不能相互赋值转换的,为了能让osgi容器中的bundle能共用容器外的类且由同一个加载器加载,就必须设置这些被共用的类由相同的类加载器来加载,所以一般设置为系统类加载器,即org.osgi.framework.bundle.parent=app,参考:Java类加载器的双亲委派模型

osgi.core-4.3.0.pdf的3.9.3节:
委托给父加载器加载的包清单格式规范如下:
org.osgi.framework.bootdelegation ::= boot-description ( ‘,’ boot-description )*
boot-description::= package-name | ( package-name ‘.*’ ) | ‘*’
比如:org.osgi.framework.bootdelegation=sun.*,com.sun.*

  • *是通配符,意味着com.acme.*可匹配所有com.acme.下的所有子包,但是不包括com.acme包。注意此时的通配符仅仅是对package的通配,不会对类进行统配,比如com.acme.*这个通配是不能匹配com.acme.AAA的,但是可以匹配com.acme.sav.BBB和com.acme.sav.ttr.CCC,因为com.acme.*的意思是用”com.acme.”对包名(不包括类目以及类目前面的点号)作为前缀匹配,只要符合前缀条件就匹配上了。
  • 如果不使用通配符,则表示精确匹配。
  • 以java.*为前缀的包会默认委托给父加载器来加载。

凡是配置在org.osgi.framework.bootdelegation参数的包中的类,都是由org.osgi.framework.bundle.parent对应的类加载器来加载。osgi中的bundle需要使用这些包,无需其他配置,也不用import这些包,classloader会在碰到这个类的时候优先委托给org.osgi.framework.bundle.parent对应的类加载器加载。

适用场景:某些包所在的jar不可能转成bundle时,只能通过org.osgi.framework.bootdelegation参数来达到osgi内外共享。比如:rt.jar中的类,rt.jar是jdk、jre中的jar包,这个jar不是bundle,你也不太可能去把这个包转换成bundle,osgi内部的bundle自然就无法和外部共享这些类了,而bootdelegation参数正好解决了这个问题。

org.osgi.framework.system.packages.extra

org.osgi.framework.system.packages.extra是对org.osgi.framework.system.packages的扩展,配置到这个参数上的包都将被osgi framework类加载器加载(通常就是AppClassloader加载),这些包可以来自bundle,也可以来自非bundle,往往osgi framework类加载器就是app类加载器(除非自己写了个类加载来加载启动osgi的类,比如:在web容器中使用osgi,那么osgi framework类加载器就是web容器加载war包的类加载器,不是app类加载器),所以这些类外部app和osgi容器内部是共用class的,可以相互赋值和互传。同时这个参数还可以设置包的版本。注意:org.osgi.framework.system.packages与该参数基本意义相同,但是不能随意给org.osgi.framework.system.packages设置值,因为他的默认值是框架所需的包,默认值为org.osgi.framework,如果设置其他值,则会覆盖原来的默认值,所以一般都是给org.osgi.framework.system.packages.extra设置值,用来把某些包中的类共享到osgi外。

org.osgi.framework.system.packages.extra的配置规则如下:

  1. 可以配置多个package,每个package之间用逗号隔开
  2. package可以配置版本信息,如果没有配置版本,则默认其版本为0.0.0
  3. package不能写成类似的org.osgi.framework.bootdelegation通配符形式,因为extra相当于启动osgi容器的classloader export了package给osgi中其他的classloader,用到这些package的bundle需要显性的import这些package(import时若没写版本号,则默认import的版本号为0.0.0),且import的包的版本号不能大于org.osgi.framework.system.packages.extra里面配置的版本号。如果大于的话,会抛出类似这样的错误: org.osgi.framework.BundleException: Unresolved constraint in bundle hello.impl [8]: Unable to resolve 8.0: missing requirement [8.0] osgi.wiring.package; (&(osgi.wiring.package=ipojo.example.hello)(version>=1.5.0))

对osgi类加载顺序不了解的请看这篇:osgi类加载顺序源码解析

有以下的两种case需要注意:

  • 如果一个bundle X包被bundle Y require了(即在require-bundle中),如果这个required bundle X中的包packageA export出去了,然后在org.osgi.framework.system.packages.extra中设置了这个packageA,如果bundle Y中的某个类使用了这个packageA中的类A,但没有import packageA,那么类A不会由app classloader加载,因为类加载顺序是先bootdelegation,再import-package,再require-bundle,再local,(import里面以extra的包优先,require-bundle里面没有extra的逻辑),因为这个bundle没有import packageA,所以尽管配置了org.osgi.framework.system.packages.extra,也不会走进import的逻辑,而是进入了local的逻辑,从而由bundle自己的classloader加载packageA中的类。
  • 如果bundle Z中包含packageA,若想要让pacakgeA由app classloader加载,且让bundle Z中的其他类也能使用pacakgeA中的类,则需要bundle Z同时export和import这个packageA,并在extra中配置packageA,且外部调用的应用也要把这个package所在jar包作为第三方包依赖上(否则,app classloader在加载package中的类是会发现在app classloader的classpath中找不到这个类,因为这个类在bundle的classpath中。也就是说其实app classloader加载是外部应用依赖的jar包中的packageA,而不是bundle中的packageA,但是二者是一样的,app classloader一旦加载过,class实例就已经存在了,后面碰到相同的类时就不需要再加载了。也就是说在运行时bundle的jar中的packageA并没有作用,但是为了编译通过,bundle依赖了packageA而已,这都是为了让osgi容器内外互通的不得已。

一切都是按照类加载顺序逻辑来!!!!!

总结对比

为了简化,以下bootdelegation代表org.osgi.framework.bootdelegation,extra代表org.osgi.framework.system.packages.extra

  1. bootdelegation和extra都是为了解决osgi内外类共享的问题。
  2. 配置语法上各有优劣,bootdelegation支持通配符,但不支持包版本;extra不支持通配符,但支持包版本配置。
  3. bootdelegation可以指定4种类加载器,extra只能是app classloader。
  4. bootdelegation配置的包不需要在bundle的menifest.mf中import,整个osgi中甚至可以根本没有bootdelegation配置的包;如果其他bundle想用extra配置的包,那需要其他bundle import这个包的,只是这个包不再是由包所在的bundle的类加载器加载,而是由app classloader加载。如果extra配置的包是某个指定版本的,那么import了这个指定版本的bundle将使用framework类加载器加载,相同包的其他版本还是由bundle的classloader加载。
  5. 形象的说,bootdelegation相当于是把osgi外部的类共享给了osgi内部使用;extra相当于把osgi内部的类共享给了外部的使用,当然extra也可以用于把osgi外部的类共享给osgi内部使用,只是需要想用外部类的bundle import一下。

Reference

使用iPOJO时抛出异常java.lang.UnsupportedOperationException: Cannot add elements inside this collection

 

felix ipojo
felix ipojo

Felix iPOJO的功能很强大,配置也比较复杂,配置和实现代码之间有着千丝万缕的关联,完全脱离代码去配置iPOJO的component和instance势必不会达到自己想要的依赖注入效果,甚至很难查到问题的原因。尽管iPOJO的配置有iPOJO xml schemaiPOJO Annotation,但是仅仅按照schema和annotation来不一定能配置正确,还需要理解iPOJO配置背后的一些机制和原理,否则会出现一些奇怪的异常。见如下的代码和iPOJO annotation注解:

上面的代码乍一看没啥问题,但是其实这个component的timeoutConfigList属性是无法注入成功的,这和iPOJO的一些的依赖注入实现机制有关系,如果用以上的代码来配置instance,在启动这个component和instance所在的bundle时会抛出以下的异常堆栈:

我们可以看到属性timeoutConfigList是List集合类型,它上有@Requires的annotaion,又有对这个属性的@Bind和@Unbind方法。这种用法是不合适的。如果使用不当就会出现上面的异常。因为当在一个属性上注明了@Requires的annotaion时,就表示这个属性需要由ipojo来负责依赖注入,ipojo默认会为集合类型的属性创建一个ServiceCollection类的代理对象,也就是说timeoutConfigList属性在被替换成了ServiceCollection类的对象,调用这个对象里的所有修改方法都会抛出UnsupportedOperationException异常,因为这个属性已经交由ipojo的来维护了,对其进行修改、添加或者删除的话,可能会导致ipojo维护对象的混乱。而@Bind和@Unbind方法是在属性的代理对象创建后,当有适合的属性类型的对象满足注入条件时回调的方法,在以上的代码中有add和remove的操作自然会导致抛出上面的异常了。

解决办法

方法一: 同时使用@Requires注解以及@Bind和@Unbind方法时,保证@Bind和@Unbind方法中没有修改操作(可以执行非修改操作,比如contains、size、get等,详见ServiceCollection类的源码)

方法二: @Requires注解与@Bind、@Unbind方法不同时出现。其实@Bind、@Unbind方法基本包含了@Requires注解的功能,而且还可以对这个属性对象的注入进行aop操作,相比@Requires注解只是需要多写些判空和初始化的代码而已。

方法三:在示例代码的基础上,在@Requires的注解中添加proxy=false,这样示例代码就不会抛出异常了(这个时候这个属性在没有被bind过前,其值为null,所以需要做判空处理),比如下面的写法:

(如果没有使用annotation,而是component的配置也是用的xml方式,解决方案还是如上面的讲的三点相类似。)