OpenDoc Series':OSGI实战(七):3

 由  ValRay 发布

7.2.4. Configuration Admin Service

7.2.4.1. 规范描述

Configuration Admin Service 是 OSGI 中非常重要的一个服务,它用于动态的管理 Bundle 的配置的属性,而这些属性的存储可能是在本地、也有可能是在远端、甚 至可能会在某些设备上,基于 Configuration Admin Service 就可以统一的、动态的、远程的完成 Bundle 配置属性的管理工作了。

规范中的这张图挺形象的解释了 Configuration Admin Service 的作用:

图表 8 Configuration Admin Service Overview(摘自OSGI R4)

Configuration Admin Service 的实现简单的去看可以认为它是一个事件订阅/通知模型,所以在使用 Configuration Admin Service 的时候也是基本可以按照事件订阅/通知模型的使用方法而开展,尽管实际上的 Configuration Admin Service 的实现并不 是如此的简单,要知道这个服务的目的是要实现远程的管理 Bundle 的属性配置的。 对于开发人员来说重要的仍然是了解基于 Configuration Admin Service 如何去实 现属性的管理和获取。

7.2.4.2. 跟踪属性的变化

假设 Bundle A 中有一个服务的接口是 org.riawork.opendoc.service.DemoCM,该 Service 需要使用的配置属性有 cm.port,那么怎么去跟踪这个属性值的变化呢?

为了获取配置的属性,需要了解 Configuration Admin Service 中的这么几个对象:

  • ManagedService

ManagedService 接口只有一个 updated 方法,从这个方法可以看出事件订阅/ 通知的模型,既然是这样,必然要将当前实现对外提供 ManagedService 的服务,同时做为需要告诉外部它订阅的是什么,这个就需要在注册时对此服务增 加 Constants.SERVICE_PID 属性的注册,这样 Configuration Admin Service 在 服务属性被改动的情况下就会相应的根据Constants.SERVICE.PID的值来通知相应的服务。

  • ManagedServiceFactory

ManagedServiceFactory 接口有三个方法需要实现,其功能和 ManageService 是 差不多的,区别在于 ManageService 只能订阅一个服务的属性的变化,而 ManagedServiceFactory 可以订阅多个服务的属性的变化,这个对于一个组件是 多个服务的实现的情况下就非常有用了。

通过实现 ManagedService 接口来完成上面的例子的要求:

private ServiceRegistration serviceReg=null; 
    private ServiceRegistration manSrvReg=null; 
public void start(BundleContext context) throws Exception { 
        DemoCM cm=new DemoCMImpl(); 
        Dictionary props=new Hashtable();           props.put("cm.port", "1220");           serviceReg=context.registerService(DemoCM.class.getName(),  cm, 
props); 
        Dictionary manProps=new Hashtable();        manProps.put(Constants.SERVICE.PID, DemoCM.class.getName()); 
         
 manSrvReg=context.registerService(ManagedService.class.getName(),  this, manProps); 
} 
public void stop(BundleContext context) throws Exception {          serviceReg.unregister();        manSrvReg.unregister(); 
    } 
 
public void updated(Dictionary props) throws ConfigurationException {           serviceReg.setProperties(props); 
    }

当DemoCM的服务的属性被改变后,就会相应的通知到DemoCM,在updated方法中可以根据需要做出处理。

7.2.4.3. 管理属性

管理属性包括对于属性的获取、增加、修改和删除,通过 Configuration Admin Service 可以较为容易的实现这些操作,例如要管理上面服务的属性:

ServiceReference serviceRef=context.getServiceReference(ConfigurationAdmin.class.getName()); 
ConfigurationAdmin admin=(ConfigurationAdmin)context.getService(serviceRef); 
Configuration config=admin.getConfiguration(DemoCM.class.getName());

获取到了config之后就可以通过config.getProperties来获取、增加、修改和删除其中的属性,在完成了所有操作后通过config.update()方法即可完成对于相应服务的通知。

7.2.4.4. 其他作用

Configuration Admin Service 的管理配置属性的方法还可以被用于动态的调整系统 行为方面,如用户登录验证模块在获取验证服务时的方法,就可以通过 Configuration Admin Service 来改变为动态的改变的方式,只需要将获取验证服务 时的属性做为可配置的,在系统运行期就可以通过动态的修改这个属性值来切换对于验证服务的获取,同样的,这样的方法也可以被用于升级接口 API、接口的实现等,这对于动态的改变系统行为是具备很重要的意义的。

不过到本文编写完毕之时 Equinox 还没有实现 Configuration Admin Service,如果要试用的话可以从 equinox 的 cvs 中下载。

7.2.5. Event Admin Service

7.2.5.1. 规范描述

