当今软件系统复杂性不断增加并且对动态性提出了更高的要求,大型软件项目通常采用构件模型和构件化技术提高系统的可扩展性、易理解性和可重用性。 Java 技术通过 JAR 文件、包和命名空间以及类加载机制对基于构件模型的构件化设计开发提供支持,但存在着缺乏动态性、依赖声明、版本控制和信息隐藏等问题 [1] 。面向服务构件模型将服务计算引入到构件模型中,为构件模型提供了动态性,由此可见构建面向服务基于构件的系统是非常必要的。同时, OSGi 是 轻量级的面向服务基于构件的框架,用来部署和执行面向服务构件化的应用,并提供了服务注册和服务获取的机制。首先,提高动态性,当构件需要更新和重新部署 时无需重新启动系统,真正体现了热插拔和即插即用的特点。其次,提供对其他构件和资源依赖的显式声明,使构件划分清晰。再次,有效的进行版本控制,支持同 一系统中多个不同版本包被同时加载。最后,实现了信息隐藏,只对外部提供必要的调用接口,隐藏其他不必要的信息。因此,对现有大型 Java 工程进行构件化,通过 OSGi 使系统采用面向服务基于构件的框架,可以更有效的对系统进行开发、维护和管理。但对系统进行框架上的改动是一件艰巨而有挑战性的工作,我们通过对 OSGi 框架的分析,结合具体实践中遇到的问题,提出了一套切实可行的解决方案。
基于分层思想提出了基于 OSGi 的构件化软件系统总体框架,如 图 1 所示系统大致分为以下四个层次。内核层,采用 OSGi 实现作为系统执行环境。基础层,包括公共服务、共享资源和第三方 JAR 等。其中,公共服务 Bundle 提供上层构件实现中可能会用到的服务;共享资源 Bundle 提供软件构件层各 Bundle 可能会用到的共享资源;第三方 JAR Bundle 是将原系统中 JAR 包统一管理,对外提供调用接口。软件构件层,根据高内聚低耦合的原则,对原来的系统进行构件划分,每个构件实现特定的功能,相互之间的功能相对独立。根据实现和接口相分离的原则,每个构件都是由构件的具体实现即软件构件 Bundle 和构件抽象出来的接口即构件 API Bundle 所构成。软件构件 Bundle 调用其他的构件 API Bundle 和基础层 Bundle 提供的功能,并且通过构件 API Bundle 对外提供调用接口。服务提供层,将软件构件层各个 Bundle 提供的服务接口进行统一管理,以供上层应用开发时使用,便于通过调用底层功能和服务对系统进行扩展。
图 1 基于 OSGi 的构件化软件系统框架图
基于 OSGi 的软件构件化方法分为两个阶段,第一阶段为软件构件化,第二阶段为构件服务化,如 图 2 所 示。第一阶段分析遗留软件以确定构件划分,这一步骤需要领域知识或者已有的文档。根据分析结果,遗留软件被划分为若干低耦合、高内聚的块,它们随后被封装 成各个构件。分析各构件之间的依赖,依照这些依赖抽取出构件的接口,使构件之间的依赖都变成以依赖相应接口的形式体现。第二阶段是依照构件的接口构造各个 构件提供的服务,目标是提供运行时动态性,服务的注册、发现和绑定都交由底层框架实现,构件提供生命周期接口,供管理员启动、停止和更新服务。
图 2 基于 OSGi 的软件构件化方法
( 1 )构件划分
构件是可以独立部署和卸载的单元,一个构件一般具有一项或几项功能,因此构件化的软件有较高的内聚性。同时,一个构件不需要关心其它构件的实现,它只需要了解其它构件的接口和相应的约定,因此构件化的软件是低耦合的。传统的 Java 软 件系统缺乏有效的信息隐藏机制,各构件之间的依赖是隐式的,经常会出现多个构件共同实现一项功能或者一个构件包含多项功能。我们需要做一些调整,将共同实 现一项功能的多个构件合并为一个构件,同时,一个实现多项功能的构件需要被划分为多个构件,并且良好地定义相互之间的依赖关系。 OSGi 是一种面向服务的体系结构,它不但提供了 Package 方式共享代码和资源,而且提供了一个定义良好的服务层,支持面向服务的编程。通过使用 OSGi 的服务层,可以实现系统的动态性,使系统具有较低的耦合性。基于 OSGi 平台的 Bundle 实现应该避免强依赖于其他 Bundle 的资源或服务,即程序不能认为其他 Bundle 中的资源或服务一定存在,这可以通过具体的编程实现来完成。
( 2 )接口抽取
为增加软件的动态性,我们需要将接口与实现相分离。在传统 Java 开发中,工厂设计模式和控制反转被用来创建对象实例,调用和被调用的对象之间的关系是隐式的。在 OSGi 环境中, Bundle 之间的依赖由延迟绑定机制实现,面向接口的设计方法将接口与实现划分到不同的 Bundle 中。 OSGi 在运行时才构成依赖,为每个 Bundle 提供独立的类加载器,使得我们可以真正的做到面向接口的开发。将接口和实现放在不同的 Bundle 中,并导出接口 Bundle 中所有的包,隐藏实现 Bundle 的包,可以强制用户使用服务和接口进行调用。这样分离后能够容易地添加新的服务实现或者替换原有的服务实现,而这个过程中,接口 Bundle 都不需要有任何变化。将构件实现和提供的服务接口封装在不同的 Bundle 中,在运行时由 OSGi 框架自动完成注入,可使系统具有较高的动态性。
( 3) 依赖显式声明
大 型软件系统一般都会由若干构件组成,这些构件合作以实现软件的功能。若一个构件找不到它所要依赖的服务,则不能正常运行。在构件开发中,开发人员可能并未 对记录服务之间依赖的文档引起足够重视。依赖描述不足使构件的组合和演化变得困难,构件化的软件需要显式地声明服务接口。另外,由于第三方构件更新频繁, 而一个构件一般需要依赖另一构件的某一特定版本,因此在声明依赖时也需要声明版本号。
( 1) 服务注册和服务发现
通 过将服务计算的思想引入构件模型,松散耦合的构件可以在系统运行时加入或离开系统。构件被用来实现服务,一个构件依赖其它构件的一些服务,也向外提供一些 服务。系统核心层支持服务的注册、发现和绑定机制,核心层依照构件和服务的描述文件来管理系统中的构件和服务。我们定义服务单元来发布服务,服务单元由服 务构件实现,暴露若干服务接口,如 图 3 (A) 所示。同时,服务构件查找和绑定它所依赖的其它服务,如 图 3 (B) 所示。
图3 服务机制实例图
( 2) 服务绑定和生命周期管理
通 过注册事件或使用构件化软件管理工具,我们可以让构件感知系统的动态变化。在软件构造过程中,构件依赖服务接口,而不是服务的具体实现,这是通过将接口与 实现相分而离达到的,接口与实现在调用服务时绑定。通过服务生命周期管理接口和延迟绑定机制,构件可以在系统运行时加入或离开系统,不需要重新启动依赖它 们的构件。因此,我们可以使用这种机制在运行时添加、删除或更新构件。
我们结合在基于 OSGi 的软件构件化实践过程中遇到的困难,分析了需要处理的典型问题,给出了相应的解决方案。
(1) 类加载机制
Java 的类加载器决定了应用程序的运行时边界;如果将一个类加载器赋予一个构件,则它决定了这个构件的运行时边界。类加载器分离是 OSGi 的一个重要特性,在 OSGi 框架中,每个 Bundle 都有自己的类加载器。每个 Bundle 有明确定义的边界,一个 Bundle 对其它 Bundle 隐藏自己的包和类,名字冲突就可以避免。并且, Bundle 可以动态地从系统中卸载掉,装卸 Bundle 就不会对 Java 虚拟机产生污染。通过类加载机制, Bundle 之间可以共享资源和类,前提是要显式声明导入和导出。在传统的 Java 应用程序中,代码经常会使用线程的上下文类加载器来加载类,程序代码也经常会将上下文类加载器设置为一个用户自定义的类加载器对象,该对象通常以 AppClassLoader 为它的父类加载器,这样上下文类加载器总能找到在该应用程序类路径上的类。然而在 OSGi 环境下,已经没有一个可以加载到类路径上所有类的类加载器。在一些情况下,当一个线程的控制流从一个 Bundle 跳转到另一个 Bundle ,该线程的上下文类加载器在新 Bundle 中变得几乎没有什么用处,因为一个 Bundle 的类加载器不能加载其它 Bundle 中的类。因此,我们应使用 Bundle 的类加载器来加载本 Bundle 中的类或者从其它 Bundle 中导入的类。然而,不少第三方库使用线程的上下文类加载器来查找资源,例如,在我们对 Java EE 应用服务器进行构件化的过程中,类 javax.naming.InitialContext 的代码使用线程的上下文类加载器来查找 jndi.properties 文件。为解决此问题,当控制流进入这些第三方库之前,我们需要将线程的上下文类加载器设置为合适的对象,根据这个库决定需要加载哪些类。
(2) 资源共享
在传统的 Java 项目中,程序各构件编译后的类文件通常会处于同一个空间中,如同一个目录或者同一个 JAR 包。代码中用到的资源文件也可能位于这一相同的空间中,如果各构件都需要访问某一个资源,大家都能够在程序的默认的类路径中找到这个资源。但是在 OSGi 环境中,各个构件被封装到不同的 Bundle 中,每个 Bundle 都有独立的类加载器和类路径,每个 Bundle 只能访问自己的类路径下的资源。如果需要访问其它 Bundle 中的资源,则需要在 Manifest.MF 中使用 Require-Bundle 。如果一个资源被多个构件使用,则大量的 Require-Bundle 会减弱 OSGi 的构件化,使构件间存在较多依赖关系,使得系统难以维护。第三方 JAR 包同样存在共享问题,传统的 Java 项目都会使用一些外部的库,这些库往往以第三方 JAR 包的形式存在。在将软件 OSGi 化过程中,如何处理这些 JAR 包往往会成为一个难题。因为可能几个 Bundle 都需要访问同一个 JAR 包,如果将这些 JAR 包放到所有需要它的 Bundle 的类路径中,很大程度上会造成重复,并且更新 JAR 包过程较为繁琐。为了减少重复,提高构件的可替代性,可以将第三方 JAR 包封装成 Bundle ,导出将会被外界使用的包,这样就能够使得各 Bundle 都能够使用第三方 JAR 包含的类。
( 3 )依赖分析
在 OSGi 环境中,若一个 Bundle 要访问其它 Bundle 中的类,则需要通过 MANIFEST.MF 文件中的 Import-Package 头或 Require-Bundle 头显式导入这些类。当遗留系统中的构件被封装成 Bundle 后,这些 Bundle 之间的依赖需要用一些静态分析工具来计算得到。但一些运行时依赖难以用静态分析工具获得,这些依赖需要人工的代码检查。例如由于 Class.forName(stringVar) 语句造成的类依赖就很难通过静态分析得到。同时,递归依赖也是 Java 应用程序中的一个常见而且困难的问题,在传统 Java 系统中,虽然系统被划分为若干构件,但这些构件由于共享类路径而难以被分离开。构件间的隐式依赖可能会导致递归依赖。若 Bundle 依赖图中有环,则无法找到一个有效的 Bundle 加载顺序,这样 Bundle 的加载过程就会失败。为解决此问题,我们将接口与实现分离开,如 图 4 所示,构件就可以找到它所依赖构件的接口,这样就解决了递归依赖的问题,系统就可以加载成功。同时,若接口 Bundle 之间出现了递归依赖,则这样的一些接口 Bundle 应合并为一个 Bundle 。
图 4 OSGi 系统对循环依赖问题的处理
Java EE 是开发、部署、执行和管理基于 Java 分布式应用的标准平台。近年来 Java EE 在企业应用领域飞速发展,越来越多的共性特性被提炼出来,以构成 Web 应用服务器( WAS , Web Application Server )的功能服务。当前 WAS 功能逐渐复杂、规模不断扩大,其标准已经包括了 20 多种服务。因此,以较低的管理代价,组织和整合如此数量众多、功能各异的服务,以构建具有较高动态性的 WAS 面临着巨大的挑战。首先,组成 WAS 的构件需要具有可移植性。 WAS 规模不断扩大,完全由一个组织单独完成 WAS 开发变得越发困难,大多数 WAS 需要使用其他组织提供的构件,因此便于其他构件的整合显得尤为重要。其次, WAS 需要具有柔韧性。为了提高启动速度, WAS 提供接口按需启动所必须的服务,可定制精简的 WAS 不仅节省了系统资源,提高了启动速度,而且减少了管理复杂性。最后,构件具有独立性。每个构件需要独立于 WAS 整体,具有自身生命周期,使得运行时能够对构件进行重配而不必重启整个 WAS ,并且单个构件出现问题也不影响系统其他部分,从而提高了可靠性。为了解决上述问题,我们基于 OSGi 对由中国科学院软件研究所研制的 Java EE 应用服务器 OnceAS 进行构件化,通过此实例验证本文所提出方法的有效性。
根据 Java EE 标准规范,我们将 OnceAS 划分为 Web 容器、 EJB 容器、 JNDI 构件、事务管理构件等部分,这使得每个构件实现相对独立的功能, Bundle 及其依赖关系如 图 5 所示。每个构件包裹为两个 Bundle ,即实现 Bundle 和接口 Bundle 。与此同时,一些独立的公共服务构件如数据传输、文件处理等也被分别包裹为 Bundle 形式。在传统的 Java EE 应用服务器中,类加载器定义为树状结构,应用的类加载器能够在树中找到合适的类加载器。但在基于 OSGi 的 WAS 中,每个 Bundle 具有独立的类路径,我们需要改变使之适应 OSGi 环境。当 Bundle 中的线程启动,我们为其设置 URL 类加载器实例,使之能够得到全局文件的文件夹,并且采用 Bundle 的类加载器作为其上下文加载器。同时, Bundle 间的类共享可以通过导入和导出包的机制完成,而不是直接采用其构件中的类加载器。
图 5 构件划分及依赖关系
在 OSGi 框架中,发布与引用服务通过显示方法调用,这种方式较为复杂。在 Bundle 开发过程中,开发人员通常需要做较多额外工作,通过实现 BundleActivator 接口以管理构件生命周期,并且增加静态代码以注册和引用服务。此外,服务间的依赖并不能直接交由 OSGi 框架管理,而手工管理构件又是较为复杂的工作。所以我们采用 OSGi R4 规范中的 DS 机制管理构件和服务,其简化了服务发布、引用和绑定的过程,管理了服务的生命周期。在 DS 中,服务由构件实现,包括实现类构件及与其对应的一个或多个接口。每个服务构件具有 XML 描述文件,声明了实现类、所依赖服务、所提供服务和其他相关信息。如果服务依赖关系满足,服务构件运行时( SCR , Service Component Runtime )将会激活方法,启动服务构件,而后所提供的服务注册到服务注册器。
对大规模软件系统进行构件化是较为繁琐而困难的工作。通过使用工具软件,在软件构件化过程中开发人员可以将更多的注意力放在系统的整体结构设计和需要改变的内容,而不必过多的注意细节,从而提高构件化的效率,减少错误发生的可能性。
( 1 ) Bundle 的构建:描述文件 Manifest.MF 是每个 Bundle 必不可少的部分,通过分析其原数据 OSGi 对 Bundle 进行解析。但是,编辑 Manifest.MF 复杂且易于出错,我们采用 Bnd 工具以简化该工作。 Bnd 用于创建符合 OSGi R4 规范的 Bundle 和检测 Bundle 是否符合 OSGi R4 规范。与 Bundle 的描述文件 Manifest.MF 相比, Bnd 的描述文件更容易编写,根据用户编写的描述文件能够自动添加相关信息并产生规范的 Manifest.MF 文件。同时可以自动按照 OSGi R4 规范把类路径下的文件打包为 OSGi Bundle ,并且帮助我们检测已有的 Bundle 是否符合 OSGi R4 规范。
( 2 )项目的构建:采用 OSGi 平台后,系统各构件构建后都需要打包成 Bundle ,这样一个软件可能有几十甚至上百个 Bundle 。手动生成如此大量的 Bundle 是件繁琐而且困难的工作。另外,因为构件之间存在依赖,如构件 A 依赖构件 B ,那么构件 B 必须先于构件 A 被构建。所以我们还必须对各构件根据依赖进行拓扑排序,按照排序结果顺序构建各构件。 Maven 是开源项目,提出了一个软件开发领域具有普遍意义的概念性模型,部分模型被硬编码为 Maven 代码库的一部分。通过使用 Maven 就可以分析构件间的依赖,从而生成有效的构建顺序,然后完成多构件构建工作并一次性自动构建 Bundle ,还可以通过使用插件,方便的集成其他工具,最后将构建的结果组装成一个发行包。
本文选用 TPC-W 基准测试作为实验基础, TPC-W 是一个被广泛使用的模拟在线电子书店的 Web 应用标准测试规范。实验对象选用了中国科学院软件研究所研发的符合 TPC-W 规范的基准测试套件 Bench4Q 。本实验系统的软硬件测试环境在 表 1 中给出,负载发生器模拟动态变化的工作负载, Bench4Q 应用部署在 WAS 上,通过访问后端的数据库获取数据。为了评价构件化方法的可行性,我们考虑两个度量来评价其性能开销,即单位时间完成的事务数和 CPU/ 内存的资源占用率。
表1 实验软、硬件环境
|
Processor |
RAM |
OnceAS(Original/OSGi) |
Intel® Xeon™ 2.5GHz (8 CPUs) |
2G |
Database(DB2) |
Intel® Xeon™ 3.0GHz (4 CPUs) |
2G |
Clients(Emulated-Browsers) |
Intel® Pentium® 4 2.80GHz |
1G |
客户端以包括一系列连续请求的会话形式访问 Web 站点,用户登录、浏览商品、将商品放入购物车、结账等操作。根据 TPC-W 规范,在整个实验过程中,模拟浏览器数量保持恒定。性能指标通过每秒交互数量和每个请求的响应时间来度量。 WAS 构件化前后的对比结果如 图 6 所示,可以看到性能开销是可以接受的,这是由于 OSGi 框架带来较低的管理开销。基于 OSGi 的系统在启动阶段, OSGi 进行初始化操作,记录导入 / 出的包,由此可见越多 Bundle ,越多导入 / 出包,启动时间越长。而 OSGi 采用延迟加载机制按需启动服务,所以启动阶段完成的工作占用的时间有限。在稳定阶段,服务构件具有所依赖的服务引用,通过直接得到调用方法可以完成服务调用,这样此阶段的 CPU 开销是相当低的;只有在构件更新时会做些构件初始化操作,而此时开销也不大。由此可见, OSGi 框架所带来的资源和性能开销是可以接受的,基于 OSGi 的构件化方法具有可行性。
图 6 性能开销评估
首先我们分析一下基于 OSGi 框架的 WAS 在动态可扩展方面的增强。目前 Servlet 规范最新的版本是 3.0 ,但现在大量 Web 应用仍在采用 2.5 或者更低的版本。由于传统的 Java 项目缺乏对 JAR 的版本控制机制,不同版本 Servlet 同时存在会产生冲突,因此无法同时支持采用不同版本 Servlet API 的 Web 应用。当有新的 Web 应用需要不同版本 Servlet API 时,就要修改系统代码,替换原有的 JAR 包,重新构建系统,操作复杂繁琐,可扩展性差。采用我们基于 OSGi 框架的 WAS 后,就可以向系统中添加不同版本的 Servlet Bundle 。各个 Bundle 提供不同版本的 Servlet API ,只须在 Manifest.MF 文件中声明所 Export 的版本号, RequestHandler Bundle 指定需要导入的版本号,便可以满足 Web 应用对不同版本 Servlet API 的需求,根据 Web 应用的需要选用合适的 Sevlet Bundle 调用相应的接口。
动态可重配性同样也是 OSGi 框架提供的重要特性,在传统的 Java 项目中添加或修改构件需要停止系统,通过硬编码的方式实现构件的添加修改,这样的操作需要在编译阶段完成。 WAS 部署的一些应用需要提供 7*24 小时的服务,而原先的 WAS 并不能满足这方面的需要。通过基于 OSGi 的构件化后,在系统运行过程中就可以动态添加或更新需要的构件而不需要重启系统。这样的动态操作由 OSGi 框架的服务层来完成的,通过动态注册和注销服务实现构件的运行时插拔,即插即用。在基于 OSGi 的 WAS 中可以动态插拔不同版本的 JSP Processor Bundle 或替换为更高效的 HTTP 解析器而不必重启系统,保证了系统的可用性。
当前的软件系统对动态性、可扩展性和可维护性提出了更高的要求。面向服务构件模型通过将面向服务计算引入到构件模型中,为动态可扩展的系统开发提供了支持。 OSGi 为开发面向服务基于构件的大型软件项目提供了简单、动态和轻量级的基础框架。目前,大量的大型项目都在推出 OSGi 做为基础框架的新版本,如 Eclipse3.0 , JonAS5 , BMW 汽车控制应用系统等,由此可见,基于 OSGi 对原有软件系统进行构件化显得尤为重要。构件化和动态性是 OSGi 的两个重要优势,文中从软件构件化和构件服务化两方面提出了基于 OSGi 的软件构件化方法,并且分析了在实践中的关键问题以及解决方案。以上这些工作对基于 OSGi 的软件设计开发和面向服务构件化的软件再工程实践具有着普遍的借鉴意义。最后,通过对 Java EE 应用服务器 OnceAS 构件化的实例研究,验证了文中所提出方法的有效性。
参考文献
[1] Kaegi S R, Deugo D. Modular Java web applications [C]. In: Proceedings of the 2008 ACM
symposium on Applied computing.Fortaleza, Ceara, Brazil: ACM, 2008. 688-693.
[2] Adamek J, Hnetynka P. Perspectives in component-based software engineering [C]. In:
Proceedings of the 2008 international workshop on Software Engineering in east and south
europe.Leipzig, Germany: ACM, 2008. 35-42.
[3] Bichier M, Lin K J. Service-oriented computing[J]. 2006, 39(3): 99-101.
[4] Cervantes H, Hall R S. Autonomous Adaptation to Dynamic Availability Using a Service-
Oriented Component Model [C]. In: Proceedings of the 26th International Conference on
Software Engineering.IEEE Computer Society, 2004. 614-623.
[5] Alliance O. OSGi Service Platform Core Specification Release 4[M]. 2007.