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方式,解决方案还是如上面的讲的三点相类似。)

 

obr安装资源的时抛出异常:java.lang.IllegalStateException: Framework state has changed, must resolve again.

Apache Felix OSGi Bundle Repository
Apache Felix OSGi Bundle Repository

在使用obrApache Felix OSGi Bundle Repository)的实现类org.apache.felix.bundlerepository.Resolver分析和安装资源的时可能会抛出这个异常:java.lang.IllegalStateException: Framework state has changed, must resolve again.

见代码:

在deploy之前先调用resolve方法,确保这个resolver所需的requirement都能满足,如果有任意的requirement没有满足,都进不了if语句里面。但是有时会碰到resolver.resolve()返回true时,执行deploy抛出上面的异常的情况。

异常堆栈如下:

java.lang.IllegalStateException: Framework state has changed, must resolve again.

at org.apache.felix.bundlerepository.impl.ResolverImpl.deploy(ResolverImpl.java:500)

。。。。。

原因在ResolverImpl.java的deploy(int)方法中,见如下的代码:

m_repositories[repoIdx].getLastModified()这是每个repository的最后一次的修改时间

m_resolveTimeStamp这是所有repository在执行resolve()方式时初始化的一个时间,这个时间是所有的repository最后修改时间的最大值。

上面的判断是为了确保在执行resolve()方法和deploy(int)方法之间,所有的repository都没有被修改。

而某些特殊情况由于异步执行的原因可能导致某些repository的最后修改时间在执行resolve()后deploy(int)之前被修改。下面是具体的分析:

m_repositories可以分为三种:LocalRepositoryImpl、RepositoryImpl、SystemRepositoryImpl。其中LocalRepositoryImpl实现了SynchronousBundleListener接口和AllServiceListener接口的serviceChanged(ServiceEvent event)方法,由于SynchronousBundleListener是会在bundle安装和卸载时同步调用的,所以只要不异步的安装bundle(通常安装bundle都是通过resolver.deploy(int)方法来安装)就不会出现LocalRepositoryImpl被异步修改的情况的情况; 但是AllServiceListener接口的serviceChanged(ServiceEvent event)方法会在service注册和删除时修改LocalRepositoryImpl的repository最后修改时间,而bundle中的service的安装就不一定是同步的了,比如Felix iPOJO的instance的创建和注册默认情况就是异步进行的,很有可能出现service的注册发生在resolve()后,deploy(int)之前。见LocalRepositoryImpl.java中的代码:

可见如果安装了一个bundle,里面有了ipojo的instance,那么这些instance会被异步的注册到osgi中,上面的方法会被异步的调用,就会导致resolve()后,LocalRepositoryImpl的时间戳被修改,再执行deploy(int)时出现标题中的异常了。

解决办法一:如异常中所说,就是把这个resolver重新resolve一次,如下面的代码:

解决办法二:把ipojo的instance注册方式设置为同步的。ipojo提供了多个渠道可以设置instance同步注册,相关源码详见:org.apache.felix.ipojo.extender.internal.Extender.enablingSynchronousProcessing()方法
方法1:在felix的Framework properties中添加一个参数ipojo.processing.synchronous=true
方法2:在系统参数中添加ipojo.processing.synchronous=true
方法3:在org.apache.felix.ipojo bundle的menifest.mf文件中添加head信息:ipojo-processing-synchronous=true

Felix iPOJO annotation中@ServiceProperty、@StaticServiceProperty和@Property的区别

Apache Felix iPOJO
Apache Felix iPOJO

Felix的官方文档中有关Felix iPOJO annotation中@ServiceProperty、@StaticServiceProperty和@Property的区别和意义的解释很少,但是Apache Felix forum上面有,并有人有相同的问题高手进行了解答。另外在stackoverflow上面也有人回答了这个问题。

意思是:

  • @ServiceProperty会在service实例开放的时候一同开放出去,用来作为service registration的附带,在service被注册到osgi容器BundleContext中时,注册上这个属性和属性值,允许消费者instance或者component通过<properties/>过滤或者选择符合条件的service实例。
  • @StaticServiceProperty与@ServiceProperty很类似,都是可以在osgi容器BundleContext中根据这个property对service进行筛选的,只是它不用附着在一个field上,也正因为它不用附着于field,所以在配置时,它需要指定其类型type。
  • @Property则是service内部的属性,仅用来通过ipojo的配置来实现属性的初始值注入,不会开放出去,即外部的service不能根据这个属性进行service筛选。

