深入理解<mark>OSGI</mark>:第三章 生命周期层规范与原理(1)

 由  IcyFenix 发布

本章简介

OSGi规范把模块化“静态”的一面,比如如何描述元数据、如何加载模块中的类和资源等内容定义于模块层规范之中;而把模块化“动态”的一面,比如模块从安装到解析、启动、停止、更新、卸载的过程,以及在这些过程中的事件监听和上下文支持环境等定义于生命周期层(Life Cycle Layer)之中。在第2章介绍了模块层相关知识后,这里再从OSGi运行时的角度去看一下模块化“动态”的相关知识。

因为生命周期层本身具有“动态”的特性,所以本章介绍的大多数例子的演示都要求OSGi框架真正运行起来才能进行。笔者演示例子采用的OSGi框架是Equinox 3.8(用于支撑Eclipse 4.2的版本),后面不再单独说明。如果读者想了解如何部署运行此框架,可参考本书第5章的相关内容。

3.1 Bundle标识

在模块层的讲解中,笔者介绍过Bundle的唯一标识是由Bundle-SymbolicName和Bundle-Version标记共同构成的。对于生命周期层,我们依然可以采用Bundle-SymbolicName和Bundle-Version标记来确定唯一的Bundle。不过,基于API使用方便的考虑,在运行期还可以采用其他Bundle标识进行定位,包括:

  • Bundle ID(Bundle Identifier)。Bundle ID是运行期最常用的标识符,尤其是在Equinox Console的命令中。它是由OSGi框架自动分配的一个长整型数字,在Bundle整个生命周期内(包括Bundle更新、卸载之后)都不会改变,甚至在OSGi框架重启后都能保留下来。Bundle ID是在Bundle安装过程中由OSGi框架根据Bundle安装时间的先后次序,由小到大进行分配的。在代码中可以通过Bundle接口的getBundleId ()方法来获取当前Bundle的ID。
  • Bundle位置(Bundle Location)。Bundle位置是OSGi容器在Bundle安装过程中分配给Bundle的定位字符串。这个字符串通常是该Bundle的JAR文件地址,但是这并不是强制性的。在一个OSGi容器中,每个Bundle的定位字符串都必须是唯一的,即使Bundle更新时改变了JAR文件的路径,也不会修改这个定位字符串,所以它可以唯一确定一个Bundle。在代码中我们可以通过Bundle接口的getLocation()方法来获取一个Bundle的定位字符串。
  • Bundle符号名称(Bundle Symbolic Name)。前面介绍过,Bundle的符号名称由开发人员设定,保存于Bundle元数据信息之中。它是静态的信息,在Bundle打包发布的那一刻它就被确定下来,不会因使用了不同的OSGi框架而有所不同(前面的Bundle ID和Bundle Location是由OSGi框架所决定的)。Bundle的版本与符号名称一起可以唯一定位一个Bundle,在代码中可以通过Bundle接口的getSymbolicName()方法获取当前Bundle的符号名称,通过getVersion()方法获取Bundle的版本号。

我们可以写一小段简单的代码,在Equinox框架中运行查看这3个唯一标识,示例如下:

System.out.println("Location:" + bundleContext.getBundle().getLocation());
System.out.println("ID:" + bundleContext.getBundle().getBundleId());
System.out.println("SymbolicName:" + bundleContext.getBundle().getSymbolicName());
输出结果为:
Location: initial@reference:file:../WorkSpaces/equinox/BundleA/
ID: 1
SymbolicName: Bundle

3.2 Bundle状态及转换

