1. 背景介绍
OSGi称做Java语言的动态模块系统,它为模块化应用的开发定义了一个基础架构。在产品设计中,OSGI是一种规范或者是一种基础结构,以OSGI的形式设计出产品的工作单元。在企业实际的应用中,也会采用spring DM框架来开发OSGI程序,通过maven管理OSGI组件的结构,并且也有多样的设计模式。
本文在介绍OSGI,maven,spring DM的基础上,引入pax-exam对组件进行代码级自动化case的实施方案,并且就其中的优点与不足展开讨论
2. 基础知识
2.1 OSGI
2.1.1 什么是OSGI
OSGi(JSR 291)亦称做Java语言的动态模块系统,它为模块化应用的开发定义了一个基础架构。简单的说,通过OSGi可以在后台对设备组件进行安装、升级或卸载而无需打断该设备的正常运行.
OSGi规范的核心组件是OSGi框架。这个框架为bundle提供了一个标准环境。整个框架可以划分为一些层次:
L0:运行环境——是Java环境的规范
L1:模块——模块层定义类的装载策略,OSGi模块层为一个模块增加了私有类同时有可控模块间链接
L2:生命周期管理——增加了能够被动态安装、开启、关闭、更新和卸载的bundles
L3:服务注册——服务注册提供了一个面向bundles的考虑到动态性的协作模型。
2.1.2 OSGI的特点
1. 模块化
在实际产品设计中,各种模块化设计各有特色,OSGI针对模块化提出一个标准,期望能形成一个市场规范。模块化的具体体现就是bundle的形式,bundle是osgi的部署单元,以jar包的形式封装业务逻辑,对外提供可使用的类,在代码复用、重构方面更有优势。
2. 动态性
bundle的状态有installed,resolved,active三种。这几个状态的含义可理解为:install是bundle能被正确编译,编译完毕之后被OSGI容器解析正常后为resolved,在容器内启动后为active状态,如下图:
Bundle的启动、卸载可以在服务运行时进行,这便是它的动态性。
2.1.3 OSGI的classloader
Osgi容器为bundle创建classloader,每一个bundle都有独立的classloader,class loader之间形成一个依赖关系图:
在实际运行环境中,Bundle 的 Class Loader 根据如下规则去搜索类资源。规则简要介绍如下:
1. 如类资源属于 java.* 包,则将加载请求委托给父加载器;
2. 如类资源定义在 OSGi 框架中启动委托列表(org.osgi.framework.bootdelegation)中,则将加载请求委托给父加载器;
3. 如类资源属于在 Import-Package 中定义的包,则框架通过 Class Loader 依赖关系图找到导出此包的 Bundle 的 Class Loader,并将加载请求委托给此 Class Loader ;
4. 如类资源属于在 Require-Bundle 中定义的 Bundle,则框架通过 Class Loader 依赖关系图找到此 Bundle 的 Class Loader,将加载请求委托给此 Class Loader ;
5. Bundle 搜索自己的类资源 ( 包括 Bundle-Classpath 里面定义的类路径和属于 Bundle 的 Fragment 的类资源);
6. 若类在 DynamicImport-Package 中定义,则开始尝试在运行环境中寻找符合条件的 Bundle 。
2.2 Spring DM
Spring DM即Spring动态模型,允许开发者构建Spring应用程序,这种应用程序能够在osgi容器内进行部署。可以理解为spring与osgi的结合,带来的优点是:
1. 使用spring框架在模块之间实例化、配置、集成组件
2. 开发者使用熟悉的编程模型开发osgi平台程序,使编写osgi程序变得容易一些
如上图解释下spring dm这个框架的一些功能特性:
1. spring extender bundle:负责实例化业务组件的application context,并且这个过程是异步的,也就是说bundle可以先全部被置为active状态,但是实例化application context(即实例化配置文件中定义好的bean,导出可用service)这个过程是异步的,bundle的状态和application context是否实例化之间没有直接关联
2. application context:对于传统的spring框架开发的程序就一个上下文文件,spring容器通过该文件配置的bean进行实例化,对于osgi的程序,每一个bundle可以有自己的application context,声明bundle本身的导入和导出的service
3. 注册service:如下是application context中配置的bean与service信息,bean被实例化之后其中的接口被注册,被注册的service可以为其他bundle所访问。
2.3 Maven
Maven是一个项目管理工具,它包含了一个项目对象模型 (Project Object Model),一组标准集合,一个项目生命周期(Project Lifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。使用Maven的时候,用一个明确定义的项目对象模型来描述项目,然后Maven可以应用横切的逻辑,这些逻辑来自一组共享的(或者自定义的)插件。
Maven的功能与Ant类似,管理项目而用,对于osgi项目,组件众多,依赖关系众多,通过mave的组织管理方式,能方便的处理这些问题
2.4 Pax-exam
说到测试框架,spring DM自带了一个集成junit的测试框架spring-dm test,在实际使用中,因为需要tester也要定义一大串的依赖关系,并且对组件的启动顺序有要求,提高了环境调试的工作量,转而调研了pax-exam这个框架。
Pax-exam是用来测试osgi框架和应用程序的工具。它负责的自动化case执行顺序大致如下:
1. 启动一个OSGi的容器,比如equinox,flex,这是基础,因为所有的OSGI组件都要生存在一个OSGI容器之上
2. 启动需要用到的所有bundle,包括被测bundle,以及被测bundle依赖的bundles
3. 动态的将test代码project创建为一个bundle
4. 把test bundle发布到已经启动了的OSGI容器内
5. 执行test函数
Pax-exam由多个组件组成,如下图:
3 测试方案
3.1 测试开发环境
3.1.1 JDK
JDK:1.5以上,JDK需要配置环境变量
3.1.2 Eclipse
Eclipse:笔者使用的版本3.5.1,所需安装插件有:svn,maven(详见下方)
3.1.3 Maven
Maven:2.2.1,安装与使用如下说明:
1. Maven的eclipse插件配置:
第一:下载apache-maven-2.2.1-bin.zip
第二:在eclipse的window-preferences-maven-installation中add maven2.2.1如下并选择:
第三:maven本地仓库配置,在eclipse的window-preferences-maven-user settings中配置如下
Settings.xml中的两处配置注意为:
一个是本地仓库的路径
<localRepository>E:/maven_eclipse/maven-repository</localRepository>
另一个是线上仓库的地址
<url>http://maven.scm.baidu.com:8081/nexus/content/groups/public</url>
2. Maven的环境变量配置
想要在eclipse中执行junit的函数,需要从环境变量得到maven程序的路径,配置如下
然后在Path环境变量中,增加%M2_HOME%\bin;如下:
3.1.4 Pax-exam
Pax-exam:2.0.0-RC4
测试代码的pom.xml中需要配置以下对pax-exam的依赖关系
3.2 测试思路
3.2.1 代码关系与管理
如2.4中提到的,osgi组件的代码级case,思路都是把测试代码发布为bundle,由osgi框架触发执行测试bundle中的test函数,而test函数的逻辑就是对被测bundle的接口进行调用,如下图所示:
那被测代码、测试代码、被测和测试代码之间究竟是怎么样的组织关系?因为传统的方式是import jar包到build path下,使用maven是在pom.xml中定义对jar包的依赖。为了更好的说明maven组织管理bundle之间的依赖关系,可参考下图示例:
说明:
1. 左侧的parent,sub2,sub2/sub1是被测代码结构的示例,这是一种继承结构,parent也是maven模块,类似于面向对象语言中的父类。Parent一般定义了工程需要依赖的第三方的bundle,那么sub2以及sub2的子模块直接从parent继承下来这些第三方的依赖关系。
2. 本地仓库的作用是从线上仓库download bundle下来,开发和执行case时都是到本地仓库去寻找bundle的。那么在定义parent的依赖组件关系后,编译parent时maven就会执行download的过程
3. 被测代码通过mvn install的命令在开发环境下编译,编译后的jar包会存放至本地仓库;如果被测代码已经成熟,则可以直接发布到线上仓库
4. 测试代码的结构可以简单的就是一个maven project或者仿照被测代码的集成结构
5. 测试代码所依赖的第三方组件,被测代码的组件,都在本地仓库
3.2.2 Pax-exam junit用法
Pax-exam的几个主要的功能是:将所有的bundle解析、加载、启动,执行单测case。下面以一个单测用例的例子做说明。
第一句@RunWith通过注解使得该class被Junit4TestRunner来驱动执行,Junit4TestRunner在2.4节中提到过,是封装了Juit4的TestRunner。做的事情就是在组件容器内找到TestClass和TestMethod,并且通过反射调用执行单测函数。
第二句的注解是pax-exam2才新升级的功能,2.4提到过pax-exam执行test case的流程,最开始就是要启动测试容器,然后启动所有的bundle,这个注解有两个class可选,分别为:
AllConfinedStagedReactorFactory.class
这个类使得test case执行时,每一个@Test函数都要启动一次容器,并且启动所有的bundle,开销较大。一般不建议这么做。
EagerSingleStagedReactorFactory.class
这个类是让每一个test class初始化时启动容器,并且启动所有bundle,class中的所有@Test函数就在这已经启动好的容器内执行了。这样减少了启动容器的次数,减少开销。
第三句@Configuration()这个注解是pax-exam本身所需设置一些配置项,包括了配置需要启动哪些bundle,所以这个是重点。以上述代码片段为例说明,都需要配置哪些内容:
mavenBundle().groupId("javax.servlet").artifactId("com.springsource.javax.servlet").version("2.5.0"),
这一句比较有代表性,用来配置pax-exam测试执行时所需启动的bundle信息。被测代码需要启动什么bundle,pax-exam测试时就需要在这里进行配置,否则容器启动就会失败。
@Test注解就跟Junit4一样了,标明函数有Junit4来执行。其代码逻辑中,如何调用被测的接口呢?
还得接2.2节来说,采用spring-dm开发,bundle之间互相通过接口来通信,那么接口就是通过spring来实例化的。BundleContext这个对象,能够获得组件容器内可访问的接口,因此调用被测接口时一般为:
XService x = bundleContext.getServiceReference(XService.class)
这里XService就是某个bundle在spring的applicationcontext文件中配置的bean,实例化之后,通过bundleContext就可以得到。
3.2.3 测试执行
有两种方法来执行pax-exam的junit case。
第一是在开发环境下,使用eclipse的junit4和maven插件来执行。在test class上右键run as Junit test,当然要这么执行需要关注3.1.3中maven的环境变量配置,因为这么执行需要告诉junit,那些依赖的bundle去哪里能拿到。
第二是在命令行环境下,使用maven的命令:mvn test执行。在2.3节中提到过,maven定义了一些插件的目标,所谓的插件就是具有特定功能的程序,能在maven中配置使用,比如要执行mvn test,需要有一个maven插件maven-surefire-plugin,在测试代码的pom.xml中配置为:
如上,在pom中以plugins标签来配置maven-surefire-plugin,在configuration标签中有几项需要说明下:
Parallel和threadCount配合使用,可用以多线程执行单测case,不过对于osgi组件级的测试,多线程的方式会明显加大开销,不建议这么做。如果是非osgi的程序,可以通过多线程方式加快单测执行时间。
forkMode可以是once,pertest,与agrline配合起来可以为case执行时启动jvm传入参数,forkMode的含义是jvm初始化的机制,分别为:只初始化一次,每一个case初始化一次。
Mvn test执行完毕的report就是junit的结果report,为xml格式。
3.2.4 代码调试
在单测实施中,如果抛出的异常还不够看出原因所在,可通过代码调试来跟踪执行情况,从而定位问题。
Osgi组件,因为每一个组件都有自己的classloader,测试代码是一个组件,都是组件的话并不能直接调试,只能采用远程调试的方式。如下代码意思是在整个bundle容器的jvm启动后,开放一个监听端口以便远程调试:
new VMOption( "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" ),
new TimeoutOption(0),
在eclipse的debug configuration中新增一个remote java application,配置上面的地址和端口。设置断点之后执行单测case,就如同本地调试一样,可单步跟踪。
3.2.5 覆盖率
考虑到被测程序架构是maven+osgi,覆盖率可选的工具有多种:maven-emma-plugin,jacoco。
Jacoco采用的是动态插桩方式,不会额外的输出一份插桩后的class,而是在jvm中载入class的时候做的插桩,它的使用过程如下:
4 持续集成方案
进行持续集成之前,需要考虑到,程序架构为maven+osgi+pax-exam junit case+jacoco,各个环节都有自己的特别之处,先以一个流程图说明下,在如此架构下ci的执行过程:
5 小结
Osgi组件的架构,初衷是引入模块化的设计理念,就好比搭积木一样,一块一块的垒起来。不过也带来一些问题就是框架重量级,初期开发难度大。随之而来的自动化测试或者单元测试也面临同样的问题。
Pax-exam是一种osgi组件自动化测试的框架,上文都是基于这个框架设计的方案,它有哪些优势和不足呢?
优势:比较于spring-dm自带的test框架,pax-exam不需要依赖一个庞大的pom.xml文件,而是自己配置,并且能自动理清各种bundle的前后依赖和启动顺序,这一点很重要,否则N多个bundle如何启动都是个问题。
不足:开销较大,主要体现为执行测试时,需要把要依赖的bundle都配置并且启动,这个过程是比较花时间的,比如启动容器和组件需要7s,而执行case本身都是毫秒级。
后续的计划:pax-exam在实际的工作中带来一些便利,但是为了减少开销,也为了提高编写单测case的易用性,后续的方向会向spring容器偏移,spring容器与组件容器相比,确实是轻量级的。
作者:xdwei