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

 由  ValRay 发布

7.2.2. StartLevel Service

StartLevel Service 是 OSGI 规范中的核心服务,是 OSGI 框架必须实现的服务,通过 StartLevel Service 可以动态的设置和改变 Bundle 的启动顺序,OSGI 框架在启动 Bundle 时按照启动顺序和 Bundle ID 的倒序来启动系统。

在 OSGI 中,启动顺序称为 StartLevel,在之前的实战章节中,没有去控制 Bundle 的启动顺序,通过 config.ini 可以静态的设置系统的启动级别和各 Bundle 的启动级别,而通过在运行期获取 StartLevelService 则可动态的改变系统、各 Bundle 的启动级别以及新安装的 Bundle 的启动级别。

在未设置 OSGI 系统的启动级别时,系统将会启动所有的 Bundle,然后将系统的启动级别设置为当前启动的 Bundle 的高级别,在未设置默认的 Bundle 的级别时, OSGI 框架将采用默认设置 Bundle 的启动级别为 4。

7.2.2.1. 静态的设置系统和各 Bundle 的启动级别

在 equinox 中,通过在 config.ini 中设置 osgi.startLevel 的值来设置基于 OSGI 系统的启动级别,打开之前在实战章节中编写的 config.ini,其中没有 osgi.startLevel 属性的设置,在这种情况下 equinox 会将系统的启动级别默认的设置为 6,在 config.ini中设置了 osgi.bundles.defaultStartLevel 的值,这个属性是用来设置 bundle 的默认启 动级别的,在 config.ini 中并没有去设置各 bundle 的启动级别,这个时候就会采用 osgi.bundles.defaultStartLevel 作为 Bundle 的级别,根据这样的说法,之前的用户登 录验证模块的系统启动级别为 6,各 Bundle 的启动级别则为 4,由于系统的启动级别比各 Bundle 的启动级别高,所以在启动系统时所有的 Bundle 都启动了,可以试着把系统的级别改为比各 Bundle 的启动级别低来看看会造成什么样的效果,在config.ini 中增加一行这样的设置:

osgi.startLevel=3

这样系统的启动级别就被设置为 3 了,而各 Bundle 的启动级别则为 4,这个时候 再运行 run.bat 试试:

从上图中,可以看出在这种情况下除了 system bundle,其他所有的 bundle 都没有启动,这是由于 system bundle 的启动级别设置的为 0,并且是不可改变的,而其他 bundle 的启动级别比系统启动级别高,所以就没启动,这个时候再手动去启动bundle,看看会发生什么,输入 start 1,再输入 ss 查看下状态,会看到 id 为 1 的 state 仍然是 RESOLVED,这就说明了当 Bundle 启动级别比系统启动级别高时, Bundle 无论通过什么方法都是无法启动的,这个时候要启动 Bundle 的做法只能是去动态的改变系统的启动级别,使其等于或者大于 Bundle 启动级别,才可启动Bundle。

静态的设置各 Bundle 的启动级别有两种做法,一种就是通过设置 Bundle 的默认启动级别来改变所有未设置启动级别 Bundle 的启动级别,另外一种就是分别的设置 各 Bundle 的启动级别。

之前已经说过,可以通过设置 osgi.bundles.defaultStartLevel 的值来改变 Bundle 的默认启动级别,打开刚刚修改后的 config.ini,把其中的 osgi.bundles.defaultStartLevel 的值改为 2,再运行 run.bat,输入 ss 来查看系统中 Bundle 的状态,可以看到这个时候所有的 Bundle 的状态均为 ACTIVE,表明所有的 Bundle 的都启动了。

如果要单独的设置 Bundle 的启动级别,打开 config.ini,可以看到其中有这样的格式:
reference:file:bundles/ConfigFileValidatorBundle_1.0.0.jar@start 可以通过在@后增加启动级别来单独的设置该 Bundle 的启动级别:

reference\:file\:bundles/ConfigFileValidatorBundle_1.0.0.jar@4:start

再运行 run.bat,输入 ss 查看系统 bundle 的状态,可以看到 ConfigFileValidatorBundle 的 state 为 RESOLVED,而其他的 Bundle 由于采用的是默认的启动级别,state 则为 ACTIVE。

通过这样的方法可以静态的设置系统和各Bundle的启动级别,在启动系统时,OSGI 框架将按照各 Bundle 的启动级别从小到大的启动,如启动级别相同,则按照 Bundle ID 从小到大的启动,一直启动到系统的启动级别为止。

7.2.2.2. 动态的设置系统和各 Bundle 的启动级别

