深入理解<mark>OSGI</mark>:第二章 模块层规范与原理(1)

 由  IcyFenix 发布

本章简介

从本章开始,我们将为读者讲解OSGi中最常用和最重要的内容。“最常用和最重要”意味着并不会涵盖OSGi规范的所有方面。“讲解”也不是对OSGi规范的直接翻译,笔者简化了OSGi规范中一些近乎于数学公式的严谨描述,改用尽可能通俗的语言进行介绍,并添加了目前业界在实际应用中使用规范中定义的内容的例子。

如果要学习Java语言,相信没有人会推荐从《Java语言规范》和《Java虚拟机规范》学起,因为刚开始接触Java的人没有必要把所有Java语言和Java虚拟机的细节都记下来。同样,对于之前没有接触过OSGi的读者,没有必要一字不漏地把本部分内容读完,这很枯燥。我们推荐初学者花上一、两个小时把本书第一部分的内容大致浏览一遍,然后迅速转入第三部分,配合第三部分的应用案例来学习会更有效率。如果读者此前已有基于OSGi的开发经验,或者已经阅读过本书第三部分,那么细读这部分OSGi规范内容将有助于了解OSGi的原理和完整的面貌。了解OSGi的规范和原理,是深入理解OSGi必不可少的过程。

2.1 OSGi规范概要

目前最新的OSGi规范是2012年7月发布的Release 5,Version5.0(后文简称为R5.0)版本,该规范定义了Java模块化系统所涉及的各种场景(开发、打包、部署、更新和交互等),以及其中用到的标准接口和参考模型。它是一份内容很全面、涉及范围很广泛的技术规范,从嵌入式系统到大型服务器系统,从模块的编码开发到部署使用,从OSGi核心框架到外围扩展服务都有专门的定义。

OSGi规范并不是单一的规范文档,而是由一系列子规范构成,这些子规范主要可分为两大部分,其中一部分用于描述OSGi的核心框架(OSGi Framework)。OSGi核心框架是一个可运行OSGi系统的最小集合,它由以下内容组成:

  • 执行环境(Execution Environment)。由于OSGi所适用的目标范围非常广泛,为了更好地处理不同硬件、软件环境对OSGi造成的兼容性问题,在建立其他约定之前,必须先定义好系统的执行环境。
  • 安全层(Security Layer)。描述了基于Java 2安全架构实现的代码验证、JAR文件数字签名、数字证书服务,安全层贯穿了OSGi框架的其他各个层次。
  • 模块层(Module Layer)。模块层从“静态”的角度描述了一个模块的元数据信息、执行环境定义、模块约束和解析过程、类加载顺序等内容。模块层是整个OSGi中最基础、最底层的层次。
  • 生命周期层(Life Cycle Layer)。生命周期层从“动态”的角度描述了一个模块从安装到被解析、启动、停止、更新、卸载的过程,以及在这些过程中的事件监听和上下文支持环境。
  • 服务层(Service Layer)。描述了如何定义、注册、导出、查找、监听和使用OSGi中的服务。服务层是所有OSGi标准服务的基础。
  • 框架API(Framework API)。由一系列通过Java语言实现的接口和常量类构成,为上面各层提供面向Java语言的编程接口。

构成OSGi规范的另外一部分内容是OSGi标准服务,这些标准服务试图以OSGi为基础,在软件开发的各种场景中(如配置管理、设备访问、处理网络请求等),建立一套标准服务和编程接口。软件开发所遇到的场景是多种多样、极其复杂的,因此OSGi对应定义的标准服务也非常庞大和复杂,OSGi所包含的数十个子规范大部分都用于定义这些标准服务。以下列举了一小部分较为常用的OSGi标准服务。

  • 事件服务(Event Admin Service)
  • 包管理服务(Package Admin Service)
  • 日志服务(Log Service)
  • 配置管理服务(Configuration Admin Service)
  • HTTP服务(HTTP Service)
  • 用户管理服务(User Admin Service)
  • 设备访问服务(Device Access Service)
  • IO连接器服务(IO Connector Service)
  • 声明式服务(Declarative Services)
  • 其他OSGi标准服务

大部分OSGi标准服务都没有写入OSGi核心(Core)规范之中,而是定义在OSGi服务纲要(Service Compendium)规范和企业级(Service Enterprise)规范之中。从上面对OSGi规范简要介绍我们可以总结出来,平时所说的“OSGi”大致包含了如图2-1所示的内容。

http://assets.osgi.com.cn/article/7289374/图2-1.jpg

图2-1 OSGi内容总览

虽然伴随着OSGi规范文档还发布了一些代码性质的内容,例如XML Schema定义和少量的JAR包,但是这些JAR包仅仅包含OSGi框架API及一些标准服务的接口。换句话说,仅仅靠这些随规范发布的代码是无法建立一个可运行的OSGi系统的。要让OSGi运行起来,还需要具体实现OSGi规范的程序才行,我们把这些程序称为“实现框架”或“OSGi实现”,如Eclipse的Equinox、Apache的Felix和Makewave的Knopflerfish等都是常见的OSGi实现。

2.2 Bundle

从本节开始,一直到本书结束,我们都会不断地提起“Bundle”这个词。Bundle是OSGi中最基本的单位,通俗地讲,如果说OSGi是基于Java平台的“模块化开发体系”,那么Bundle便是其中的“模块”。

OSGi中的Bundle是在JAR文件格式规范基础上扩展而来的,一个符合OSGi规范的Bundle首先必须是一个符合JAR文件格式规范的JAR包。与JAR文件格式兼容这点虽然没有太多技术含量可言,但是这个简单的举措极大地加速了OSGi的发展传播,它令OSGi的Bundle可以不经任何修改就直接应用于非OSGi的系统之中,也为将非OSGi的JAR包转换为可在OSGi系统运行的Bundle提供了很大的便利。

Bundle相对普通的JAR文件主要进行了以下三个方面扩展。

  • JAR文件格式规范里定义的/META-INF/MANIFEST.MF文件用于描述JAR包的元数据信息,如JAR包的版本、数字签名信息等,Bundle在MANIFEST.MF文件中添加了大量扩展定义,如描述该Bundle可以提供哪些资源、依赖哪些其他Bundle、启动或卸载时要执行哪些动作等,这部分内容我们会在2.3节中详细介绍。
  • 加入了一个可选的/OSGI-OPT文件夹,可以在其中保存一些与Bundle运行无关的信息,比如Bundle源码、软件说明书等。Bundle的使用者可以从中获取一些额外的信息,也可以安全地删除该文件夹,以节约OSGi系统的存储空间。
  • Bundle中可以包含一些具备特殊含义的程序和资源,如使用Bundle-Activator定义的初始化类、定义在OSGI-INF/l10n目录中的本地化信息等。

