对OSGi软件构件化方法的设计理解

来源:互联网 发布:c语言2的n次方 编辑:程序博客网 时间:2024/05/16 08:35

引言

       在对OSGi框架的设计过程中,我们遇到了有很多需要考虑和设计的问题。我们把中间件设计成OSGi框架,而OSGi也非常适合作为中间件基础。关键的关键还是在于设计。在用传统的方式进行设计时,就算不考虑太多关于接口、包隔离、模块隔离、依赖关系都可以做一个能运行良好的系统,把功能加上能跑就好。但OSGi却要强迫人们先拿出一个良好的设计再说,很多设计者缺乏这方面的经验,难受是自然的。

       除了不断的尝试和讨论分析,我们还需要参考一些开源的项目设计。比如Apache的Amdatu项目,这个云计算应用部署平台里还集合了许多subprojects,有许多与我们类似的架构设计,而且他有个比较核心的类以及类的管理存储机制,是关于multi-tenant(多租户)的,这对我们来说也是一个有启发性的思路。Amdatu集合了Apache的Felix框架,ACE,还运用到了Jclouds的Blobstore。目前我对这几个项目的源码和框架设计还解读不熟,可能会在今后以博文的方式记录下我的学习心得,到时候会把我们自己的项目也大体进行一个对比介绍。主要目的还是希望可以和从事OSGi框架设计的朋友们一起探讨和发现。


基于OSGi的软件构件化方法

1    软件系统结构设计

基于分层思想提出了基于OSGi的构件化软件系统总体框架,如图 1所示系统大致分为以下四个层次。内核层,采用OSGi实现作为系统执行环境。基础层,包括公共服务、共享资源和第三方JAR等。其中,公共服务Bundle提供上层构件实现中可能会用到的服务;共享资源Bundle提供软件构件层各Bundle可能会用到的共享资源;第三方JAR Bundle是将原系统中JAR包统一管理,对外提供调用接口。软件构件层,根据高内聚低耦合的原则,对原来的系统进行构件划分,每个构件实现特定的功能,相互之间的功能相对独立。根据实现和接口相分离的原则,每个构件都是由构件的具体实现即软件构件Bundle和构件抽象出来的接口即构件API Bundle所构成。软件构件Bundle调用其他的构件API Bundle和基础层Bundle提供的功能,并且通过构件API Bundle对外提供调用接口。服务提供层,将软件构件层各个Bundle提供的服务接口进行统一管理,以供上层应用开发时使用,便于通过调用底层功能和服务对系统进行扩展。

图1 基于OSGi的构件化软件系统框架图

2    基于OSGi的软件构件化方法

基于OSGi的软件构件化方法分为两个阶段,第一阶段为软件构件化,第二阶段为构件服务化,如图 2所示。第一阶段分析遗留软件以确定构件划分,这一步骤需要领域知识或者已有的文档。根据分析结果,遗留软件被划分为若干低耦合、高内聚的块,它们随后被封装成各个构件。分析各构件之间的依赖,依照这些依赖抽取出构件的接口,使构件之间的依赖都变成以依赖相应接口的形式体现。第二阶段是依照构件的接口构造各个构件提供的服务,目标是提供运行时动态性,服务的注册、发现和绑定都交由底层框架实现,构件提供生命周期接口,供管理员启动、停止和更新服务。

 

图2 基于OSGi的软件构件化方法

2.1       软件构件化

(1)构件划分

构件是可以独立部署和卸载的单元,一个构件一般具有一项或几项功能,因此构件化的软件有较高的内聚性。同时,一个构件不需要关心其它构件的实现,它只需要了解其它构件的接口和相应的约定,因此构件化的软件是低耦合的。传统的Java软件系统缺乏有效的信息隐藏机制,各构件之间的依赖是隐式的,经常会出现多个构件共同实现一项功能或者一个构件包含多项功能。我们需要做一些调整,将共同实现一项功能的多个构件合并为一个构件,同时,一个实现多项功能的构件需要被划分为多个构件,并且良好地定义相互之间的依赖关系。OSGi是一种面向服务的体系结构,它不但提供了Package方式共享代码和资源,而且提供了一个定义良好的服务层,支持面向服务的编程。通过使用OSGi的服务层,可以实现系统的动态性,使系统具有较低的耦合性。基于OSGi平台的Bundle实现应该避免强依赖于其他Bundle的资源或服务,即程序不能认为其他Bundle中的资源或服务一定存在,这可以通过具体的编程实现来完成。

(2)接口抽取

为增加软件的动态性,我们需要将接口与实现相分离。在传统Java开发中,工厂设计模式和控制反转被用来创建对象实例,调用和被调用的对象之间的关系是隐式的。在OSGi环境中,Bundle之间的依赖由延迟绑定机制实现,面向接口的设计方法将接口与实现划分到不同的Bundle中。OSGi在运行时才构成依赖,为每个Bundle提供独立的类加载器,使得我们可以真正的做到面向接口的开发。将接口和实现放在不同的Bundle中,并导出接口Bundle中所有的包,隐藏实现Bundle的包,可以强制用户使用服务和接口进行调用。这样分离后能够容易地添加新的服务实现或者替换原有的服务实现,而这个过程中,接口Bundle都不需要有任何变化。将构件实现和提供的服务接口封装在不同的Bundle中,在运行时由OSGi框架自动完成注入,可使系统具有较高的动态性。

(3) 依赖显式声明

大型软件系统一般都会由若干构件组成,这些构件合作以实现软件的功能。若一个构件找不到它所要依赖的服务,则不能正常运行。在构件开发中,开发人员可能并未对记录服务之间依赖的文档引起足够重视。依赖描述不足使构件的组合和演化变得困难,构件化的软件需要显式地声明服务接口。另外,由于第三方构件更新频繁,而一个构件一般需要依赖另一构件的某一特定版本,因此在声明依赖时也需要声明版本号。

2.2       构件服务化

(1) 服务注册和服务发现

通过将服务计算的思想引入构件模型,松散耦合的构件可以在系统运行时加入或离开系统。构件被用来实现服务,一个构件依赖其它构件的一些服务,也向外提供一些服务。系统核心层支持服务的注册、发现和绑定机制,核心层依照构件和服务的描述文件来管理系统中的构件和服务。我们定义服务单元来发布服务,服务单元由服务构件实现,暴露若干服务接口,如图 3(A)所示。同时,服务构件查找和绑定它所依赖的其它服务,如图 3(B)所示。

 

图3 服务机制实例图

(2) 服务绑定和生命周期管理

通过注册事件或使用构件化软件管理工具,我们可以让构件感知系统的动态变化。在软件构造过程中,构件依赖服务接口,而不是服务的具体实现,这是通过将接口与实现相分而离达到的,接口与实现在调用服务时绑定。通过服务生命周期管理接口和延迟绑定机制,构件可以在系统运行时加入或离开系统,不需要重新启动依赖它们的构件。因此,我们可以使用这种机制在运行时添加、删除或更新构件。

3    关键问题分析与解决

我们结合在基于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系统对循环依赖问题的处理




以上分析及图示,希望能对您的设计有所启发



原创粉丝点击