动态的设置系统和各Bundle的启动级别可通过在运行时调用StartLevelService来实现。

同样的,既然要使用 StartLevelService,先打开 MANIFEST.MF,在 import packages 中 import org.osgi.service.startlevel,通过 BundleContext 获取 StartLevelService:

ServiceReference serviceRef=bc.getServiceReference(StartLevel.class.getName()); 
StartLevel startLevelSrv=(StartLevel) bc.getService(serviceRef);

StartLevel 提供了管理系统和各 Bundle 启动级别的 API:

  • getBundleStartLevel(Bundle bundle)

获取 Bundle 的启动级别。

  • getInitialBundleStartLevel()

获取默认的 Bundle 的启动级别。

  • getStartLevel()

获取系统的启动级别。

  • setBundleStartLevel(Bundle bundle,int startLevel)

设置 Bundle 的启动级别,如设置的启动级别高于系统的启动级别,那么 OSGI 框架将会停止此 Bundle,如设置的启动级别低于系统的启动级别,如此时 Bundle 尚未启动,OSGI 框架则会启动此 Bundle,如已启动,OSGI 框架不会做任何动作。 - setInitialBundleStartLevel(int startLevel) 设置 Bundle的默认启动级别,对于新安装的Bundle将使用此默认启动级别。

  • setStartLevel(int startLevel)

设置系统的启动级别。 如设置的系统启动级别和目前系统的启动级别一致,则 OSGI 框架会发布FrameworkEvent.STARTLEVEL_CHANGED 的事件;

如设置的系统启动级别比目前的系统启动级别低,那么 OSGI 框架将现有的启动级别按 1 递减,并停止相应启动级别的 Bundle,直至设置的系统启动级别,当系统的启动级别和设置的启动级别一致时, OSGI 框架会发布FrameworkEvent.STARTLEVEL_CHANGED 的事件;

如设置的系统启动级别比目前的系统启动级别高,那么 OSGI 框架将现有的启动级别按 1 递增,并启动相应启动级别的 Bundle,直至设置的系统启动级别,当系统的启动级别和设置的启动级别一致时, OSGI 框架会发布FrameworkEvent.STARTLEVEL_CHANGED 的事件。

7.2.2.3. 适用场景

在 OSGI 规范中列举了 StartLevel Service 的适用场景:

  • 安全模式

安全模式可用于只启动系统所需要的 Bundles,就象 windows 的安全模式一样,可只启动必须的 Bundles,而其他不是必须的的 Bundles 则不启动。 可通过静态或动态的设置系统的启动级别来实现安全模式。。

  • Splash Screen

就像 Eclipse 一样,在启动前先出现一个等待的屏幕,等启动完毕后再进入系统界面,通过设置 Bundle 的启动级别,就可以实现这点了。

典型的实现方法就是写一个用于提供 Splash Screen 的 Bundle,将该 Bundle 的启动级别设置为 1,而其他的 Bundle 则设置比 1 高的启动级别,Splash Screen 的 Bundle 同时实现 FrameworkListener,当监听到 FrameworkEvent.STARTED 时,则表明系统已启动完毕,这时可以将 Splash Screen 关闭。

  • 处理不稳定的 Bundles

不稳定的 Bundles 是指那些依赖别的 Bundle 提供的 Service 才能正常启动的 Bundle,这其实是 OSGI 实践时要注意的一点,就是在启动时不要去静态的依赖其他的 Bundle,充分的发挥基于 OSGI 的动态性。

如果一定依赖其他的 Bundle,那么就可以通过设置 Bundle 的启动级别来避免不稳定的 Bundles 启动造成的错误。

  • 高优先级的 Bundles

系统有些时候会要求某些 Bundles 快速的启动,这个时候就可以将他们的启动 级别设置的低一点,使得在运行时先启动这些 Bundles。

7.2.3. Declarative Services

尽管 Declarative Services(DS)和别的 Standard Services(例如 Log Service、Http Service)放在同样的章节级别中,但 DS 绝对称的上是 OSGI R4 的重大改进,它对 OSGI 的 Core Framework 进行了完善和提升,在 OSGI Core Framework 对于系统采用的是 Module+Service 的开发方式,但按照分结构的开发方式而言,系统按照的是 Module 分解为 Component+Service,在 OSGI Core Framework 中,并没有明确的 Component 的概念,只能借助 BundleActivator 作为 Module 中的 Component,而其他的都作为 Service,尽管 OSGI Core Framework 中的 Service Layer 对于 Service-Oriented 的系统已经提供了完整的服务注册、查找、绑定和监听的定义,但对于 Service 的发布、使用而言并不是非常方便,DS 则提升了 Service 的发布和使用的简便性,同时更好的去实现 Service 的动态性,在 DS 上会看到 DI Container 的影子,不过 DS 除了具备 DI Container 的特征之外,还有一点更为根本的是保证了系统的动态性。