Fragment Bundle是一种特殊的Bundle,它无法独立存在,必须依附于某个其他的普通Bundle来使用,可以将它视为“Bundle的插件”、“模块中的模块”。

Fragment Bundle经常用来提供某些可选的功能,譬如为某个实现具体功能的Bundle提供一个中文语言包。有这个语言包,实现功能的Bundle能显示中文界面;在没有这个中文语言包时,实现功能的Bundle也能够正常使用。Fragment Bundle的另一项主要用途是隔离Bundle中经常变动的部分,譬如把系统的内部配置文件(开发模式还是生产模式、连接的数据库地址、调试级别等)集中在Fragment Bundle中,通过更换不同的Fragment Bundle来实现配置快速切换。

从静态角度(开发期)来看,Fragment Bundle与普通Bundle没有太大区别,它们都以JAR文件格式为基础,具备相同的元数据信息标记,标记的含义与设置方式也一样。区别仅仅是Fragment Bundle的元数据中会使用Fragment-Host标记说明它的宿主Bundle。

从动态角度(运行期)来看,Fragment Bundle与普通Bundle在运行时的处理差别却非常大,最重要的一点差异是Fragment Bundle不具备自己独立的类加载器。OSGi利用每个Bundle独立的类加载器互相协作来维护Bundle间导入、导出的依赖关系。没有类加载器,就无法直接与其他Bundle交互,必须依附于宿主,使用宿主Bundle的类加载器完成。关于这部分内容,我们在后面会有更详尽的介绍。

2.3 描述元数据

Bundle的元数据信息定义在/META-INF/MANIFEST.MF文件之中,OSGi规范中明确要求实现框架必须能够正确识别那些被预定义过的标记(在R5.0规范中预定义了28项标记),对于不可识别的标记以及不符合MANIFEST.MF标记格式的内容都要忽略且不能影响Bundle的正常解析。

2.3.1 预定义标记

以下列出了MANIFEST.MF文件中常用的预定义标记,除非特别说明,所列举的标记项都是可选的。对于其中某些较重要的标记(如Import-Package和Export-Package等),我们将在后续章节着重分析讲解。

(1)Bundle-ActivationPolicy

标记Bundle-ActivationPolicy设置Bundle的加载策略,该参数目前只有一个值:lazy,设置该参数后,Bundle将延迟激活,延迟至有其他的Bundle请求加载该Bundle中的类或资源时它才会被激活,如果不设置这个参数,那么Bundle启动时就会被激活。 示例: Bundle-ActivationPolicy: lazy

(2)Bundle-Activator

标记Bundle-Activator指明一个Activator 类,在Bundle启动和停止时会分别调用该类的start()和stop()方法,以便执行程序员所希望的动作,该类必须实现org.osgi.framework.BundleActivator接口。 Activator类通常用于在Bundle启动时注册和初始化服务,在Bundle卸载时注销这些服务。它很常用,但并不是必须的。 示例: Bundle-Activator: com.acme.fw.Activator

(3)Bundle-Category

标记Bundle-Category指明该Bundle的功能类别,可使用逗号分隔多个类别名称。这个功能类别仅供人工分类和阅读,OSGi框架并不会使用它。 示例: Bundle-Category: osgi, test, nursery

(4)Bundle-Classpath

标记Bundle-Classpath指明该Bundle所引用的类路径,该路径应为Bundle包内部的一个合法路径,如果有多个Classpath,使用逗号分隔。在介绍Bundle类加载过程时我们会详细介绍这个标记。 示例: Bundle-Classpath: /jar/http.jar,.

(5)Bundle-ContactAddress

标记Bundle-ContactAddress描述Bundle发行者的联系信息,仅供人工阅读,OSGi框架并不会使用它。 示例: Bundle-ContactAddress: 2400 Oswego Road, Austin, TX 74563

(6)Bundle-Copyright

标记Bundle-Copyright描述Bundle的版权信息,仅供人工阅读,OSGi框架并不会使用它。 示例: Bundle-Copyright: OSGi (c) 2002

(7)Bundle-Description

标记Bundle-Description给出关于该Bundle的简短描述信息,仅供人工阅读,OSGi框架并不会使用它。 示例: Bundle-Description: Network Firewall

(8)Bundle-DocURL

标记Bundle-DocURL给出该Bundle文档的链接地址,仅供人工阅读,OSGi框架并不会使用它。 示例: Bundle-DocURL: http:/www.acme.com/Firewall/doc

(9)Bundle-Icon

标记Bundle-Icon给出该Bundle的显示图标,图标应为一张正方形的图片,并通过参数size指出图标的宽度。OSGi规范要求实现框架至少要支持PNG图片格式。 示例: Bundle-Icon: /icons/acme-logo.png;size=64

(10)Bundle-License

标记Bundle-License给出该Bundle的授权协议信息。 示例: Bundle-License: http://www.opensource.org/licenses/jabberpl.php

(11)Bundle-Localization

标记Bundle-Localization给出该Bundle在不同语言系统下的本地化信息,如果不设置此标记,它的默认值为OSGI-INF/l10n/bundle。 示例: Bundle-Localization: OSGI-INF/l10n/bundle

(12)Bundle-ManifestVersion

标记Bundle-ManifestVersion指出该Bundle应遵循哪个版本的OSGi规范,默认值为1。对于OSGi R3规范,该值为1;对于OSGi R4/R5规范,该值为2。也可能在以后的OSGi规范中使用更高的数字,但现在仅允许将它设置为1或2。 示例: Bundle-ManifestVersion: 2

(13)Bundle-Name

标记Bundle-Name定义该Bundle的名称。注意该名称只供人工阅读,在Bundle-SymbolicName标记中定义的名称才会作为程序使用的Bundle的唯一标识来使用。根据一般开发习惯,Bundle-Name中所定义的名称会在打包发布时与Bundle-Version一起构成该Bundle的文件名,所以这个名称一般不含空格或其他不能在文件名中出现的字符。 示例: Bundle-Name: Firewall