OSGI 提供了 Event Admin Service 以方便开发人员实现自定义的事件发布和事件处理,和 Java 的事件处理模型基本是一致的。

图表 9 Event Admin Service Architecture(摘自OSGI R4)

事件发布者通过调用 Event Admin Service 的 sendEvent 或 postEvent 方法对外发布事件,事件订阅者则通过实现 Event Handler 并注册为 EventHandler 的服务来订阅感兴趣的事件。

Event Admin Service 从规范上来说比较的简单,在这里还是以如何使用它来进行介绍。

7.2.5.2. 发布自定义的事件

对于发布自定义的事件,需要接触的主要是这么两个对象:

  • EventAdmin

EventAdmin 提供了 sendEvent 和 postEvent 两个方法供外部调用以发布事件,两个方法的区别在于 sendEvent 是同步调用,postEvent 是异步调用。

  • Event

Event 的构造器有两个参数,一个是 String 类型的 Event 的 Topic,也就是事件的主题了,就像 BUNDLE_STARTED 这种事件,在规范中建议 Topic 采用包名 / 类名 / 事件主题名这样的方式来构成 Topic ,就像这样:org/riawork/opendoc/DemoEventPublisher/RIAWORKEVENT;第二个参数是 Event 的属性,其实也可以看出是用于传递 Event 的参数的,这样可以方便事件订阅者通过获取属性值来处理事件。

实际应用的一个简单的例子如下:

EventAdmin 
eventAdmin=(EventAdmin)context.getService(context.getServiceReference(EventA dmin.class.getName())); 
    eventAdmin.postEvent(new    Event(“org/riawork/RIAWORKEVENT”,new 
Hashtable());

在本文附带的代码的 EventAdmin 目录下可以找到更为详细的代码,在使用前请从 Equinox 的网站下载 EventAdmin 的实现并放入 Eclipse 的 plugins 目录中。

7.2.5.3. 处理自定义的事件

对于处理自定义的事件,需要涉及的对象同样有三个:

  • EventHandler

EventHandler 接口中有 handleEvent(Event event)方法,通过完成此方法可完成 事件的处理,如何订阅自己感兴趣的事件呢,通过在注册 EventHandler 的实 现为 EventHandler 服务时通过属性进行控制。

  • EventConstants EventConstants 中有 EVENTTOPIC 属性和 EVENTFILTER 是常用的,EVENTTOPIC 用于控制订阅的事件的主题名,主题名的值中可以采用通配符,如 org/riawork/,但不支持这样的格式 org/riawork/RIA*;EVENTFITLER用于过滤订阅的事件的属性。

  • Event

Event 通过 event.getProperties 获取 Event 的属性值,以进行事件的处理。

一个简单的事件处理器的例子如下:

public class Activator implements BundleActivator,EventHandler { 
 
private static final String[] topics=new String[]{"org/riawork/*"}; 
        private ServiceRegistration serviceReg=null; 
        public void start(BundleContext context) throws Exception {             Dictionary props=new Hashtable();               props.put(EventConstants.EVENT_TOPIC, topics); 
serviceReg=context.registerService(EventHandler.class.getName(), this, props);  }       public void handleEvent(Event event) { 
            System.out.println(event.getTopic()); 
 } 
  
}

OSGI 规范中还定义了非常多值得关注的 Service 以支撑基于 OSGI 的应用,例如 Http Service、Log Service、Preferences Service、Metatype Service、XML Parser Service 等等,这些 Service 的详细介绍请大家参见《OSGI Service Platform Release 4》。

7.3. OSGI 关键部分讲解

7.3.1. ClassLoader

在 Java 中 ClassLoader 是非常重要的概念,而大家也知道,JVM 本身在 ClassLoader 上并没有提供非常强的功能,象对于模块开发非常重要的模块隔离 ClassLoader 的机制,对于版本升级非常重要的版本加载机制等,当然,基于 JVM 现有的 ClassLoader 可以来实现这些,OSGI 基于 JVM ClassLoader 形成模块隔离 ClassLoader 的机制,同时也增强了 ClassLoader 按版本加载、属性过滤等多种功能。

OSGI 的系统的 ClassLoader,OSGI 规范中有张图可以解释:

图表 10 OSGI ClassLoader Delegation Model

在 OSGI 中,ClassLoader 可以通过下面几个区域来加载类和资源: - Boot Class path

Boot Class path 中包含了 java.*以及其实现的包。

  • Framework Class path

框架通常会为其实现的类建立一个单独的 ClassLoader。

  • Bundle Space

Bundle Space 则包含了与这个 Bundle 有关的所有的 jar 文件。 Class space 是指通过一个给定的 Bundle 的 Classloader 可以获取的类,对于一个 Bundle 而言,它的 ClassLoader 能加载的类包括:

  • Parent Classloader 加载的类(在 OSGI 的实现中 Bundle ClassLoader 的 Parent ClassLoader 通常都是 Boot ClassLoader,这里能加载的类通常是 java.*);
  • 当前 Bundle Imported 的 packages;
  • 当前 Bundle Required 的 Bundles 的类;
  • Bundle 自己的 Classpath 中的类;
  • 附加的其他的 Bundle。

关于OSGI在对一个Bundle进行类加载的顺序请见图表 7 OSGI Class loading流程图。

按照规范的这种翻译式的讲解可能会很晦涩、难懂,在下面还是以基于 OSGI 构建系统时会经常碰到的一些 ClassLoader 的问题来进行实际性质的讲解,可能会让大家更加容易去理解 OSGI 的 ClassLoader 机制,在讲解问题之前还是先解释下ClassNotFoundException 和 NoClassDefError 两个异常,这也是在 ClassLoader 加载类出现问题时常常碰到的两个异常,这两个异常的区别在于前者ClassNotFoundException 是指通过 ClassLoader 加载不到所需要的类,而后者 NoClassDefError 是指通过 ClassLoader 已经找到了所需要的类,但找不到该类所依赖的其他的类,明白了这两种异常后来看实际使用 OSGI 搭建系统时常碰到的ClassLoader 问题:

  • 在 Bundle A 的 Build Path 中引用了 lib 的 xstream.jar,但在运行时当加载到 XStream 类时报 ClassNotFoundException

这是为什么呢,参照图表 7 OSGI Class loading流程图,可以看出Bundle的 ClassLoader在加载类时是通过MANIFEST.MF中的bundle-classpath属性的描述来加载Bundle私有的类的,而上面这个问题就出在这了,xstream.jar是bundle私有引用的 jar ,那么就需要打开 MANIFEST.MF 文件,把它加入到 Bundle-Classpath属性中,这其实是个习惯的问题,因为在通常的project的开发中我们都习惯把引用的lib加入到classpath,可能新手的话就会忘了同步 MANIFEST.MF的改动,在Eclipse V3.2 的版本中在MANIFEST.MF中指定 Bundle-Classpath属性的内容可自动同步到Build Path中去,以后要给Bundle引 新的lib的时候可以直接到MANIFEST.MF中去改,然后选择自动同步就可以了。

在实际使用的过程中还有一个要养成的习惯就是如果要引用其他 Bundle 的类的时候,不要直接的引用那个 Bundle 的工程,而是通过 MANIFEST.MF 的 Import-Package 属性导入所需要的类的包。

  • Bundle A 是个小的 Web 应用,使用了 Spring 完成业务逻辑处理的部分,但在运行过程当Spring加载配置在xml中的bean时报出了ClassNotFoundException 这个是在基于 OSGI 搭建 B/S 应用时与一些开源产品集成时会碰到的典型问题,这个问题不完全算 OSGI ClassLoader 的问题,但因为是基于 OSGI 搭建 B/S 应用才会碰到的,所以还是放在这里来讲讲了。

会出现这个问题的原因是 Spring 在加载 xml 中 bean 的类时采用的是获取当前线程ClassLoader的方法,而由于Bundle A是个Web应用,当在页面请求Bundle A 调用 bean 的时候,HttpService 的实现是要启动线程监听的,这个时候当前线程的 ClassLoader 就变成 HttpService 实现的那个 Bundle 的 ClassLoader,这个时候 spring 再用当前线程的 ClassLoader 去加载类,自然就会找不到了,所 以就抛出 ClassNotFoundException 了。

这个问题的解决方案有两种,一种是抛弃 Spring,如果你只是使用 Spring IoC 的话那么就可以选择这种,因为 OSGI R4 的 DS 本来就具备 IoC 的特性;另外一种是修改 Spring 的 ClassUtils 类,将其使用当前线程 ClassLoader 加载的方法改为使用当前类的 ClassLoader 进行加载。

这个问题在使用 Axis 的时候也会碰到,同样的做法,修改它的 ClassUtils 类。 其他的Bundle的ClassLoader的问题应该会碰得很少,碰到时只要参照图表 7 OSGI Class loading流程图看看就差不多了,记住OSGI对于每个Bundle采用的是独立的ClassLoader机制,同时Bundle可以通过象Import-Package、Require-Bundle的方式引用其他Bundle Export的Package就可以了。

7.3.2. Bundle 的生命周期

具体见Lifecycle Layer。

7.3.3. Bundle 的通讯机制

现在大家都知道,OSGI 对于每个 Bundle 采用的是独立的 ClassLoader的,那么 Bundle 之间是怎么样通讯的,在之前解释 OSGI R4 规范的 Module Layer 中也有部分的介绍,在这里来结合个人的实际使用来进行更为形象点的解释,Bundle 的通讯机制在 OSGI Component Programming 这份 PPT 中有个不错的图进行了解释:

图表 11 OSGI Bundle通讯机制(摘自OSGI Component Programming PPT)

从这张图中可以很清楚的看出 Bundle 之间通过 Service 和 Packages 两种方式来进行 通讯,Packages 方式是通过定义 Bundle 的 Import-Package 和 Export-Package 来实现, Packages 方式看起来更适合设计时的定义 Bundle 的依赖,当然,也可使用类如 DynamicImport-Package 的方式来实现动态的引用,Packages 的机制是的引用其他 Bundle 中的类成为了可能,那么 Service 机制呢,通过 Service 机制可以实现引用其他 Bundle 中提供的类的实例,而 Service 机制同时还保证了基于面向接口的方式能够更加的简单的去完成,举例来说:

  • Bundle A 需要使用实现了 org.riawork.demo.Opendoc 接口的类,在 OSGI 中一 种典型的做法就是:

将 org.riawork.demo.Opendoc 做为一个单独的 Bundle ,并 Export 中 org.riawork.demo 包,Bundle A 引用 org.riawork.demo 包,采用如下的方式调 用实现了接口的实例:
Opendoc service=(Opendoc)context.getService(context.getServiceReference(Opendoc.class.g etName())); 可以看到在这样的方式中,Bundle A 和实现 Opendoc 接口的类的 Bundle 完全没有发生任何的关系,真正的面向接口的编程得以实现。

而如果不采用 Service 的方式的话,只能让实现了 Opendoc 接口的类的 Bundle Export 出实现类的 package,然后 Bundle A Import 这个 package,通过类似 Opendoc opendoc=new OpendocImpl();的方式来获取 Opendoc 的实现,这样的方式还使得接口实现的依赖在程序中已经确定,我们知道这是 IoC 反对的重点,而在 OSGI 中使用 service 的话不仅不会造成这种现象,而且还能做到实现了此接口的类的装载是动态的,不像很多传统的 IoC 容器是设计期就确定的。

可以看出,在OSGI中要实现Bundle之间的通讯并不是什么难事,而且正因为Bundle 的通讯机制是动态的,从这方面 OSGI 保证了基于其构建的系统可在运行期动态的改变行为,而在 OSGI 框架具备了 DS 的实现后 Bundle 间的通讯就更为容易去实现了。

7.3.4. DS 中 Component 的生命周期

DS 中的 Component 的生命周期是如何被控制的,尽管对于 Component 的控制 DS 是提供了接口可以通过程序来实现控制的,但在实际的系统中基本都是交由 OSGI 框架去控制,而很少通过程序主动去控制,仍然举个例子来看看 DS 中 Component 的生命周期:

  • Component Opendoc 是 Bundle A 中的一个 Component;
  • 当 Bundle A 启动时,Component Opendoc 的配置文件的信息被加载并解析, 此时 Component Opendoc 的状态设置为 Enabled;
  • 之后根据 Component Opendoc 的配置寻找引用的服务,如引用的服务的条件均满足了,那么此时 OSGI 将此 Component 的状态设置为 Statisfied;如引用的服务的条件未满足,此时 OSGI 则将此 Component 的状态设置为 Unstatisfied;
  • 当 Component Opendoc 被调用时,如 Component 的状态为 Statisfied,那么 OSGI 将激活此 Component,首先设置 Component Opendoc 的依赖,如 Component Opendoc 中存在 activate(ComponentContext context)方法,则调用此方法。
  • 当 Bundle 停止时 OSGI 将通知引用了 Component Opendoc 提供的服务的 Component,如那些 Component 必须引用 Component Opendoc 提供的服务,那 么 OSGI 将调用该 Component 的 deactivate(ComponentContext context)(如果存在)方法,之后把该 Component 的状态设置为 Unsatisfied,在完成了这些工作 后,OSGI 将调用 Component Opendoc 的 deactivate 方法,之后将 Component Opendoc 的状态设置为 Disabled。

从上面的描述可见,Component 的生命周期完全是动态的,也因为这个原因在使用 Component 时当发现 Component 并没被激活也不是什么很奇怪的事,这个时候就可以去查查 Bundle 是否启动了,Component 所必须的依赖在系统中是否可用,由于 Component 的变化完全是动态的,所以依靠跟踪去判断错误比较困难,大多数时候能采取的方法就是通过启动 Equinox 对于 LogService 的实现,然后在控制台中输入 log 来查看 ds 的日志;另外的方法就只能是推导原因了。

7.3.5. DS 中 Component 的通讯机制

DS 中 Component 的通讯通过服务的方式来完成,所以 DS 称自己为 Component-Oriented Service Model,关于DS中Service的详细描述请见Declarative Services中Service的相关章节。

查看评论