DS 提出了完整的 Service-Oriented Component Model(SOCM)概念,使得在 Bundle 中可以按照Component+Service的方式进行开发,来看看在DS的支撑下怎么去做。

7.2.3.1. Component 的概念和定义

Component 和 Service 从定义上来看是差不多的,任何一个普通的 Java 对象都可以通过在配置文件中定义成 Component,那么 Component 到底有什么作用呢:

  • 对外提供 Service;
  • 使用其他 Component 提供的 Service;
  • 交由 OSGI 框架管理生命周期。

只要具备上面三点之一的 Java 对象就可以定义为 Component,从而借助 DS 来简化的达到上面三点需求。

首先来看看怎么把一个普通的 Java 对象定义为 DS 中的 Component:

假设需要把 org.riawork.opendoc.osgi.DemoComponent 类定义为 DS 中的 Component,首先需要编写一个 xml 文件来声明,xml 文件格式类似如下:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="osgi.DemoComponent"> 
<implementation class="org.riawork.opendoc.osgi.DemoComponent"/></component>

把这个文件存为OSGI-INF/components.xml。

然后在MANIFEST.MF文件中引用这个文件就可以了:

Service-Component: OSGI-INF/components.xml

关于 Component 对外提供 Service 和使用其他 Component 提供的 Service 的部分在后续章节做详细讲解,在这里主要讲讲交由 OSGI 框架管理生命周期的 Component,在 DS 中对于 Component 分为三种类型:

  • Immediate

对于 Immediate Component,DS 会立刻激活这个 Component。 在这个 Component 的配置文件被改动、配置信息文件(properties 文件)被改动 以及所引用的 Service 被改动的情况下,DS 都会重新装载并激活这个 Component,同时对于引用了这个 Component 中 Service 的 Component 也会做 同样的动作。 - Delayed

对于 Delayed Component,DS 会根据配置文件中的 Service 的配置,注册 Service 的信息,但此时不会激活这个 Component。

直到这个 Component 被请求的时候 DS 才会去激活这个 Component,相应的设 置引用的 Service。

在这个 Component 引用的 Service 发生改变的情况下,DS 不会重新装载这个 Component,而会采用调用配置中设置的 bind、unbind 方法来重新设置引用的 Service。

  • Factory

Factory Component 和 Immediate Component 基本相同,不过它在激活后注册的只是一个 ComponentFactory 服务,只有在调用 ComponentFactory 的 newInstance 后才会激活里面的各个组件。

对于这三种类型的 Component 大家是不是会觉得和 Spring 中的 Bean 有所类似呢? 这三种类型的组件中无疑 Delayed Component 是需要的,为系统的动态性带来了很大的好处,同时也节省了内存的占用。

组件的生命周期受 bundle 生命周期影响,当 bundle 停止时 bundle 中所有组件也会随之停止,组件的生命周期可以通过 activate 和 deactivate 这两个方法来主动控制, activate 方法在组件被激活时被调用,deactivate 方法在组件被停止时调用,这两个方法也是可以不被实现的,同时,通过这两个方法可以获取到 ComponentContext,而通过它则可以获取到 BundleContext,通常来说这是非常有用的。

一个简单的 Component 示例如下:

public class LifecycleComponent{ 
 
 public void activate(ComponentContext context){ 
 // 进行组件的初始化工作 
} 
 
public void deactivate(ComponentContext context){ 
 // 进行组件被停止前的工作 
} 
 
}

7.2.3.2. Service 的发布

在DS中则不需要这么做了,在DS中只需要通过在之前component的配置文件中进行定义即可完成: 例如需要提供 org.riawork.opendoc.osgi.DemoComponent 中实现的 ValidatorService, 那么只需要修改之前编写的 xml 文件为如下格式:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="osgi.DemoComponent"> 
<implementation class="org.riawork.opendoc.osgi.DemoComponent"/> <service> 
        <provide interface=” org.riawork.opendoc.osgi.ValidatorService”/> 
</service> 
</component>

DemoComponent 示例如下:

public class DemoComponent implements ValidatorService{ 
 
 public boolean validator(String username,String userpass) throws Exception{  return true; 
} 
 
}