(14)Bundle-NativeCode

如果Bundle中需要使用JNI加载其他语言实现的本地代码,那么必须使用Bundle-NativeCode标记进行说明。这个标记有如下附加参数:

  • osname:操作系统名称,如Windows等。
  • osversion:操作系统版本号,如3.1等。
  • processor:处理器指令集架构,如x86等。
  • language:遵循ISO编码的语言,如en,zh等。
  • seleciton-filter:选择过滤器,该值为一个过滤器表达式,指定被选中或未被选中的本地代码。 示例: Bundle-NativeCode: /lib/http.DLL; osname = QNX; osversion = 3.1

(15)Bundle-RequiredExecutionEnvironment

标记Bundle-RequiredExecutionEnvironment定义该Bundle所需的执行环境,支持多种执行环境的Bundle使用逗号分隔。OSGi在设计上就有非常广泛的应用范围,从嵌入式系统至大型服务器执行环境必然会有许多差异,因此在这个标记中需要指出该Bundle所适合的执行环境。在后续章节中我们还会继续介绍OSGi执行环境。 示例: Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0

(16)Bundle-SymbolicName

标记Bundle-SymbolicName给出该Bundle在OSGi容器中的全局唯一标识符。与其他可选标记不同,这个标记没有默认值,并且是Bundle元数据信息之中唯一一个必须设置的标记。程序将基于此标记和版本号在OSGi容器中定位到一个独一无二的Bundle。 当且仅当两个Bundle的Bundle-SymbolicName和Bundle-Version属性都相同的时候,它们才是完全相同的,不允许同时安装两个完全相同的Bundle到同一个OSGi容器之中。 Bundle-SymbolicName有以下两个附加参数。 singleton:表示Bundle是单例的。如果OSGi系统中同时存在两个Bundle-SymbolicName相同的(当然,要求Bundle-Version不相同,否则是不可能同时存在的)单例Bundle,那么仅有其中一个会被解析。如果其中一个没有声明为单例Bundle,则不会受到另外一个单例Bundle的影响,默认值为false。 fragment-attachment:定义Fragment Bundle是否能附加到该Bundle之上。允许值为always、never和resolve-time,含义为允许附加、禁止附加和只允许在解析过程中附加,默认值为always,即允许附加。 示例: Bundle-SymbolicName: com.acme.daffy

(17)Bundle-UpdateLocation

标记Bundle-UpdateLocation给出Bundle的网络更新地址。如果Bundle需要更新版本,将使用这个地址。 示例: Bundle-UpdateLocation: http://www.acme.com/Firewall/bundle.jar

(18)Bundle-Vendor

标记Bundle-Vendor给出该Bundle的发行者信息。 示例: Bundle-Vendor: OSGi Alliance

(19)Bundle-Version

标记Bundle-Version给出该Bundle的版本信息,默认值为“0.0.0”。注意,这项信息并不是仅供人工阅读的,“版本”在OSGi中是一项受系统管理的信息。维护一个Bundle的不同版本也是运行OSGi框架的重要特征之一,当一个Bundle依赖另一个Bundle时,经常需要指明它依赖的是什么版本范围内的Bundle。 版本号是有序的,在Symbolic-Name相同的前提下,两个Bundle的版本可比较大小。完整的版本号会由“主版本号(Major)”+“副版本号(Minor)”+“微版本号(Micro)”+“限定字符串(Qualifier)”构成。 示例: Bundle-Version: 22.3.58.build-345678 根据一般的开发习惯,上述4项版本号约定俗成地表示如下含义。

  • 主版本号:表示与之前版本不兼容的重大功能升级。
  • 副版本号:表示与之前版本兼容,但可能提供新的特性或接口。
  • 微版本号:表示API接口没有变化,只是内部实现改变,或者修正了错误。
  • 限定字符串:通常用于表示编译时间戳或者编译次数。

在比较版本大小时,从前往后逐项(含限定字符串)进行比较,当且仅当4个比较项都对应相等,两个Bundle的版本才相等,否则以第一个出现差异的版本号的大小决定整个Bundle版本的大小。 示例: 1.2.3 < 3.2.1 < 4.0 有一点必须注意,对于限定字符串的处理,OSGi和Maven是恰恰相反的,在Maven里,版本“1.2.3.2012”<=“1.2.3”,但在OSGi里则是版本“1.2.3.2012”>=“1.2.3”。

(20)DynamicImport-Package

标记Dynamic Import-Package描述运行时动态导入的Package。Package的导入和导出构成了OSGi多模块之间的组织协作关系,这是一个相对复杂而又很重要的内容,我们将在后续章节中专门介绍。 示例: DynamicImport-Package: com.acme.plugin.*

(21)Export-Package

标记Export-Package描述被导出的Package。导入导出Package是模块层的核心功能,该内容将在后续章节中具体介绍。 示例: Export-Package: org.osgi.util.tracker;version=1.3

(22)Export-Service

标记Export-Service描述被导出的服务,这个标记在OSGi规范中已经被声明为Deprecated了,不推荐继续使用此标记。 示例: Export-Service: org.osgi.service.log.LogService

(23)Fragment-Host

当该Bundle是一个Fragment Bundle时,标记Fragment-Host指明它的宿主Bundle。 示例: Fragment-Host: org.eclipse.swt; bundle-version="[3.0.0,4.0.0)"

(24)Import-Package

标记Import-Package描述该Bundle需要导入的Package。导入导出Package是模块层的核心功能,该内容将在后续章节中具体介绍。 示例: Import-Package:org.osgi.service.io;version=1.4

(25)Import-Service

标记Import-Service描述导入的服务。这个标记在OSGi规范中已经被声明为Deprecated了,不推荐继续使用此标记。 示例: Import-Service: org.osgi.service.log.LogService

(26)Provided-Capability

标记Provided-Capability描述该Bundle提供的服务特性(Capability)。服务特性是在OSGi R4.3规范中加入的新概念。在此之前,Bundle只能通过Bundle-RequiredExecution-Environment来声明所需的执行环境;但是在某些场景下一些执行环境特性是由其他Bundle提供的,这样依赖运行环境来描述所需特性就受到限制了。因此在R4.3规范中加入了Provided-Capability和Require-Capability来声明Bundle所需要和能够提供的特性。 示例: Provided-Capability: com.acme.dict; from=nl; to=de; version:Version=1.2