“状态”是Bundle在运行期的一项动态属性,不同状态的Bundle具有不同的行为。生命周期层规范定义了Bundle生命周期过程之中的6种状态,分别是:UNINSTALLED(未安装)、INSTALLED(已安装)、RESOLVED(已解析)、STARTING(启动中)、STOPPING(停止中)、ACTIVE(已激活),它们的含义为:

  • UNINSTALLED,未安装状态。处于未安装状态的Bundle导出的Package和包含的其他资源都是不可使用的。但是OSGi容器中代表这个Bundle的对象实例仍然可以操作,在某些场景,比如自省(Introspection)中这个对象还是可用的。UNINSTALLED的状态值为整型数1。
  • INSTALLED,已安装状态。Bundle处于已安装状态就意味着它已经通过OSGi框架的有效性校验(有效性校验的内容可参见第2章中模块层定义的介绍)并产生了Bundle ID,但这时还未对它定义的依赖关系进行解析处理。INSTALLED的状态值为整型数2。
  • RESOLVED,已解析状态。Bundle处于已解析状态说明OSGi框架已经根据元数据信息中描述的依赖关系成功地在类名空间中找到它所有的依赖包,这时它导出的Package就可以被其他Bundle导入使用。RESOLVED的状态值为整型数4。
  • STARTING,启动中状态。Bundle处于启动中状态说明它的BundleActivator的start()方法已经被调用,但是还没执行结束。如果start()方法正常执行结束,Bundle将自动转换到ACTIVE状态; 否则,如果start()方法抛出了异常,Bundle将退回到RESOLVED状态。STARTING的状态值为整型数8。
  • STOPPING,停止中状态。Bundle处于停止中状态说明它的BundleActivator的stop()方法已经被调用,但是还没执行结束。无论stop()是正常结束还是抛出了异常,在这个方法退出之后,Bundle的状态都将转为RESOLVED。STOPPING的状态值为整型数16。
  • ACTIVE,Bundle处于激活状态,说明BundleActivator的start()方法已经执行完毕,如果没有其他动作,Bundle将继续维持ACTIVE状态。ACTIVE的状态值为整型数32。

OSGi规范定义的部分API接口对Bundle的状态有要求,只有处于特定状态的Bundle才能调用这些API。在代码中可以使用Bundle接口中的getState()方法来检测Bundle目前的状态,示例如下:

import static org.osgi.framework.Bundle.*;
……
Bundle bundle = bundleContext.getBundle();
if((bundle.getState() & (STARTING | ACTIVE | STOPPING)) != 0){
    // 进行某些只允许在STARTING、ACTIVE、STOPPING状态下执行的动作
}

目前Bundle的状态值采用由整型数存储的位掩码(Bit-Mask)来表示,而没有直接采用JDK 1.5后提供的枚举类型。从OSGi R4.3规范开始,规范中定义的标准接口中已经开始使用了部分JDK1.5的语言特性,主要是泛型支持,在对应的实现R4.3规范的Equinox 3.7框架的元数据信息中,运行环境项也升级到了“J2SE-1.5”。不过,其他JDK 1.5中的新语言特性,如注解和枚举,仍然没有被采用。这主要是考虑到已有代码的兼容性,Java基于擦除法实现的范型可以在编译时直接使用Javac的“-target jsr14”转换出能部署在JDK 1.4环境下的代码,而注解、枚举等还需要其他Backport运行库支持才可以。

Bundle状态是动态可变的,上述6种状态可以在特定条件下互相转换,图3-1描述了状态转换的规则和所需的条件,随后笔者将详细解释这些状态的转换过程。

http://assets.osgi.com.cn/article/7289375/图3-1.jpg

图3-1 Bundle 状态转换示意图

3.2.1 安装过程

OSGi规范定义了BundleContext接口的installBundle()方法来安装新的Bundle,方法参数为要安装的Bundle的Bundle Location。但是OSGi规范没有详细规定Bundle的安装过程应当如何进行,只是很笼统地要求OSGi框架在实现这个方法时,至少要完成生成新的Bundle ID、对元数据信息进行有效性校验、生成Bundle对象实例这些工作。我们不妨从Equinox的代码中看看Bundle安装过程是如何进行的,具体分析如下。

1)根据Bundle Location判定Bundle是否已经安装过,如果已安装,那么直接返回之前安装的Bundle实例。