见下面的代码和配置来看其用法和区别:

相当于是如下的xml配置:

从上面的配置可以看出,如果需要把某个属性开放到osgi容器的BundleContext(即在注册service实例的时候把属性也一起注册)就需要把属性配置在<provides/>标签中,表明这些属性是要开放给其他service实例进行过滤筛选使用的。注意:使用xml形式来配置component时,java类MyComponent中是没有static这个属性的任何信息的,xml中的这个属性仅仅是为了告诉ipojo,在注册这个component相关的instance时,把这个属性和instance配置的属性值一同注册,相当于是给这个instance实例打了这个属性的标签,其他实例需要用到这个实例时可以通过这个标签筛选出来。

instance的配置类似这样如下:

上面的instance配置,可以发现三种不同的property,在instance配置中是看不出区别的,配置方式完全相同。但是ipojo会根据其component中的信息来区分每个property的功能和作用范围。哪些property是用来给service注入值的;哪些是既注入值,有用来附带注册的;哪些仅用来附带注册的。

假设有一个类ClassRequireInstance,其中的一个field依赖了MyService接口,现在通过ipojo的配置注入这个testInstance的实例对象,看下面的ipojo配置:

上面的配置尽管对instan字段有一个复杂过滤条件,但是之前的配置的instance:testInstance仍然是满足条件的,因为testInstance的static和foo两个property跟随testInstance一起注册到BundleContext中了。filter过滤就相当于在BundleContext中进行过滤。当然过滤条件可以再宽松一点,比如:(static=My Value For Static)或者(foo=bar)。但是如果过滤条件中包含(array=1234)时,那instance classRequireInstance就找不到合适的MyService来注入了,因为array是一个仅用来做注入值的属性,不会注册到BundleContext中。

同理也可以直接写代码来从BundleContext中筛选获取testInstance:

这样就好理解了

Reference

 

felix ipojo入门教程

Felix iPOJO介绍

在OSGI框架中,Felix iPOJO意在简化面向服务编程。iPOJO的全称是inject POJO。Felix iPOJO提供了一种新的开发OSGI服务组件的方式,主要目标是简化服务组件的实现,使环境的动态变化对服务组件的实现透明。Felix iPOJO可以让开发者更好的分离功能代码(比如:POJO)和非功能代码(比如:依赖管理、服务的提供、配置等)。

Felix iPOJO服务组件简介

服务组件可以提供和(或者)依赖一个或者多个服务,服务(service)是一个实现了某个java接口的对象。另外Felix iPOJO提供了一个回调的机制,把各种状态变化通知给组件。

组件是Felix iPOJO的核心概念,一个组件描述了服务的依赖、提供哪些服务以及哪些回调功能;这些信息配置在组件描述中(metadata)。Felix iPOJO中的第二个重要的概念是组件的实例,一个组件实例是一个特殊版本的组件。通过合并组件描述和组件实例的配置,Felix iPOJO可以在运行时管理组件。比如:管理其生命周期、服务的依赖注入、开放服务、发现需要的服务。

Felix iPOJO Hello World(简单的示例)

下面的示例将阐述怎样使用Felix iPOJO的核心功能。示例中包含两个组件,一个提供Hello服务,一个需要任意数量的Hello服务。这些组件通过maven打包,被放在三个不同的bundle中。

  • hello.service包含服务接口Hello的定义
  • hello.impl包含一个组件,它实现了Hello服务接口,具有了提供Hello服务的能力
  • hello.client包含了一个组件的消费者,它依赖一个或者多个Hello服务

这个示例的代码可以在这里下载

1. 准备工作

开始前,先下载并安装配置maven