(27)Require-Capability

标记Require-Capability描述该Bundle所需要的服务特性。 示例: Require-Capability: osgi.ee; filter:="(&(osgi.ee=AcmeMin)(version=1.1))"

(28)Require-Bundle

标记Require-Bundle描述该Bundle所依赖的其他Bundle,一旦声明了依赖某个Bundle,就意味着可以直接使用所有从这个Bundle中导出的Package。 示例: Require-Bundle: com.acme.chess

2.3.2 使用可视化工具

记录Bundle元数据的MANIFEST.MF文件是一个纯文本文件,这意味着手工编辑它是完全可行的。不过这样很繁琐且易于出错,在实际开发中一般不会手工去做,而通常会使用一些可视化的工具来配置Bundle的元数据信息。本节将介绍如何在Eclipse IDE下配置Bundle的元数据信息。

普通的Java工程是不存在OSGi元数据标记的,因此要先在Eclipse中建立一个“Plug-in Project”,如图2-2所示。

http://assets.osgi.com.cn/article/7289374/图2-2.jpg

图2-2 新建工程

在New Project中选中“Plug-in Project”,单击“Next”后出现如图2-3所示的界面。

http://assets.osgi.com.cn/article/7289374/图2-3.jpg

图2-3 设置目标平台

从图2-3中可见,新建Plug-in工程的界面与新建普通Java工程界面的主要不同是增加了“Target Platform”项。如果要开发的是运行在Eclipse之上的插件,那么在此选择插件所支持的Eclipse版本号,系统会根据不同的Eclipse版本所支持的API为插件建立可用的目标平台。

如果准备开发一个标准的OSGi Bundle,那么应该选择“an OSGi framework”,后面的下拉框有“standard”和“Equinox”两个选项。它们的区别是当选择standard时,只能使用OSGi规范所定义的标准API,这样建立出来的OSGi Bundle在任何一个符合OSGi规范的实现中都能执行;而选择Equinox时,除了可以使用OSGi的标准API外,还可以使用Equinox框架自己的扩展功能,譬如专有API、扩展点机制、Buddy加载器和P2更新平台等,不过这样建立的OSGi Bundle只能运行于Equinox框架之上,无法在Felix、Knopflerfish等其他OSGi实现框架上运行。

就本书的例子来说,除了第三、四部分中专门介绍Equinox框架特有功能的例子外,其他各部分内容都可以选择“standard”,通过标准的OSGi框架API来完成。

继续单击“Next”按钮,将出现如图2-4所示的“Content”对话框。

http://assets.osgi.com.cn/article/7289374/图2-4.jpg

图2-4 设置工程基本信息

在这个对话框中可以设置一些最基本的Bundle元数据信息,其中各输入项与前面元数据标记的对应如下:

  • ID对应于Bundle-SymbolicName
  • Version对应于Bundle-Version
  • Name对应于Bundle-Name
  • Provider对应于Bundle-Vendor
  • Execution Environment对应于Bundle-RequiredExecutionEnvironment
  • Activator对应于Bundle-Activator

在这里可以直接单击“Finish”按钮,之后出现如图2-5所示的MANIFEST.MF编辑界面,这个界面可以作为可视化工具对Bundle的元数据进行编辑。

http://assets.osgi.com.cn/article/7289374/图2-5.jpg

图2-5 Bundle元数据编辑界面

这个元数据编辑界面中有Overview、Dependencies、Runtime、Build四个面板,这四个面板之后的MANIFEST.MF和build.properties显示对应文件的文本内容编辑器。无论是使用面板进行可视化操作,还是直接用文本编辑的方式修改MANIFEST.MF文件的内容,这两种修改方式都是等效的且内容会互相同步。

可以使用Overview面板来编辑Bundle的基础信息(在图2-4中设置的那些信息)及测试、打包Bundle;使用Dependencies面板来配置Bundle的依赖关系(元数据中的Import-Package和Require-Bundle标记);使用Runtime面板来配置Bundle的导出Package和该Bundle的Classpath(元数据中的Export-Package和Bundle-Classpath标记)。如果在这两个面板中修改了Bundle的依赖关系,那么Eclipse会自动同步更新工程的“.classpath”文件,以便用户编码时能使用依赖包中提供的API;还可以使用Build面板来配置打包发布工程时是否需要发布源码、说明文档和其他资源等细节信息。

在这个例子中,工具生成的元数据如下:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Bundle111
Bundle-SymbolicName: org.fenixsoft.osgi.bundle
Bundle-Version: 1.0.0.qualifier
Bundle-Activator: org.fenixsoft.osgi.bundle.Activator
Import-Package: org.osgi.framework;version="1.3.0"
Bundle-RequiredExecutionEnvironment: JavaSE-1.6

2.4 Bundle的组织与依赖

既然是以模块化方式开发一个系统,那么必不可少的步骤是根据业务和技术的需要,将系统划分为多个模块,通过这些模块互相协作完成系统的功能。系统中绝大部分模块都不是孤立的,通常会依赖其他模块所导出的某些Package,又会被另外一些模块所依赖。这种依赖关系在元数据配置中简单体现为Import-Package、Export-Package和Require-Bundle标记的配置,使用起来并不算复杂,但是各个OSGi框架的实现者都要花费大量心思在组织Bundle与管理依赖上,如何查找最合适的Bundle、如何处理循环依赖关系、如何导入导出Bundle中的类和资源等一系列问题都是需要OSGi框架去解决的。

2.4.1 导出和导入Package

Export-Package用于声明Bundle要导出哪些Package,Import-Package用于声明Bundle需要导入哪些Package。这两个标记最简单的方式是直接跟随导入或导出的Package名称,如果导入或导出多个Package,则使用逗号分隔,如下所示: Export-Package: org.osgi.service.io, org.osgi.service.web Import-Package: org.osgi.service.io, org.osgi.service.web

这种写法是OSGi中导入导出Package的基本用法,也是最常见用法,已经能满足相当多的应用场景,但是在开发过程中总会遇见各种不同的需求,要根据特定规则去选择适合的Package。下面对OSGi规范中定义的几种对导入导出进行筛选的过滤方式进行介绍。

1.根据类名过滤

如果仅在Package层次上控制导出的粒度不够精细,无法满足应用需求,那么可以使用附加参数include和exclude进一步描述要导出Package中哪一部分内容。比如,只希望导出org.osgi.service.io包中命名以“IO”开头的接口,不导出实现类(假设实现类都以Impl结尾),那么可以这样写: Export-Package: org.osgi.service.io;include="IO"; exclude:="Impl"