// 代码位置:Framework.installWorker()
AbstractBundle bundle = getBundleByLocation(location);
// 如果Bundle已安装,直接返回之前安装的Bundle实例
if (bundle != null) {
    Bundle visible = origin.getBundle(bundle.getBundleId());
    if (visible == null) {
        BundleData data = bundle.getBundleData();
        String msg = NLS.bind(Msg.BUNDLE_INSTALL_SAME_UNIQUEID, new Object[] {data.getSymbolicName(), data.getVersion().toString(), data.getLocation()});
        throw new BundleException(msg, BundleException.REJECTED_BY_HOOK);
    }
    return bundle;
}

2)根据Bundle Location生成BundleData、BundleInstall等访问对象,这时需要完成下面几个关键的动作:

  • 产生新的Bundle ID;
  • 读取MANIFEST.MF文件中的内容,并且对其进行有效性校验(见2.4.3节),但不对内容进行具体的解析。
  • 更新Bundle的LastModified等信息(在Bundle更新检测中会使用到)。

    // 代码位置:BaseStorage.installBundle()
    // 产生新的BundleID
    BaseData data = createBaseData(getNextBundleId(), location);
    return new BundleInstall(data, source, this);
      ……
    // 代码位置:BundleInstall.begin()
    // 更新Bundle的LastModified等信息
    data.setLastModified(System.currentTimeMillis());
    

    data.setStartLevel(storage.getInitialBundleStartLevel());   …… // 代码位置:BundleInstall.begin() // 读取MANIFEST.MF文件中的内容 Dictionary manifest = storage.loadManifest(data, true);

3)检查将要安装的Bundle是否与系统中已有的Bundle重名,Bundle的符号名称和版本确定其唯一标识,OSGi不允许将两个符号名称和版本完全一样的Bundle安装到系统中。

// 代码位置:Framework.createAndVerifyBundle()
// 检查将要安装的Bundle是否设置了Bundle-SymbolicName,
// 以及符号名称和版本是否在系统中出现过
if (!allowDuplicateBSNVersion && bundledata.getSymbolicName() != null) {
    AbstractBundle installedBundle = getBundleBySymbolicName(bundledata.getSymbolicName(), bundledata.getVersion());
    if (installedBundle != null && installedBundle.getBundleId() != bundledata.getBundleID()) {
        String msg = NLS.bind(Msg.BUNDLE_INSTALL_SAME_UNIQUEID, new Object[] {installedBundle.getSymbolicName(), installedBundle.getVersion().toString(), installedBundle.getLocation()});
    throw new DuplicateBundleException(msg, installedBundle);
    }
}

4)根据Bundle类型(Fragment、Bundle Host等)确定Bundle对象的实现类,初始化Bundle对象实例。

// 代码位置:Framework.createBundle()
protected static AbstractBundle createBundle(BundleData bundledata, Framework framework, boolean setBundle) throws BundleException {
    AbstractBundle result;
    if ((bundledata.getType() & BundleData.TYPE_FRAGMENT) > 0)
        result = new BundleFragment(bundledata, framework);
    else if ((bundledata.getType() & BundleData.TYPE_COMPOSITEBUNDLE) > 0)
        result = new CompositeImpl(bundledata, framework);
    else if ((bundledata.getType() & BundleData.TYPE_SURROGATEBUNDLE) > 0)
        result = new SurrogateImpl(bundledata, framework);
    else
        result = new BundleHost(bundledata, framework);
    if (setBundle)
        bundledata.setBundle(result);
    return result;
}

5)将Bundle实例加入到OSGi框架的Bundle Repository之中,在这个过程结束之后,新安装的Bundle在Equinox Console和其他Bundle中就已经可见了。

// 代码位置:Framework.installWorkerPrivileged()
bundles.add(bundle);
storage.commit(false);

6)发布Bundle的INSTALLED状态转换事件。

// 代码位置:Framework.installWorker()
publishBundleEvent(new BundleEvent(BundleEvent.INSTALLED, bundle, origin.getBundle()));