7.2.3.3. Service 的引用

在Service Layer中需要通过BundleContext来获取其他Bundle提供的Service,非常的 不方便,因为通常要使用Service的类并不是Bundle,这就导致了要在启动Bundle 时获取Service或在Bundle中提供对于BundleContext的静态获取,而在DS中则可以象DI Container一样,采用注入的方式来获取需要引用的Service。

例 如 org.riawork.opendoc.osgi.DemoComponent 需 要 使 用 HttpService , DemoComponent 这么写:
public class DemoComponent{ private HttpService service;
public void setHttpService(HttpService service){ this.service=service; }public void unsetHttpService(HttpService service){ if(this.service!=service) return; this.service=null; } }

配置文件修改如下:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="osgi.DemoComponent"> 
<implementation class="org.riawork.opendoc.osgi.DemoComponent"/> 
<reference name=”HTTPService” interface=”org.osgi.service.http.HttpService” bind=”setHttpService” unbind=”unsetHttpService”/> 
</component>

当 HttpService 可用时,DS 就会自动的将 HttpService 注入到 DemoComponent 中,如当 HttpService 不可用时,DS 就会自动调用 DemoComponent 的 unsetHttpService 方法。

7.2.3.4. Service 的引用策略

在Service Layer中提供了通过设置过滤属性来更加准确的获取说需要的Service,而在DS中则提供了更加完整的Service引用策略: - Cardinality

由于在 OSGI 框架中服务的提供通常都是提供接口的方式,那么就会产生系统中提供多个实现接口的服务的问题,例如在系统中可能同时有三个验证服务的实现,那么在引用服务时其实是可以同时引用这三个服务的,在 DS 中可以通过 Cardinality 的定义来只引用一个或多个服务。

Cardinality 有四种策略:

0..1 多只引用其中的一个服务,但系统也可以不存在可用的服务,象 Log 服务就很适合这种情况,即使系统中不存在 Log 服务组件应该也同样保持可用。

1..1 引用的服务必须至少有一个可用的(默认)。

0..n 引用 0 到多个可用的服务。

1..n 引用的服务必须存在,使用时可使用多个服务。
设置 Cardinality 策略只需要在之前的 service 引用的配置上增加 Cardinality 属 性的配置即可:
在这样的情况,假设系统中有三个可用的 HttpService,那么 setHttpService 方法就会被调用三次,如果不配置 cardinality 属性,那么系统将采用默认的 cardinality=”1..1”,在这种情况下,setHttpService 只会被调用一次,并且要求系统中必须至少有一个可用的 HttpService,否则 Component 将会启动不了。

  • 引用策略

引用策略是指在引用服务时可采用 static 和 dynamic 两种策略方式,static 策略是默认的方式,在 Component 启动后,如果引用的 service 发生变动时,整个 Component 会重启;而采用 dynamic 策略的情况下,在 Component 启动后,如果引用的 service 发生变动,并不会导致整个 Component 重启,而只会调用其中的 unbind 和 bind 方法。

要使用引用策略只需要在之前的 service 引用配置中增加 policy 属性的配置:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="osgi.DemoComponent"> 
<implementation class="org.riawork.opendoc.osgi.DemoComponent"/> 
<reference name=”HTTPService”  interface=”org.osgi.service.http.HttpService”  bind=”setHttpService”  unbind=”unsetHttpService” cardinality=”0..n” policy=”dynamic”/> </component>

- 属性过滤

在Service Layer中是通过填充属性到Dictionary对象中来实现根据属性过滤获 取服务,而在DS中同样可以通过在配置中定义需要过滤的属性:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="osgi.DemoComponent"> 
<implementation class="org.riawork.opendoc.osgi.DemoComponent"/> 
<reference name=”HTTPService”  interface=”org.osgi.service.http.HttpService”  bind=”setHttpService”  unbind=”unsetHttpService” cardinality=”0..n” policy=”dynamic” target=”(component.version=1.0)”/> 
</component>

那么这个属性是怎么配置到 component 中的呢?在 DS 中可以通过 property 和 properties 两种方式来配置 component 的属性:
1.0 true 可以看出 property 用于配置单个属性,在 property 中可以指定 property 值的类型,而 properties 则用于引用 bundle 中的配置文件。

7.2.3.5. Service 的状态监听

在Service Layer中是采用实现ServiceListener的方式来监听服务的状态的,而在DS 中则不需要,在引用的service发生变化时,DS会自动的通知相关的Component。

7.2.3.6. 基于 DS 重构用户登录验证模块