include和exclude参数的具体使用方法如下:

  • 附加参数include用于列举需要导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只有符合规则的类才会被导出。
  • 附加参数exclude用于列举禁止导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只要符合规则的类就不会被导出。

include和exclude的限制是在导出时处理的,导入时无需对应用做任何特殊声明,Import-Package标记也无法与这两个参数搭配使用。例如与前面配对的导入声明依然为: Import-Package: org.osgi.service.io

2.根据版本过滤

在OSGi系统中,同一个名称的Package可能存在多个不同版本。假设Bundle C开发时引入了Spring 2.0版的Package,并且使用了某些只在这个版本私有的特征,而Bundle D开发时使用的是Spring 3.0版的Package,那么从这个系统中导出Spring的Bundle就必须明确指明Spring的版本号,以便导入时区分。示例如下:

Bundle A(导出Spring 2.0):
Export-Package: org.springframework.core; version="2.0.0"
Bundle B(导出Spring 3.0):
Export-Package: org.springframework.core; version="3.0.0"

对应的,导入时也要指明版本,准确地说应指明某个版本范围,例如:

Bundle C(导入Spring 2.x版):
Import-Package: org.springframework.core; version="[2.0.0,2.9.9]"
Bundle D(导入Spring 3.0以上版本):
Import-Package: org.springframework.core; version="3.0.0"

这里要注意我们导入的是“版本范围”而不是某个具体的“版本”,例如示例中Bundle D的Import-Package写法,只声明了version="3.0.0"的含义并不是“只导入”3.0.0版本的Package,而是导入3.0.0或以上版本的Package,因为这更符合一般的开发使用场景。如果需要指定只导入3.0.0版的Package,需要这样写:

Bundle E(只导入Spring 3.0版本):
Import-Package: org.springframework.core; version="[3.0.0,3.0.0]"

version参数的具体使用方法是:在导出Package时,此参数声明导出Package的版本号,如果不设置,默认值为0.0.0;在导入Package时,此参数声明要导入Package的版本范围,如果不设置,默认值为[0.0.0, ∞)。在声明版本范围时,方括号“[”和“]”表示“范围包含此数值”,圆括号“(”和“)”表示“范围不含此数值”。

在OSGi R3.x及之前的版本中,version标记原本叫做specification-version,在R4.x规范中新增了version标记,但依然保留了specification-version这个别名,但是已将它声明为Deprecated,如果同时设置了version和specification-version的值,那么这两个值必须相等。

3.根据提供者过滤

OSGi还允许开发人员根据Bundle名称、版本等信息来过滤Package,这种过滤方式在规范中被称为“选择提供者(Provider Selection)”。由开发人员明确指明导入的Package必须来自某个提供者的做法在实际开发之中是很少见的,它会增加系统的兼容性风险和人为的不确定因素。就如同我们组装电脑选择显示卡一样,理性的选择方式是根据性能需求确定显示芯片、显存大小和位宽等参数,再来选择合适的产品,而不是明确要求必须选择某公司出产的某型号产品。

根据提供者过滤一般使用在测试Bundle的时候,Import-Package标记提供了两个附加参数bundle-symbolic-name和bundle-version来完成这项功能,它们分别用于对Bundle的名称和版本进行过滤,示例如下:

Bundle A:
Bundle-SymbolicName: BundleImport-Package: com.acme.foo; bundle-symbolic-name="BundleB";
bundle-version="[1.4.1,2.0.0)"

Bundle B
Bundle-SymbolicName: BundleB
Bundle-Version: 1.4.1
Export-Package: com.acme.foo

上面配置指明一定要是来自于名称为“BundleB”并且版本在1.4.1至2.0.0之间的com.acme.foo包才会被选择。

bundle-symbolic-name和bundle-version参数的具体使用方法如下。

  • 附加参数bundle-symbolic-name:参数值为某个Bundle的名称,只有符合该名称的Bundle所导出的Package才会被导入。
  • 附加参数bundle-version:参数值为导入Bundle(注意不是Package)的版本范围,只有版本在该范围之内的Bundle所导出的Package才会被导入。

4.根据属性过滤

导入和导出的Package除了使用include、exclude对类名进行过滤,使用version对版本进行过滤和使用bundle-symbolic-name、bundle-version对提供者信息进行过滤外,还有第四种方式:使用自定义的扩展属性进行过滤。包名、类名和版本这几项信息都是很客观的数据,在代码开发完成那一刻就已确定下来,不可能随意改变。而自定义的扩展属性可以满足某些根据开发人员自己加入的属性进行过滤的需求。示例如下: Export-Package: org.osgi.simple;filter="true";

在导出org.osgi.simple时加入了自定义属性filter,它的值为“true”(属性值按照字符串处理),那下面三句Import-Package中,只有第Bundle B、C可以成功导入前面发布的org.osgi.simple包,而Bundle A中由于自定义属性冲突导致而导致匹配失败,示例如下:

Bundle A:
Import-Package: org.osgi.simple; filter="false"

Bundle B:
Import-Package: org.osgi.simple; filter="true"

Bundle C
Import-Package: org.osgi.simple;

注意,Bundle C虽然没有声明自定义属性filter,但在默认情况下这并不会产生匹配冲突。如果要改变这种情况,可以使用mandatory附加参数,强制要求必须存在扩展属性才能成功匹配,示例如下: Export-Package: org.osgi.simple; filter="true";mandatory:="filter"

这样,在下面两句Import-Package中,只有第一句能成功导入前面发布的“org.osgi.simple”包,因为它发布时filter属性被声明为“必须”的,示例如下: Bundle A: Import-Package: org.osgi.simple; filter="true"

Bundle B:
Import-Package: org.osgi.simple

如果没有在mandatory中指定属性名称,那这种属性被默认为“optional”,即可选的,在导入时没有声明这个属性也不影响正常导入。

mandatory参数的具体使用方法:只适用于Export-Package,用于声明哪些属性是必选的,只有导入Package时提供了必选的属性,才能正确匹配到导出的Package。多个属性用逗号分隔,用双引号包裹。

5.可选导入与动态导入