虽然Bundle安装需要经过以上6个步骤,但是从Equinox Console或其他Bundle的角度观察,一个Bundle的安装过程是个原子过程,即要么Bundle已经安装了,要么Bundle还没有安装,不会观察到Bundle“正在安装之中”的状态,也不会出现Bundle安装了一半的情况。并且,Bundle的INSTALLED状态是一个持久状态,如果没有外部作用(改变启动级别、调用start()方法或卸载Bundle),Bundle将一直维持这个状态。

3.2.2  解析过程

解析过程是OSGi框架根据Bundle的MANIFEST.MF文件中描述的元数据信息分析处理Bundle依赖关系的过程。在实际开发中,经常会遇到“某个Bundle安装不上”这类问题,这种“安装不上”在大多数情况下不是指“安装过程”失败,而更多是指解析过程中的处理失败,抛出了异常。接下来我们了解一下OSGi框架是如何实现解析过程的。

1)先把OSGi框架中所有已安装的Bundle(包括未解析的)按照以下规则进行排序,以便在某个Package有多个导出源可供选择时确定依赖包的优先级关系(下面的Package优先级依次递减)。

  • 已经解析的Bundle导出的Package优先于未解析的Bundle导出的Package。
  • 具有更高版本的Package优先于低版本的Package。
  • 具有较低Bundle ID的Bundle导出的Package优先于较高Bundle ID的Bundle导出的Package。

    // 代码位置:ResolverImpl.resolve()
    resolverExports.reorder();
    resolverBundles.reorder();
    reorderGenerics();
    

2)对将要解析的Bundle执行一些基本的检查校验,确定它们是否符合被解析的基本条件,包括以下检查项: - 检查OSGi容器提供的执行环境是否能满足Bundle的需要; - 检查当前系统是否能满足在Bundle中定义的本地代码(Native Code)的需求(例如Bundle中的本地代码是以*.so形式发布的就只能用于Linux系统,是基于x86指令集的就无法用于ARM架构的机器等)。

// 代码位置:ResolverImpl.resolveBundles()

for (ResolverBundle bundle : bundles) {    state.removeResolverErrors(bundle.getBundleDescription());
   bundle.setResolvable(isResolvable(bundle, platformProperties, hookDisabled) || developmentMode); }

3)将Fragment Bundle附加到宿主Bundle之中,在对任何一个Bundle解析之前,要保证框架中所有的Fragment Bundle都已经正确附加到宿主上。

// 代码位置:ResolverImpl.resolveBundles0()
Collection<String> processedFragments = new HashSet<String>(bundles.length);
for (int i = 0; i < bundles.length; i++)
    attachFragment(bundles[i], processedFragments);

4)确保OSGi容器能提供所有Bundle元数据信息中声明的依赖项,如Require-Capability、Import-Package和Require-Bundle中声明的依赖内容,并且这些依赖项都要符合要求,具体要求如下:

  • Bundle声明导入的Package在容器中能够匹配到符合版本范围要求的提供者;
  • Bundle声明导入的Package包含了提供者在导出时所有声明要求包含的强制属性;
  • 如果Bundle声明导入的Package定义了属性,那么这些属性的值必须与导出时声明的属性值相匹配。
  • 如果某个Package的导入者与提供者都依赖于同一个Package,那么导入者必须满足导出Package时使用uses参数声明的约束。
  • Equinox对上述检查采用“Fast-Fail”方式实现,即检查到异常,校验程序立即中断并抛出异常,这些代码实现在ResolverImpl.resolveBundle()中。由于代码量较大,考虑版面关系,不再贴出具体代码,读者可自行阅读源码。

5)将Bundle的状态调整为已解析的状态。这里不仅要修改Bundle对象的getState()返回值,更重要的是要将前面计算出来的Bundle各个依赖项的提供者记录在Bundle对象实例上,并且把Bundle所能提供的导出包和Capabilities记录到OSGi容器之中。

