使用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