在大多数情况下,导入某个Package,就说明当前这个Bundle的正常运行是必须依赖导入的Package的,比如一个基于Spring开发的程序,没有导入Spring的Package就肯定无法运行。但还有另外一些场景,导入某个Package是为了实现一些不影响Bundle正常运行的附加功能,比如为某个英文软件开发了一个实现中文语言支持的插件,不导入这样的Package也不应当影响整个系统正常运行,只不过软件仍以英文形式显示而已。

Import-Package标记也可以很好地支持上述需求,它有一个名为resolution的附加参数,用于说明一个依赖的Package是否是必需的。示例如下:

Bundle A:
Import-Package: org.osgi.simple;resolution:="mandatory"

Bundle B:
Import-Package: org.osgi.simple

Bundle C:
Import-Package: org.osgi.simple; resolution:="optional"

在上面例子中,Bundle A和Bundle B的Import-Package是等价的,因为resolution的默认值就是mandatory。在这个设置下,如果没有任何Bundle导出org.osgi.simple包,那么Bundle A 和Bundle B将会解析失败。而Bundle C不会,因为它明确指出了这个Package的导入是可选的(resolution:="optional")。

resolution参数的具体使用方法是这样的:附加参数resolution只适用于Import-Package标记,用于确定某个Package是可选的还是必须的。

还有一种场景,可能某个被导入的Package确实是必需的,但是提供这个Package的Bundle并不会在系统启动时就被安装。在这种情况下,只有真正使用到这个Package的类时,才会去导入这个Package,而不是在启动时就查找是否有提供Package的Bundle存在。

DynamicImport-Package标记可以处理这样的需求,这个标记的语义和Import-Package标记很类似,不同点在于DynamicImport-Package标记不在Bundle的解析阶段进行处理,无论它要求导入的Package是否存在,都不会影响Bundle的解析,示例如下:

Bundle A:
Import-Package: org.osgi.simple

Bundle B:
DynamicImport-Package: org.osgi.simple

如果没有任何Bundle提供org.osgi.simple包,那么Bundle A将无法解析,而Bundle B不受影响;但是如果真正使用到org.osgi.simple中的类时还是没有任何Bundle可提供,那么Bundle B依然要抛出ClassNotFoundException异常。

DynamicImport-Package也可是使用version附加参数来过滤导入Package的版本,在导入Package时声明属性,以便符合Package导出时mandatory参数中声明必须存在属性。

与Import-Package有所不同的是,DynamicImport-Package可以使用通配符,举一个极端的例子: DynamicImport-Package: *

如果某个Bundle的元数据信息中有上面这行定义,那么它就成了一个“管理员级别”的Bundle,它可以访问整个系统中所有被导出过的Package。这样做很方便,却违背了OSGi中提倡的封装与按需使用的原则,会带来许多遗患,因此这并不是一种好的使用方式。

动态导入和可选导入实现的功能有些类似,它们的共同特征是在Bundle解析期间即使找不到要导入的依赖,也不会导致解析失败。它们的区别是,动态导入每次加载包中的类都会尝试去查找动态导入包,而可选导入包只有在Bundle解析时才进行连接尝试。

6.导出Package的依赖限制

如果导出Package的全部依赖都集中在一个Bundle之中,那么这个Package的导出是完全不受限制的。但是如果要导出的Package中有某些类还依赖于其他Bundle所提供的类,比如从其他Bundle所导出的Package中的类继承,或者其他Bundle的类出现在方法的声明中,在这种情况下,要导出这个Package就会受到依赖限制,必须先保证依赖的Package是可用的,才能保证导出的Package是可用的。

这种Package之间的关系可以通过在Export-Package标记中的uses附加参数来描述。例如:包org.osgi.service.http使用了包javax.servlet.http中的API,因此这两个包存在依赖关系。在导出org.osgi.service.http的Export-Package标记中就应当包含值为javax.servlet的uses参数,如下所示: Export-Package: org.osgi.service.http;uses:="javax.servlet.http"

当一个系统中同时存在不同版本的Package时,uses参数对于协调依赖关系很有用。比如上面例子中的javax.servlet.http包同时存在2.4和2.1两个版本,而Bundle A导入的是2.4版,如下所示:

Bundle A:
Export-Package: org.osgi.service.http;uses:="javax.servlet.http"
Import-Package:javax.servlet.http; version="2.4"

对于任意一个Bundle,只要导入Bunlde A中发布org.osgi.service.http包,同时又导入了javax.servlet.http包,即使在导入的时候不明确指明其版本,OSGi实现也必须保证它只会使用2.4版的javax.servlet.http,如下所示: Bundle B: Import-Package:org.osgi.service.http // 由Bundle A提供 ,javax.servlet.http // 这里默认会导入2.4版的Package

uses参数的具体使用方法是:附加参数uses只适用于Export-Package标记,用于说明导出Package的依赖关系。如果同时依赖多个Package,使用逗号分隔,并用双引号包裹。

当OSGi容器中的Bundle不断增加,依赖关系逐渐变得复杂时,uses参数就是协调依赖的必要手段,也是配置OSGi依赖的难点之一。在2.4.2节中我们还会对如何使用uses参数协调Package依赖约束进行更详细介绍。

7.导入整个Bundle

前面几项介绍的都是Package级别的导入、导出和依赖,OSGi还可以支持Bundle级别的依赖关系。我们可以使用Require-Bundle标记来声明要导入这个Bundle,如果成功导入了这个Bundle,那就意味着导入了这个Bundle中所有声明为导出的Package。

Require-Bundle标记后面应跟随一个或多个其他Bundle的名称(由Bundle-SymbolicName定义的名称),多个名称之间使用逗号分隔,如下所示:

Require-Bundle:BundleA、BundleB、BundleC

如果导入了某个Bundle,就可以重复导出该Bundle中已导出过的Package,就如同导出自己的Package一样,例子如下所示:

Bundle A:
Require-Bundle: BundleB
Export-Package: p

Bundle B:
Export-Package: p;partial=true;mandatory:=partial

这时如果有Bundle C要导入Package p,而又没有声明属性partial,那它将会从Bundle A中导入Package p。实际上Bundle A只承担了转发的工作,真正的Package p在Bundle B之中。这种情况属于拆分包的一个特例,应尽可能避免。

注意:拆分包(Split Packages)是指OSGi容器中有两个或两个以上的Bundle导出了相同名称的Package。容器存在拆分包会令包导入过程变得复杂,因为只带有包名的Import-Package标识无法保证能正确导入到所需的Package。必须通过过滤或者使用Require-Bundle才能导入正确的Package。