2. 第一个工程:服务接口(hello.service

这是一个maven工程,工程仅有一个Hello接口类,类的路径为:src/main/java/ipojo/example/hello/Hello.java

在工程目录中,pom.xml文件必须包含了maven-bundle-plugin插件,因为这个工程最终mvn install生成的jar包必须是符合osgi规范的bundle包,同时因为Hello这个类是接口类,需要提供给其他的bundle import,所以在pom.xml中需要export这个接口类所在的包ipojo.example.hello。如下所示:

在这个工程中使用mvn clean install命令执行后,如果结果是SUCCESS,那么maven repository里面应该有这个jar包了,另外工程的target目录下应该也有打包好的jar包。这个包将在接下来的两个工程中被依赖上。

3. 第二个工程:服务提供者(hello.impl)

这个maven工程(hello.impl)里面有一个实现了Hello接口的类,这个工程依赖了第一个工程hello.service,这个类路径为:src/main/java/ipojo/example/hello/impl/HelloImpl.java

为了管理这个组件,iPOJO需要一些描述信息来描述这个组件提供了Hello这个接口的服务。iPOJO的描述文件(metadata.xml)在hello.impl工程的根目录下,这个描述文件的内容如下:

‘component’元素的‘classname’属性是为了告诉iPOJO这个组件的实现类是哪个;‘name’属性是给这个组件取了一个名字,如果没有属性,那么它的值就是属性‘classname’的值;这个示例中‘component’元素下有‘provides’元素,这是为了告诉iPOJO需要管理这个组件怎样公布到osgi容器中,在上面的示例中,‘provides’元素没有属性,那么iPOJO会为这个组件实现的所有的服务接口都注册上这个组件,当然‘provides’元素可以指定一个或者多个服务接口。

‘instance’元素是告诉iPOJO,在这个bundle在osgi中启动的时候创建一个组件的实例。

这个maven的工程的pom.xml文件包含以下的内容:

其中<Private-Package>ipojo.example.hello.impl</Private-Package>是指HelloImpl类所在包不被export出去,其他bundle不能import到这个包下面的类。配置好后,使用mvn clean install命令打包。

4. 第三个工程:服务消费者(hello.client)

这个工程中有一个类HelloClient类,这个类中有一个Hello数组类型的field,这个field需要让iPOJO自动的注入,当有实现了Hello服务接口的组件实例被公布后,iPOJO会自动的把实例添加到field中。

在上面的代码中,服务消费者HelloClient创建了一个线程间歇地调用可用的Hello类型的服务实例,在至少一个Hello类型的服务实例出现时,这个线程会由iPOJO的回调机制启动。代码中组件的实现对象m_hello被直接使用,且m_hello是一个数组。在iPOJO中服务数组代表了一种依赖聚合或者多个合适的依赖,当然iPOJO也支持单一的简单的对象注入,只需要把代表数组的中括号去掉即可。在一个服务的组件类中声明了一个属性,那么这个组件类的其他部分代码就可以直接使用这个属性,因为这个属性会被自动初始化,比如:m_hello[i].sayHello(“world”)。

iPOJO也是同步管理服务的。服务的调用不需要使用同步语句块。这种同步机制是以线程为单位的,每个访问服务的方法都在线程上附带了一个给定的服务实例,这样线程就能看见相同服务实例,甚至是嵌套的方法调用。线程看不到不同的服务实例,除非它从开始进入的方法中完全退出。

组件提供了两个回调方法,用来激活和钝化服务实例,比如starting()和stopping()。当相关的某个组件的状态有变更时会通知组件来回调这些回调方法。在iPOJO中,组件状态要么是INVALID(比如:不是所有的组件约束都满足时),要么是VALID(比如:所有的组件约束都满足了)。在这个例子中,starting回调方法创建和启动了一个线程,stopping回调方法停止了这个线程。在这个组件的metadata(描述信息)会告诉iPOJO,当组件的状态变为VALID或者INVALID的时候,要回调starting或者stopping方法。

iPOJO描述文件命名为metadata.xml,包含如下内容:

上面的xml配置中,组件component元素有classname属性,用来标明这个组件的实现类是哪个;requires元素描述了这个组件的m_hello字段依赖了Hello服务;callback元素描述了当组件的状态改变时哪个方法会被回调;instance元素会让iPOJO创建一个组件的实例(注意:这里没有配置instance元素的name属性,iPOJO会自动给这个instance设置一个默认的name)。instance元素下面有子元素property,它配置了组件HelloClient的m_name字段的值为字符串“world”。

最后,pom.xml的配置如下:

在dependencies中有服务接口的依赖,因为HelloClient组件中依赖了这个接口Hello类。pom.xml配置完成后,即可执行下面的命令对服务消费者进行打包:

mvn clean install

如果打包成功,在target目录下回看一个jar包,这个jar包既可以当做普通的jar包,也可以当做bundle包,因为它与普通jar包唯一的不同就是MENIFEST.MF文件中多了一些OSGI的描述信息。

5. 运行实例

要运行这个实例,首先需要启动Felix。可以先下载Felix Framework Distribution,然后通过下面的命令来启动Felix:

java -jar bin/felix.jar

你可以在控制台中使用”ps”命令来检查已经安装的bundle:

-> ps
START LEVEL 1
ID State Level Name
[ 0] [Active ] [ 0] System Bundle (2.0.5)
[ 1] [Active ] [ 1] Apache Felix Bundle Repository (1.4.3)
[ 2] [Active ] [ 1] Apache Felix iPOJO (1.6.0)
[ 3] [Active ] [ 1] Apache Felix iPOJO Arch Command (1.6.0)
[ 4] [Active ] [ 1] Apache Felix Shell Service (1.4.2)
[ 5] [Active ] [ 1] Apache Felix Shell TUI (1.4.1)

接下来安装服务接口(hello.service)、服务提供者(hello.impl)、服务消费者(hello.client):

start file:../hello.service/target/hello.service-1.0.0.jar
start file:../hello.impl/target/hello.impl-1.0.0.jar
start file:../hello.client/target/hello.client-1.0.0.jar

以上的指令输入后,hello.service-1.0.0.jar的id为6,hello.impl-1.0.0.jar的id为7,hello.client-1.0.0.jar的id为8。在启动了服务提供者(hello.impl)后,服务消费者(hello.client)会被自动激活,由于服务消费者的instance实例在创建时就注入了m_name字段的值为“world”,因此控制台会循环输出“hello world”:

-> hello world
hello world
hello world
hello world

如果此时在控制台执行“stop 7”的指令,则服务消费者hello.client会自动钝化,并停止输出“hello world”,因为它依赖的服务实例对象(hello.impl的HelloService)变得不可用了。如果此时再重新“start 7“(或者重新安装部署实现了Hello接口的其他bundle),那么服务消费者又会变得可用。这两个步骤的效果如下:

-> stop 7
-> arch
Instance ArchCommand -> valid
Instance ipojo.example.hello.client.HelloClient-0 -> invalid
-> arch -instance ipojo.example.hello.client.HelloClient-0
instance name=”ipojo.example.hello.client.HelloClient-0″
 component.type=”ipojo.example.hello.client.HelloClient”
 state=”invalid” bundle=”8″
        object name=”ipojo.example.hello.client.HelloClient@137c60d”
        handler name=”org.apache.felix.ipojo.handlers.dependency.DependencyHandler” state=”invalid”
               requires aggregate=”true” optional=”false” state=”resolved” specification=”ipojo.example.hello.Hello”
        handler name=”org.apache.felix.ipojo.handlers.lifecycle.callback.LifecycleCallbackHandler” state=”valid”
        handler name=”org.apache.felix.ipojo.handlers.architecture.ArchitectureHandler” state=”valid”
-> start 7
hello world
-> arch
Instance ArchCommand -> valid
Instance ipojo.example.hello.client.HelloClient-0 -> valid
Instance HelloService -> valid
-> arch -instance ipojo.example.hello.client.HelloClient-0
instance name=”ipojo.example.hello.client.HelloClient-0″
 component.type=”ipojo.example.hello.client.HelloClient”
  state=”valid” bundle=”8″
        object name=”ipojo.example.hello.client.HelloClient@137c60d”
        handler name=”org.apache.felix.ipojo.handlers.dependency.DependencyHandler” state=”valid”
               requires aggregate=”true” optional=”false” state=”resolved” specification=”ipojo.example.hello.Hello”
                        uses service.id=”38″ instance.name=”HelloService”
        handler name=”org.apache.felix.ipojo.handlers.lifecycle.callback.LifecycleCallbackHandler” state=”valid”
        handler name=”org.apache.felix.ipojo.handlers.architecture.ArchitectureHandler” state=”valid”

总结

felix ipojo配置和spring的配置的目的差不多,都是为了解耦和简化依赖注入。但是felix ipojo比spring的配置要复杂一些,因为spring的bean都是创建后就不怎么变了,而且felix ipojo的instance可以随组件的任意时候的安装而产生,又随组件的任意时候卸载而悄悄的消失,所以ipojo的配置除了基本的spring功能外,主要是为了很好的解决组件中service来去匆匆的问题。

进入正题,felix ipojo配置主要是围绕着类、component和instance三个元素。以下是我对三者的理解:

  • 类:平时写的普通的ipojo类(暂时先不考虑ipojo annotation),类有field,有构造函数,有method,但是没有描述这些field会被怎么注入,注入何种依赖,也没有描述这个类中的一些方法是如何按照ipojo生命周期的流程有机的结合起来。它只是一个类而已。
  • component:一个类可以对应一至多个component(如果component是用annotation的方式来配置的话,那么一个类就只跟一个component对应了),component描述了一个类提供何种功能、其field将怎样被什么样的instance注入、其method何时被调用、在ipojo生命周期中调用顺序是怎样的。
  • instance:每个instance都是根据component产生的一个具体的实例对象,它和spring中的bean的含义很接近。一个component可以有多个不同的instance(spring中一个类也可以配置多个不同id的bean),由于component描述了一个instance的整体结构(甚至可以给field设置默认实现或者默认值),但是instance自己还是可以给field设置符合一定条件的instance。

在使用felix ipojo之前首先需要在maven的pom.xml里面添加ipojo插件配置:maven-ipojo-plugin使用指南,然后在ipojo配置文件目录或者metadata.xml文件中添加component和instance配置。

Reference

no manifiest section for signature file entry OSGI-OPT/src/org/osgi/service/device/DriverLocator.java

在使用maven-bundle-plugin在进行osgi bundle开发时,有时mvn install会碰到如下的报错:

错误:构造处理程序对象 no manifiest section for signature file entry OSGI-OPT/src/org/osgi/service/device/DriverLocator.java 时抛出异常

我碰到这个问百思不得其解,由于现在玩osgi的少,在google、stackoverflow上的资料偏少,很难找到解决办法,后来反复尝试,发现只要在pom配置时,保证配置的<Export-Package>其有一个真实存在包路径即可,不能写成*,否则就会报上面的错误;如果这个bundle确实没有package需要被export出去,那么就要写<Private-Package>,且里面要有具体的真实存在包路径或者包路径的通配符。

我用的maven-bundle-plugin是1.4.3版的,不知道后来的版本是否对这方面有改进。

还有类似原因的问题,比如在mvn install的时候出现如下的错误提示:

[WARNING] Warning building bundle ipojo.examples:hello.service:bundle:1.5.0-SNAPSHOT : Instructions for Export-Package that are never used: hello\.service\..*|hello\.service
[WARNING] Warning building bundle ipojo.examples:hello.service:bundle:1.5.0-SNAPSHOT : Superfluous export-package instructions: [hello.service.*]
[WARNING] Warning building bundle ipojo.examples:hello.service:bundle:1.5.0-SNAPSHOT : Did not find matching referal for *
[ERROR] Error building bundle ipojo.examples:hello.service:bundle:1.5.0-SNAPSHOT : The JAR is empty

这个解决方法,也和上面的相同,在maven-bundle-plugin中添加<Export-Package>或者<Private-Package>的配置即可。

 

源码解析之访问osgi felix bundle中的文件和资源

Apache Felix
Apache Felix

根据osgi规范中api的定义目前访问bundle里面的文件和资源的方法有三种。本文以felix框架为例,解析一下osgi中访问bundle里面的文件和资源的方法,以及各方法的区别。

一. Bundle.getEntry(String name)

该方法只能从当前的bundle jar中获得文件和资源(包括jar中的所有文件,比如*.class、META-INF\MANIFEST.MF等)。该方法的返回值为java.net.URL,在使用这个url的时候不能将其转成File类型来读取文件内容,因为这个url不是一个普通的文件操作系统中的url,而是osgi容器中的一个抽象的url,其文件读取操作必须交由创建这个url时设置的URLHandlersBundleStreamHandler来处理。所以如下的代码是不能获取到文件和资源的:

可以看到,从bundle中获取的entry的文件路径不是一个真实意义上的路径,它只是一个osgi容器才认识的url,要对这个url处理就需要用如下的方式,见示例代码:

接下来,从felix的源码中看看为何只能用下面的方式,这要从这个url是如何创建的来看了,在BundleImpl.getEntry(String name)方法中进行追踪,可以看到这个url是由BundleRevisionImpl.createURL(int port, String path)方法产生的,其代码如下:

在追踪下这个BundleStreamHandler,可以发现它是在Felix框架初始化的时候给默认设置的。如此就明了了。这个url对应的资源其实不能算是一个具体的文件,至少在Felix框架中不是,Equinox可能可以通过其他的方式来用文件方式来读取bundle中的资源,详见stackoverflow的一个question《No access to Bundle Resource/File (OSGi)》

二. Bundle.getResource(String name)

该方法的返回值也是URL类型的,其使用方法和Bundle.getEntry(String name)方法相同,也不能把url.getFile()当做文件名来创建File对象,只能用openConnection().getInputStream()来读取文件和资源的内容。

这个方法查找资源的方式和之前一篇博客《osgi类加载顺序源码解析》相似,在bundle没有解析时(即不处于resolved状态时),直接从当前bundle中查找资源;如果已经解析,就按照类加载的优先级,优先从其他bundle的classpath下查找这个文件或资源,如果有就返回;如果没有,才从本地bundle中查找并返回。(当然如果要共享某个bundle中的资源文件,就需要export这个资源文件所在的包,而且要使用这个资源的bundle也需要import这个包才行)。详见源码:

利用这一点,可以和getEntry方法搭配使用,达到游刃有余的控制资源共享的效果。

三. Bundle.getDataFile(String filename)

该方法是从felix cache的当前bundle的data文件夹下获取得文件和资源。这个data文件夹与felix cache的目录结构有关。默认情况下felix cache文件夹下会为每个bundle创建一个文件夹,文件夹的名称为bundle0、bundle1、bundle2。。。。bundleN,其中bundle0是系统bundle。每个bundle文件夹下又会为每个bundle的revision创建一个文件夹,文件夹名称的前缀为version(比如:version0.0),另外bundle文件夹下还有一个bundle.info的文件,描述了bunlde的信息(如果bundleId、被安装的bundle的jar文件路径、时间戳等)。如下图所示:

felix-cache目录结构
felix-cache目录结构

 

默认情况下,每个bundle目录下都没有data文件夹,getDataFile方法一旦被调用,就会在felix cache的当前bundle文件夹下,创建名为data的文件夹(如果没创建则创建之,如果已创建过,则跳过),该方法的参数是文件的相对路径,这个相对路径都是相对于当前bundle的data文件夹的;该方法的返回值是File类型,与上两个api不同,这个file是一个真实存在的文件,是可以通过file.toString()得到的url直接定位的。可以这样使用这个api:

这个方法相当于是提供了一个便捷的方式来存储和获取每个bundle下的数据。其源码其实最终的调用的BundleArchive类的getDataFile方法,源码如下:

 

综合使用上述的三个api就可以比较方便的读取bundle的资源了。另外还有getEntryPaths(String path)、findEntries(String path, String filePattern, boolean recurse)、getResources(String name)等一次性获取多个资源的api,还有便于读取menifest.mf文件的api:getHeaders()、getHeaders(String locale)等。

osgi类加载顺序源码解析

osgi容器中有三种classloader:各个bundle都有自己的classLoader,osgi容器也有一个框架级的classloader,这些classloader也不同于启动osgi容器的classloader(一般是app classloader)。如果需要加载一个类,那么就需要在这三个classloader之间挑出一个合适的classloader来加载类。本文将根据equinox和felix源码肤浅的描述一下osgi类加载顺序。

osgi规范中类加载顺序的定义

首先osgi类加载顺序不是开发osgi框架的人定的,而是osgi规范定的,但是每个框架的实现可能有所不同(规范详见osgi R4.3中的第3.9.4节 《Overall Search Order》):

在类和资源加载过程中,框架必须遵循以下规则。当请求一个bundle类加载器进行类加载或者资源的查找,查找必须按照以下顺序执行:

  1. 如果类或者资源是在包java.*中,那么交由父级类加载器代理完成,否则,搜索过程进入第二步。如果父类级类加载器加载失败,那么查找过程结束,加载失败。
  2. 如果类或者资源在启动代理序列(org.osgi.framework.bootdelegation)中定义,那么交由父级代理完成,此时的父级代理有启动参数org.osgi.framework.bundle.parent指定,默认是引导类加载器(bootstrap class loader),如果找到了类或者资源,那么查找过程结束。
  3. 如果类或者资源所在的包是在Import-Package中指定的,或者是在此之前通过动态导入加载的了,那么将请求转发到导出bundle的类加载器,否则搜索继续进行下一步;如果该包在启动参数org.osgi.framework.system.packages.extra中,则将请求转发给osgi容器外部的类加载器(通常是系统类加载器)。如果将请求交由导出类加载器代理,而类或者资源又没有找到,那么查找过程中止,同时请求失败。
  4. 如果包中类或者和资源所在的包由其他bundle通过是使用Require-Bundle从一个或多个其他bundle进行导入的了,那么请求交由其他那些bundle的类加载器完成,按照根据在bundle的manifest中指定的顺序进行查找进行查找。如果没有找到类或者资源,搜索继续进行。
  5. 使用bundle本身的内部bundle类路径查找完毕之后,。如果类或者资源还没有找到,搜索继续到下一步。
  6. 查找每一个附加的fragment的内部类路径,fragment的查找根据bundle ID顺序升序查找。如果没有找到类或者资源的,查找过程继续下一步。
  7. 如果包中类或者资源所在的包由bundle导出,或者包由bundle导入(使用Import-Package或者Require-Bundle),查找结束,即类或者资源没有找到。
  8. 否则,如果类或者资源所在的包是通过使用DynamicImport-Package进行导入,那么试图进行包的动态导入。导出者exporter必须符合包约束。如果找到了合适的导出者exporter,然后建立连接,以后的包导入就可以通过步骤三进行。如果连接建立失败,那么请求失败。
  9. 如果动态导入建立了,请求交由导出bundle的类加载器代理。如果代理查找失败,那么查找过程中止,请求失败。

下面的非标准流程图展示了查找的过程:

osgi类加载流程图
osgi类加载流程图

从规范可见,类加载的优先级顺序基本按照如下的原则:父容器classloader(通常是app classloader) –> 其他bundle的classloader –> 当前bundle的classloader –> 动态导入的包所在bundle的classloader。这个原则既可以使相同的类(包名也相同)尽可能只被加载一次,减少虚拟机perm区大小,也正因为如此,不同bundle中的相同的类,委托给同一个classloader加载,才能做到他们的对象和引用可以相互转换。(要知道一个类如果由不同的classloader加载后,其中一个classloader加载的类的对象是不能赋值给另一个classloader加载的类的引用的。)

osgi类加载顺序的源码解析

目前最流行的osgi框架实现有两个:equinoxfelix,下面分别列出equinox和felix在类加载顺序上的实现代码,如果大家在开发过程中发现一些类加载方面的奇怪现象,就可以通过找到如下的代码位置进行调试,从而跟踪出问题的根源,尤其是需要osgi容器内外共享类的时候。

equinox的osgi类加载顺序实现代码

在org.eclipse.osgi.internal.loader.BundleLoader.java的findClass(String name, boolean checkParent)和findClassInternal(String name, boolean checkParent, ClassLoader parentCL)方法中:

felix的osgi类加载顺序实现代码

在org.apache.felix.framework.BundleWiringImpl.java类的findClassOrResourceByDelegation(String name, boolean isClass) 方法中:

Reference

深入分析Java ClassLoader原理