这部分的实现主要在ResolverImpl.stateResolveBundle()之中,代码较多,考虑版面关系,读者可自行阅读源码。

3.2.3 启动过程

启动过程即执行Bundle的Activator.start()方法的过程,在此方法执行期间,Bundle的状态为STARTING。如果成功执行完这个方法,那么Bundle的状态会转变为ACTIVE,而且将一直保持这个状态直到Bundle被停止。

在Bundle启动时,OSGi框架需要通过调用Class.newInstance()方法来创建Activator类的实例,因此Bundle的Activator类必须保证有一个默认的(即不带参数的)构造函数。

在启动Bundle之前,OSGi框架首先对它进行解析。如果在OSGi框架试图启动一个Bundle时这个Bundle还没有被解析,框架会自动尝试对Bundle进行解析。如果解析失败,框架会在start()方法中抛出一个BundleException异常(即使还没有真正运行start()方法)。在这种情况下,OSGi框架会记住这个Bundle“已启动过”,但它的状态仍然保持为INSTALLED,一旦条件满足,Bundle变为可以解析之后,它就应该自动启动(当然,这个Bundle还要满足后面将介绍的启动级别的要求)。如果解析本身是成功的,但是start()方法本身的代码抛出了异常导致启动失败,那么Bundle应当退回到RESOLVED状态。

3.2.4 更新过程

前面曾经拿PC类比过OSGi模块化系统:如果把PC机看成一个OSGi系统,把组成PC的CPU、内存、键盘、鼠标等部件看做模块,这些模块中有一些(如CPU等)是必须停机更换的,另外一些(如键盘、鼠标等)可以支持热插拔。与热插拔类似,OSGi框架中同样可以支持模块的动态更新。

Bundle的更新过程其实就是重新从Bundle文件加载类、生成新的Bundle对象实例的过程。OSGi规范在Bundle接口中定义了以下两种方法来更新Bundle。

  • Bundle.update():从Bundle原来的位置进行更新。
  • Bundle.update(InputStream):通过一个指定的输入流获取Bundle新的内容进行更新。

更新Bundle意味着在代码中使用Bundle ID、Bundle位置和名称等方式来获取Bundle对象实例时,能够获取到包含新信息的Bundle对象。但是如果已经有代码在Bundle更新前读取了旧的信息并保持持有状态,那么持有的状态信息自然是不可能自动刷新的。所以某个Bundle是否支持热插拔,OSGi只提供了技术上的支持,如果开发人员在编写Bundle代码时就没有考虑过对状态的管理更新,那么所做出来的Bundle仍然是无法动态更新的。

另外,如果在新的Bundle中修改了原Bundle所导出Package或改变了导出Package的版本号,那么对于已经存在的、甚至是更新之后才安装的Bundle来说,原来导出的旧版本Package都依然是可用状态,直到调用了PackageAdmin服务的refreshPackages()方法或OSGi框架重新启动之后,这些旧版本的Package才会停止导出。

由于更新Bundle不意味着旧Bundle立即消失废弃,换句话说,与旧Bundle有关的对象实例、旧Bundle在Java虚拟机中形成的类型和类加载器都不会立刻消失,因此,如果有大量更新Bundle的操作,开发人员还应注意Java虚拟机方法区的内存占用压力,避免造成内存泄露问题。

下面对Equinox框架在更新Bundle时的代码进行分析。 1)检查Bundle目前是否处于可更新的状态,还没有安装、状态为UNINSTALLED的Bundle是不能被更新的。

// 代码位置:AbstractBundle.checkValid()
if (state == UNINSTALLED) {
    throw new IllegalStateException(NLS.bind(Msg.BUNDLE_UNINSTALLED_EXCEPTION, getBundleData().getLocation()));
}

2)确定Bundle更新的数据来源。这个来源可以是用户指定的一个输入流,如果用户没有指定,默认通过Bundle Location创建一个输入流。