与Import-Package类似,Require-Bundle标记也有附加参数bundle-version和resolution。bundle-version用于过滤导入Bundle的版本,默认值为[0.0.0,∞),即不限制版本范围。resolution用于确定导入的Bundle是否是必需的,可选值为mandatory和optional,默认值为mandatory,含义与Import-Package标记的resolution参数相同,这里就不再详细举例介绍了。

在默认设置下,导入了某个Bundle,仅表示在本Bundle中可以访问被导入Bundle中声明导出的Package,除非明确用Export-Package声明过,否则这些Package在本Bundle中默认不会再次被导出。如果有必要,可以使用Import-Package标记的visibility附加参数来改变这种行为,示例如下:

Bundle Require-Bundle: BundleB;visibility:=reexport

Bundle B
Export-Package: org.osgi.service.http

在上面例子中由于明确设置了visibility的值为reexport,Bundle A中就会重复导出Bundle B的声明导出的包,即org.osgi.service.http。

visibility参数的具体使用方法是:附加参数visibility仅用于Require-Bundle标记,描述来自被导入Bundle中的Package是否要在导入Bundle中重新导出。默认值为private,代表不会在导入Bundle中重新导出。如果值为reexport,则说明需要重新导出。

在元数据信息中可以同时使用Import-Package和Require-Bundle标记来获取Bundle所需的依赖包,但是如果某个依赖的Package同时在Import-Package列表和Require-Bundle的Bundle中存在,那么OSGi实现框架必须保证要以Import-Package列表中的包优先。

使用Require-Bundle有时候确实会获得一些便利,但从长远来看,依赖的粒度越小越好。依靠Require-Bundle来处理依赖关系并非一种好的开发实践,甚至可能会带来一些令人头痛的问题,我们将在2.4.2节中通过实际例子来介绍Require-Bundle的缺陷。

2.4.2 约束规则与示例

导入和导出Package并不总是一帆风顺的,随着系统复杂性的增加,模块数量不断变多,依赖关系随之变得越来越错综复杂,尤其有拆分包或在多个不同版本的Package同时存在和出现循环依赖等情况时,理解依赖的约束规则就很重要了,本节将介绍这方面的内容。

1.图示

在本书后续章节中,我们将会以图示来表示各个Bundle、Package以及它们之间的依赖关系,如图2-6所示。这些图示最初定义在OSGi规范之中(除了最后一个“解析失败的Bundle”,这个图示是笔者为了讲解方便自行定义的),因此在本书之外的其他OSGi文档也会遇到类似的图示。

http://assets.osgi.com.cn/article/7289374/图2-6.jpg

图2-6 OSGi标准图示与标记

下面是一个如何使用图示来表示Bundle、Package及其依赖关系的例子,假设存在A、B、C三个Bundle,其定义如下所示:

Bundle A:
Import-Package: p; version="[1,2)"
Export-Package: q; version=2.2.2; uses:=p
Require-Bundle: C

Bundle B
Export-Package: p; version=1.5.1

Bundle C
Export-Package: r

这3个Bundle以及它们之间的依赖关系可以使用图2-7来表示。

http://assets.osgi.com.cn/article/7289374/图2-7.jpg

图2-7 图示示例

2.多版本Package依赖选择

前面介绍过如何使用Export-Package标记uses附加参数来协调在同一个OSGi系统中选择多个不同版本Package的问题。本节我们继续沿用前面的例子深化理解多版本Package共存时的约束。Bundle A、B、C、D的定义如下:

Bundle A:
Import-Package:org.osgi.service.http
Import-Package:javax.servlet.http

Bundle B:
Export-Package: org.osgi.service.http;uses:="javax.servlet.http";
Import-Package:javax.servlet.http; version="2.4"

Bundle C:
Export-Package: javax.servlet.http; version="2.1"

Bundle D
Export-Package: javax.servlet.http; version="2.4"

对于Bundle A来说,由于没有指明所需要的javax.servlet.http的版本号,Bundle C和Bundle D对它来说都是满足需求的。但是它依赖的org.osgi.service.http来自于Bundle B之中,Bundle B明确声明了org.osgi.service.http包要用到版本为2.4的javax.servlet.http的类,在这层隐含限制下,Bundle A就不允许从Bundle C获取2.1版的javax.servlet.http了,如图2-8所示。

http://assets.osgi.com.cn/article/7289374/图2-8.jpg

图2-8 多版本Package依赖选择示例(1)

如果Bundle A在导入javax.servlet.http的时候声明了只支持版本2.1(在导入时加入version="[2.1.0,2.1.0]"),其他条件不变,那么就会发生版本冲突,Bundle A在这种设置下是无法解析通过的,但是不会影响到Bundle B、Bundle C和Bundle D,它们依然能够解析成功,如图2-9所示。

http://assets.osgi.com.cn/article/7289374/图2-9.jpg

图2-9 多版本Package依赖选择示例(2)

遇到上述Bundle解析问题,OSGi实现框架会抛出一个BundleException异常,可能的异常信息(根据实现框架而定,这里以Equinox框架为例)类似如下所示:

org.osgi.framework.BundleException: 
The bundle "BundleA_1.0.0.qualifier [8]" could not be resolved. Reason: Package 
uses conflict: Import-Package: org.osgi.service.http; version="0.0.0"

再把场景修改一下,假设在Bundle B导出org.osgi.service.http时没有使用uses参数,在实际代码之中org.osgi.service.http也不会依赖javax.servlet.http中的类,其他条件不变,那么Bundle A、B、C、D全部能正常共存。因为Bundle A认为在使用org.osgi.service.http时不会遇到任何与javax.servlet.http有依赖关系的类,这样即使Bundle B中有其他的包需要2.4版的javax.servlet.http支持,也对Bundle A没有任何影响,因为根本不会使用到uses参数,如图2-10所示(注意Bundle B中已经没有了表示uses参数的图示)。

http://assets.osgi.com.cn/article/7289374/图2-10.jpg

图2-10 多版本Package依赖选择示例(3)