由于目前 DS 并没有融合到 OSGI R4 Core Framework 中,需要单独的下载 DS 实现 的 Bundle,先到 equinox 的下载网站下载 org.eclipse.equinox.ds_1.0.0.v20060601a.jar (类似这样名称的 jar 包),下载完毕后放入 eclipse 的 plugins 目录,启动 Eclipse。 在用户登录验证模块中用 DS 来改进服务的发布和引用:

  • 重构服务的发布

用 户 登 录 模 块 中 ConfigFileValidatorBundle 、 DBValidatorBundle 和 LDAPValidatorBundle 都是通过 BundleContext 的 registerService 方法来实现对 外发布服务的,在 DS 中则不需要这么做了,只需要将之前提供服务的类配置 为 Component,然后在配置文件中提供对外发布的服务即可。 以 ConfigFileValidatorBundle 为例:

第一步

由于不需要通过 BundleContext 来注册 Service 了,那么 BundleActivator 类就没有意义了,于是重构第一步就是把 BundleActivator 删了,删了后 同时删除 MANIFEST.MF 中 Bundle-Activator 属性;

第二步

在 ConfigFileValidatorBundle 工程下建立 OSGI-INF 目录,在该目录下建立 component.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="ConfigFileValidator"> 
<implementation class="org.riawork.demo.service.user.impl.ConfigFileValidatiorImpl"/> 
                <service> 
<provide interface="org.riawork.demo.service.user.Validator"/> 
                </service> 
</component>

通过这个文件将 ConfigFileValidatorImpl 定义成了 DS 中的Component , 同 时 对 外 提 供 了 接 口 为org.riawork.demo.service.user.Validator的服务。

第三步

打开 MANIFEST.MF 文件,在其中加入对于 component 配置文件的引入:

Service-Component: OSGI-INF/component.xml

按照同样的方式重构 DBValidatorBundle 和 LDAPValidatorBundle。

  • 重构服务的引用

    用户登录模块中只有 UserValidatorWebBundle 中的 LoginServlet 需要引用其他 Bundle 提供的服务,那么就只需要重构 UserValidatorBundle 即可。

第一步

之前是通过在 BundleActivator 监听 HttpService 来注册 LoginServlet,在 DS 中不需要通过 BundleActivator 来完成,那么第一步就是删除 BundleActivator 类,同时删除 MANIFEST.MF 中的 Bundle-Activator 属性。

第二步

LoginServlet 需要使用其他 Bundle 提供的 HttpService 和 Validator 服务,首先删除 LoginServlet 中的 LoginServlet(BundleContext context)的构造器,增加 setHttpService、unsetHttpService、setValidator 和 unsetValidator 方法,获取 HttpService 和 Validator 服务。

第三步

LoginServlet 激活的前提条件是系统中必须有一个可用的 HttpService 服务,而且 LoginServlet 只使用一个 HttpService 服务,至于 Validator 服务,即使没有系统也仍然要可运行,LoginServlet 至多也只需要一个 Validator 服务。

在 UserValidatorWebBundle 工程下建立 OSGI-INF 目录,新建 component.xml 文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?> 
<component name="LoginServlet"> 
<implementation class="org.riawork.demo.web.servlet.LoginServlet"/> 
<reference  name="ValidatorService" 
interface="org.riawork.demo.service.user.Validator" bind="setValidator" unbind="unsetValidator" policy="dynamic" cardinality="0..1"/> 
<reference  name="HttpService"  interface="org.osgi.service.http.HttpService" bind="setHttpService" unbind="unsetHttpService" policy="dynamic"/> 
</component>

unsetValidator方法是这么写的:

if(this.validator!=validator) 
                return; 
this.validator=null;

这么写的原因是当之前set的Validator服务不可用时,DS会自动的寻找新的 Validator 服务注册到 LoginServlet,之后再调用 unsetValidator 方法,这个时候其中的 validator 参数就是告诉 LoginServlet哪个validator服务已经不可用了。

重构完了服务的发布和引用后,就可以重新运行用户登录模块了,在运行用户登录 模块中加入org.eclipse.equinox.ds bundle,并将它的启动级别设置为1,让它比其他的Bundle先启动,配置完了后重新运行用户登录验证模块即可。

基于DS重构后的用户登录验证模块中的服务的注册、引用和状态的监听都变得非常容易了[ 目前DS的实现还处于测试阶段,equinox team认为使用DS可能会产生效率问题,同时不适合在多线程情 ],重构后的代码具体见附件DS目录下的Bundle工程。

查看评论