// 代码位置:AbstractBundle.update()
if (in == null) {
    String updateLocation = bundledata.getManifest().get(Constants.BUNDLE_UPDATELOCATION);
    if (updateLocation == null)
        updateLocation = bundledata.getLocation();
    if (Debug.DEBUG_GENERAL)
        Debug.println("   from location: " + updateLocation); //$NON-NLS-1$
    //从Bundle原来的位置进行更新
    source = framework.adaptor.mapLocationToURLConnection(updateLocation);
} else {
    //通过一个指定的输入流获取Bundle的新内容进行更新
    source = new BundleSource(in);
}

3)根据输入流创建Bundle对象实例,实例创建的过程与安装Bundle时是一样的。

// 代码位置:AbstractBundle.updateWorkerPrivileged()
final AbstractBundle newBundle = framework.createAndVerifyBundle(newBundleData, false);

4)将新Bundle对象实例的信息刷新到调用update()方法的Bundle对象中,并更新OSGi框架的Bundle Repository。

// 代码位置:AbstractBundle.updateWorkerPrivileged()

synchronized (bundles) { String oldBSN = this.getSymbolicName(); exporting = reload(newBundle); bundles.update(oldBSN, this); manifestLocalization = null; }

5)将新的Bundle对象的状态转换回更新之前的状态。因为刚刚更新完的Bundle是处于INSTALLED状态的,如果更新前是ACTIVE、STARTING、STOPING等状态的,需要自动切换回原来的状态。这时执行的逻辑跟对应的状态变换过程是一致的。

// 代码位置:AbstractBundle.updateWorker()
if ((previousState & (ACTIVE | STARTING)) != 0) {
    try {
        startWorker(START_TRANSIENT | ((previousState & STARTING) != 0 ? START_ACTIVATION_POLICY : 0));
    } catch (BundleException e) {
        framework.publishFrameworkEvent(FrameworkEvent.ERROR, this, e);
    }
}

6)发布Bundle的UPDATED状态转换事件。 // 代码位置:AbstractBundle.updateWorker() framework.publishBundleEvent(BundleEvent.UPDATED, this);

3.2.5 停止过程

Bundle的停止过程是启动过程的逆向转换,此时OSGi框架会自动调用Bundle的Activator类的stop()方法。在stop()方法执行期间,Bundle的状态为STOPPING。当stop()方法成功执行完毕后Bundle的状态转变为RESOLVED。

我们一般用stop()方法来释放Bundle中申请的资源、终止Bundle启动的线程等。在执行完Bundle的stop()方法后,其他Bundle就不能再使用该Bundle的上下文状态(BundleContext对象)。如果在Bundle生命周期内在OSGi框架中注册了任何服务,那么在停止该Bundle之后,框架必须自动注销它注册的所有服务。但是如果Bundle在stop()方法中还注册了新的服务,这些服务就不会自动注销。

需要注意的是,即使Bundle已经停止,它导出的Package仍然是可以使用的,无论对停止前还是停止后安装的Bundle都是如此。这意味着其他Bundle可以执行停止状态的Bundle中的代码,Bundle的设计者需要保证这样做是符合预期、没有危害的。一般来说,停止状态的Bundle只导出接口是比较合理的做法。为了尽可能保证不执行代码,导出接口的类构造器(方法)中也不应该包含可执行的代码。

3.2.6 卸载过程

调用Bundle.uninstall()方法可以实现Bundle的卸载,此时该Bundle的状态会转变为UNINSTALLED。OSGi框架应尽可能释放被卸载的Bundle所占用的资源,尽可能把框架还原成该Bundle安装前的样子。如果该Bundle导出了Package,并且这个Package被其他Bundle导入过,那么对于这些Bundle来说,原来导入的Package都是可用的,直到调用了PackageAdmin的refreshPackages()方法或框架重新启动之后才会被卸载掉。这与停用和更新Bundle时遗留的旧Package不一样,这些旧Package在卸载后会变为不可见,卸载之后才安装到OSGi框架的Bundle是不能导入它们的。

查看评论