我们继续假设,如果仅在元数据信息中没有使用uses参数描述出org.osgi.service.http对javax.servlet.http的依赖关系,而在实际代码之中它们却有依赖关系,那会怎样呢?OSGi规范并未说明这种元数据与实际代码不符的场景应当如何处理,因此这时不同实现框架表现出来的行为是不可预测的。在Equinox的实现中,Bundle A在实际使用到Bundle B发布出来的org.osgi.service.http中的类时并不会报错,但是没有遵循Bundle B的定义采用2.4版的javax.servlet.http包,而是采用了在Bundle A中引用的2.1版。

3.相同Package循环导出

我们知道,即使不考虑OSGi环境,在普通Java系统中,一个Package中的各个类也可以分布于不同JAR包中,这种情况很常见。因此OSGi必须允许多个Bundle导出相同的Package,即出现拆分包的情况。但是如果不加约束,在某些场景中会出现一些逻辑矛盾,例如接下来要介绍的相同Package循环导出的例子。Bundle A、B、C、D定义如下:

Bundle A:
Require-Bundle: BundleB, BundleC
Export-Package: p

Bundle C:
Require-Bundle: BundleD
Export-Package: p

Bundle B
Export-Package: p

Bundle D:
Export-Package: p

相同Package循环导出的图形化描述如图2-11所示。

http://assets.osgi.com.cn/article/7289374/图2-11.jpg

图2-11 相同Package循环导出(1)

这4个Bundle都共同导出了一个包p,如果这时某个Bundle向Bundle A请求加载p中的类,Bundle A会根据深度优先的规则去查找这个类(OSGi的类加载器规则决定了加载请求优先委派给导入Bundle的类加载器,后面我们再详细介绍这一点),即查找顺序是:B->D->C->A。在这个基础上,再让Bundle D依赖Bundle A(加入Require-Bundle: BundleA),即变为如图2-12所示的状态。

http://assets.osgi.com.cn/article/7289374/图2-12.jpg

图2-12 相同Package循环导出(2)

在这种场景下,Bundle A、C、D构成了一个循环,如果仍然按照深度优先搜索,就会出现B->D->C->A->B->D->C->A->……的无限循环。为了避免出现这样的循环,OSGi规范明确要求实现框架必须对每个搜索过的Bundle记录状态,保证每个Bundle只被搜索一次。因此对于图2-11所示的情况,搜索顺序依然是B->D->C->A。

4.Require-Bundle带来的问题

OSGi提倡使用Import-Package和Export-Package来构成模块之间的依赖关系,不提倡使用Require-Bundle,从实践经验来说,依赖的粒度总是越小越好。Bundle级别的导入还可能会由于Package重复导入和导出而带来一系列逻辑缺陷和性能问题。

对于“拆分包”的场景——如果Bundle A把它导入的Bundle B中某个Package使用的Export-Package重新导出就会形成拆分包,假如这时如果通过Bundle A加载这个Package,类搜索的路径就会变得更长,类加载器中抛出并接住ClassNotFoundException异常的次数会变得更多,这样就会带来额外的性能开销。

另外,Require-Bundle会让导出Package的过程变得更为复杂,可能导致Bundle开发人员不可预料地声明改变。因为在Require-Bundle中的声明的visibility可能会令Bundle的调用者无所适从。例如,有如下定义:

Bundle A: 
Require-Bundle: BundleB;visibility:=reexport, BundleC;visibility:=private

Bundle B: 
Export-Package: p

Bundle C: 
Export-Package: p

上面的定义会导致Bundle A只导出“一部分”的Package p。根据定义内容,Bundle A由于在导入Bundle B时声明了“visibility:=reexport”,暗示了它会重复导出包p,但这又与Bundle A直接声明“Export-Package p”有所不同。当Bundle A收到类查找的请求时,类查找顺序应为B->C->A,如果Bundle B中的Package p确实有这个类,那么通过visibility:=reexport还是Export-Package p实现导出并没有区别;但是如果Bundle B中的Package p没有请求的类,被请求的类在Bundle A中或在者Bundle C的Package p中,那么这时OSGi框架就会拒绝提供这些类,这是因为这些类在未被导出的私有Package中或在明确声明为visibility:=private的Bundle中。因此Bundle A只导出了“一部分”的Package p。这种导出的复杂性从Bundle A、B、C内部定义来还能够解释得过去,但是从在外部调用者的角度看,Bundle A只导出“一部分”的Package p就显得很别扭了。上面示例对应的图形表述如图2-13所示。

http://assets.osgi.com.cn/article/7289374/图2-13.jpg

图2-13 Require-Bundle导致的Package导出问题

2.4.3 校验Bundle有效性

要确定某个JAR包是否是一个合法的OSGi Bundle,首先要根据其元数据信息中的Bundle-ManifestVersion(注意,不是ManifestVersion,ManifestVersion是JAR文件规范中定义的)来确定元数据信息的版本号。如果在元数据信息中没有指定Bundle-ManifestVersion,那么其默认属性是1,表示遵循OSGi R3规范;如果Bundle-ManifestVersion为2,则代表遵循OSGi R4/R5规范;OSGi R4规范中添加了一些特定的标记。在将Bundle安装到OSGi系统的时候,必须对其进行有效性校验。下面列举一些(不完全包括)会导致Bundle有效性校验失败的问题。

  • 无法根据Bundle-RequireExecutionEnvironment的值找到一个可匹配执行环境。
  • 在Bundle中没有设置Bundle-SymbolicName标记。
  • 存在重复的标记。
  • 对某个Package重复导入。
  • Bundle导入或导出了以java.*开头的Package。
  • 在Export-Package标记中用mandatory参数指明了某些强制属性,却没有定义这些属性。
  • 尝试安装某个和OSGi框架中某个已经安装好的Bundle具有同样名称和版本的Bundle。
  • 将某个Bundle更新为和OSGi框架中一个已经安装好的Bundle具有同样名称和版本的Bundle。
  • 元数据信息中存在语法错误(例如,不合法的版本格式或Bundle名称)。
  • 同时使用Specification-version和version参数,但是为它们指定了不一样的值,例如:

    Import-Package p;specification-version=1;version=2
    

    这将导致一个错误。而下面的指定:

    Import-Package p;specification-version=1, q;version=2
    

    不会导致错误。

  • 在元数据信息中列出了权限文件:OSGI-INF/permission.perm,但此文件不存在。

  • Bundle-ManifestVersion的值不等于1或2,在今后的OSGi规范中另有规定的情况除外。

wmz 2015-11-10 15:16

http://osgi.jxtech.net/

顶(6) 踩(0) 回复
查看评论