1. OSGi历史简介
1.1 起源背景
1998年,在Sun公司的推动下成立了JCP(Java Community Process)组织,这个组织负责制定Java的技术标准,发布技术标准的参考实现(RI)和用于检验标准的技术兼容包(TCK)。JCP允许任何组织或个人对Java的演进路线提出建议,这些建议在审查立项后会以JSR(Java Specification Requests)的形式形成正式的规范提案文档;在经过专家组审核和执行委员会投票通过之后,这些JSR文档就将成为正式的Java技术规范。OSGi源于JSR-8(Open Services Gateway Specification,开放服务网关规范),这是一份由Sun发起并主导(共同发起的还有IBM、Ericsson和Sybase等公司),在1999年3月提交到JCP的规范提案。这份规范定义了不同设备之间服务互相依赖和调用的交互接口。不过,JSR-8提案在1999年5月,即提交之后的2个月就被发起者撤回。撤回并不是因为这份JSR不够资格成为Java规范发布,主要是发起者希望另外建立一个独立于JCP的组织来发展运作这份规范,让更多不适合参与到JCP的设备厂商能够参与OSGi的规范制定。因此,1999年独立于JCP的OSGi联盟成立,并于2000年发布了OSGi规范的第一个版本:OSGi R1.0。简言之,OSGi(Open Service Gateway Initiative,直译为“开放服务网关”)实际上是一个由OSGi联盟(OSGi Alliance)发起的以Java为技术平台的动态模块化规范。
1.2 版本演进
OSGi规范的演进在OSGi R4版之前都处于初级阶段,初级阶段一共发布了三个版本(R1、R2、R3),规范的主要关注点是在移动和嵌入式设备上的Java模块化应用。从OSGi R4版开始,OSGi开始脱离Java ME的约束,向Java其他领域进军,发布了几个R4的子版本。OSGi的快速演进发展和Eclipse有着密不可分的关系,从Eclipse 3.0 M4版本开始,Eclipse被重构为完全基于OSGi架构实现的产品,支持Eclipse运行的底层框架Equinox成为OSGi R4.x应用最广泛的实现框架,随着eclipse的广泛应用,也极大推动了OSGi的发展。目前最新的OSGi规范版本是OSGi R5。该规范由数十个子规范组成,包含了上千个不同用途的API接口。
OSGi联盟给出的最新OSGi定义是The Dynamic Module System for Java,即面向Java的动态模块化系统。在OSGi的术语中,模块称为bundle。OSGi提供了一个框架来管理bundle,bundle被打包成普通的Java JAR文件,里面包含了清单文件(manifest)。在清单文件中包含了重要的元数据信息,这些信息描述了bundle以及对OSGi框架的依赖。
各版本简介如下:
❑OSGi Release 1 (R1):2000年5月发布。
❑OSGi Release 2 (R2):2001年10月发布。
❑OSGi Release 3 (R3):2003年3月发布。
❑OSGi Release 4.1 (R4.1):2007年5月发布,该版本中处理了Bundle延迟初始化的问题,增加了Bundle-ActivationPolicy标识来指明Bundle的启动策略。
❑OSGi Release 4.2 (R4.2):2009年9月发布了OSGi R4.2版核心规范发布;在次年3月,还发布了OSGi R4.2企业级规范。OSGi R4.2专门独立发布企业级服务规范的一个重要任务就是解决OSGi与Java EE服务之间关系的问题。
备注:目前的OSGi企业级规范中定义了JDBC、JPA、JMX、JTA和JNDI等各种Java EE技术以及SCA、SDO这些非Java EE标准的企业级技术在OSGi环境中的应用方式,这些容器级的服务都可以映射为OSGi容器内部的服务来使用。
❑OSGi Release 4.3 (R4.3):2011年4月和2012年3月,分别发布了OSGi R4.3的核心规范和服务纲要规范。在这个版本中,OSGi的API接口开始使用已经有8年历史的、从Java SE 5开始提供的泛型。OSGi R4.3的另一个重要改进是在核心规范中添加了Bundle Wiring API子规范,该规范引入了Capabilities和Requirements的概念。
❑OSGi Release 5 (R5):2012年7月,OSGi R5发布(同时发布了核心规范和企业级规范的OSGi R5版本),OSGi R5的一个主要目标是建立一套基于OSGi的模块仓库系统(为下一步的OSGi in Cloud做准备)。
备注:模块化并不仅仅是把系统拆分成不同的块而已(这是JAR包就能做的事情),真正的模块化必须考虑到模块中类的导出、隐藏、依赖、版本管理、生命周期变化和模块间交互等一系列的问题。
1.3 优点与缺点
优点:
1)OSGi是真正的模块平台,它解决了企业级Java开发和SOA目前并没有解决的关键问题。OSGi bundle是真正的软件模块。基于OSGi的程序更新升级或者缺陷修复时,能够像电脑更换USB接口的鼠标键盘或者插拔其他USB设备那样可以即插即用,无须重启,甚至无须停顿,这是许多基于Java的、需要7×24小时运转的生产系统长期以来迫切希望而又无法实现的需求。
2)OSGI框架本身可复用性极强,很容易构建真正面向接口的程序架构,每一个Bundle都是一个独立可复用的单元。
3)基于OSGi规范比较容易实现强鲁棒性的系统。对于遵循OSGi的设计原则实现的应用程序来说,一般不会因为局部的错误导致全局系统的崩溃。
缺点:
1)OSGi框架对系统性能会有一定的损耗。从执行上来说,OSGi是在Java虚拟机之上实现的,它没有要求虚拟机的支持,完全通过Java代码实现模块化,在执行上不可避免地会有一些损耗。从内存上来说,OSGi允许不同版本的Package同时存在,这是个优点,但是客观上会占用更多内存。
2)使用OSGi框架会增加系统的复杂程度,OSGi本身就具有较高的复杂度,小型系统使用OSGi可能导致开发成本更高。
3)OSGi只适合构建单一服务节点的内部应用,OSGi并不是分布式的服务技术。OSGi的bundle会在同一个JVM中进行部署和交互,这可以在进程内轻量级地跨bundle交互。 bundle尽管可以为隔离的服务建立独立生命周期管理的热部署方式,以及明确的服务导出和导入依赖能力,但是由于其最终基于jvm,无法对bundle对应的服务实现计算资源的隔离,一个服务的故障依然会导致整个jvm crush,这使得在一个运行时的osgi上部署模块级服务只获得了模块部署和启停隔离,服务明确依赖的好处,但是没办法实现计算节点的线性扩展。
1.4 OSGi的实现
OSGI是OSGi Alliance组织制定的Java模块化规范,但OSGI联盟并没有给出OSGI容器的实现,具体实现由第三方厂商完成,目前使用较多的Eclipse的Equinox、Spring 的Spring DM、Apache的Felix和Makewave的Knopflerfish等都是OSGi容器实现。
(1)Equinox
Equinox是著名IDE工具Eclipse在从2.0版向3.0版进化过程中的衍生产物。Equinox是目前使用最为广泛的OSGi R4.x规范的实现,它是一个松散的程序集,由一系列Bundle共同组成,这些Bundle有的用于实现OSGi运行的基础架构,有的用于提供众多OSGi标准服务,还有一些Bundle提供了在OSGi标准之外的Equinox的专有功能。
(2)Spring DM
Spring DM是Spring旗下的OSGi规范实现。Spring-DM 的主要目的是能够方便地将 Spring 框架和OSGi框架结合在一起,使得使用Spring的应用程序可以方便简单地部署在OSGi环境中,利用OSGi框架提供的服务,将应用变得更加模块化。
(3)Apache Felix
Apache Felix是Apache旗下的一个OSGi框架,Felix是一个OSGi版本4规范的Apache实现。Apache Felix提供的服务几乎涵盖了全部的OSGi 4.2的标准,除此之外还提供了一些非标准的功能,例如iPOJO。Apache Felix框架本身非常紧凑,只需要3个包加一个shell就可以运行,无论是开发还是Debug都非常简便。
(4)Knopflerfish
Knopflerfish是OSGi的先行者,是一个相当标准OSGi框架,提供了绝大多数标准功能。
2. OSGi规范与原理
目前最新的OSGi规范是2012年7月发布的Release 5,Version5.0(后文简称为R5.0)版本,该规范定义了Java模块化系统所涉及的各种场景(开发、打包、部署、更新和交互等),以及其中用到的标准接口和参考模型。
OSGi规范并不是单一的规范文档,而是由一系列子规范构成,这些子规范主要可分为两大部分,其中一部分用于描述OSGi的核心框架(OSGi Framework)。
OSGi核心框架是一个可运行OSGi系统的最小集合,它由以下内容组成:
❑执行环境(Execution Environment)。需要注意不同硬件、软件环境对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服务纲要规范和企业级规范之中。
上图是OSGi标准服务的框架组成示意图,OSGi架构又被称为“微内核架构”,模块层、生命周期层和服务层的这三层规范共同组成了OSGi微内核的核心。
备注:在OSGi的术语中,模块称为bundle。OSGi提供了一个框架来管理bundle,bundle被打包成普通的Java JAR文件,里面包含了清单文件(manifest)。在清单文件中包含了重要的元数据信息,这些信息描述了bundle以及对OSGi框架的依赖。
2.1 模块层规范与原理
在OSGi中,模块层独立于生命周期层和服务层,这意味着它在使用时可以不需要生命周期层和服务层的支持,但是,这样的模块是“静态的”。生命周期层提供了对模块层的Bundle进行管理的各种API,而服务层提供了Bundle之间的通信模型。对于模块层,最重要的就是要学习理解Bundle。
2.1.1 Bundle简介
OSGi框架定义了一个模块化单元,并将其称为bundle。Bundle是OSGi中最基本的单位,通俗地讲,如果说OSGi是基于Java平台的“模块化开发体系”,那么Bundle便是其中的“模块”。OSGi中的Bundle是在JAR文件格式规范基础上扩展而来的,一个符合OSGi规范的Bundle首先必须是一个符合JAR文件格式规范的JAR包。
Bundle相对普通的JAR文件主要进行了以下三个方面扩展:
❑JAR文件格式规范里定义的/META-INF/MANIFEST.MF文件用于描述JAR包的元数据信息,如JAR包的版本、数字签名信息等,OSGi中的Bundle在MANIFEST.MF文件中添加了大量扩展定义,如描述该Bundle可以提供哪些资源、依赖哪些其他Bundle、启动或卸载时要执行哪些动作等。
❑加入了一个可选的/OSGI-OPT文件夹,可以在其中保存一些与Bundle运行无关的信息,比如Bundle源码、软件说明书等。Bundle的使用者可以从中获取一些额外的信息,也可以安全地删除该文件夹,以节约OSGi系统的存储空间。
❑Bundle中可以包含一些具备特殊含义的程序和资源,如使用Bundle-Activator定义的初始化类、定义在OSGI-INF/l10n目录中的本地化信息等。
备注:
bundle是一种特殊的JAR文件。如果一个JAR文件中包含了提供功能的资源以及关于bundle元数据的清单文件(Manifest),那么它就是一个合法的OSGi bundle。资源可以是Java类、HTML、图片、Servlet甚至JSP。元数据通过一些必需和可选的键值对来进行定义。
Fragment Bundle是一种特殊的Bundle,它无法独立存在,必须依附于某个其他的普通Bundle来使用,可以将它视为“Bundle的插件”、“模块中的模块”,经常用来提供某些可选的功能,比如为某个实现具体功能的Bundle提供一个语言包等,也可以用于隔离Bundle中经常变动的部分,比如可以将系统的内部配置文件集中在Fragment Bundle中,通过更换不同的Fragment Bundle来实现配置快速切换。
OSGi是利用每个Bundle独立的类加载器互相协作来维护Bundle间导入、导出的依赖关系的。Fragment Bundle不具备自己独立的类加载器,因此无法直接与其他Bundle交互,必须依赖于宿主,使用宿主Bundle的类加载器完成加载,进而进行模块间交互。
2.1.2 Bundle元数据标记
Bundle的元数据信息定义在/META-INF/MANIFEST.MF文件之中,OSGi规范中明确要求实现框架必须能够正确识别那些被预定义过的标记(在R5.0规范中预定义了28项标记),对于不可识别的标记以及不符合MANIFEST.MF标记格式的内容都要忽略且不能影响Bundle的正常解析。MANIFEST.MF文件中常用的预定义标记有:
(1)Bundle-ActivationPolicy
标记Bundle-ActivationPolicy用于设置Bundle的加载策略,该参数目前只有一个值:lazy,设置该参数后,Bundle将延迟激活,延迟至有其他的Bundle请求加载该Bundle中的类或资源时它才会被激活,如果不设置这个参数,那么Bundle启动时就会被激活。
(2)Bundle-Activator
标记Bundle-Activator用于指明一个Activator类,在Bundle启动和停止时会分别调用该类的start()和stop()方法,以便执行程序员所希望的动作,该类必须实现org.osgi.framework.BundleActivator接口。
(3)Bundle-Classpath
标记Bundle-Classpath用于指明该Bundle所引用的类路径,该路径应为Bundle包内部的一个合法路径,如果有多个Classpath,使用逗号分隔。
(4)Bundle-Category
标记Bundle-Category用于指明该Bundle的功能类别,可使用逗号分隔多个类别名称。仅供人工分类和阅读,OSGi框架并不会使用它。
(5)Bundle-ContactAddress
标记Bundle-ContactAddress用于描述Bundle发行者的联系信息。仅供人工阅读,OSGi框架并不会使用它。
(6)Bundle-Copyright
标记Bundle-Copyright描述Bundle的版权信息。仅供人工阅读,OSGi框架并不会使用它。
(7)Bundle-Description
标记Bundle-Description用于给出关于该Bundle的简短描述信息。仅供人工阅读,OSGi框架并不会使用它。
(8)Bundle-DocURL
标记Bundle-DocURL用于给出该Bundle文档的链接地址。仅供人工阅读,OSGi框架并不会使用它。
(9)Bundle-Icon
标记Bundle-Icon用于给出该Bundle的显示图标,图标应为一张正方形的图片,并通过参数size指出图标的宽度。OSGi规范要求实现框架至少要支持PNG图片格式。
(10)Bundle-License
标记Bundle-License用于给出该Bundle的授权协议信息。
(11)Bundle-Localization
标记Bundle-Localization用于给出该Bundle在不同语言系统下的本地化信息,如果不设置此标记,它的默认值为OSGI-INF/l10n/bundle。
(12)Bundle-ManifestVersion
标记Bundle-ManifestVersion用于指出该Bundle应遵循哪个版本的OSGi规范,默认值为1。对于OSGi R3规范,该值为1;对于OSGi R4/R5规范,该值为2。
(13)Bundle-Name
标记Bundle-Name用于定义该Bundle的名称。该名称只供人工阅读,在Bundle-SymbolicName标记中定义的名称才会作为程序使用的Bundle的唯一标识来使用。根据一般开发习惯,Bundle-Name中所定义的名称会在打包发布时与Bundle-Version一起构成该Bundle的文件名,所以这个名称一般不含空格或其他不能在文件名中出现的字符。
(14)Bundle-NativeCode
如果Bundle中需要使用JNI加载其他语言实现的本地代码,那么必须使用此标记进行说明。
这个标记有如下附加参数:
❑osname:操作系统名称,如Windows等。
❑osversion:操作系统版本号,如3.1等。
❑processor:处理器指令集架构,如x86等。
❑language:遵循ISO编码的语言,如en,zh等。
❑seleciton-filter:选择过滤器,该值为一个过滤器表达式,指定被选中或未被选中的本地代码。
(15)Bundle-RequiredExecutionEnvironment
标记Bundle-RequiredExecutionEnvironment用于定义该Bundle所需的执行环境,支持多种执行环境的Bundle使用逗号分隔。
备注:OSGi对执行环境定义的命名是直接继承于Java平台的执行环境名称。
(16)Bundle-SymbolicName
标记Bundle-SymbolicName用于给出该Bundle在OSGi容器中的全局唯一标识符。与其他可选标记不同,这个标记没有默认值,并且是Bundle元数据信息之中唯一一个必须设置的标记。程序将基于此标记和版本号在OSGi容器中定位到一个独一无二的Bundle。当且仅当两个Bundle的Bundle-SymbolicName和Bundle-Version属性都相同的时候,它们才是完全相同的,不允许同时安装两个完全相同的Bundle到同一个OSGi容器之中。
Bundle-SymbolicName有以下两个附加参数:
❑singleton:表示Bundle是单例的。
❑fragment-attachment:定义Fragment Bundle是否能附加到该Bundle之上。允许值为always、never和resolve-time,含义为允许附加、禁止附加和只允许在解析过程中附加,默认值为always,即允许附加。
(17)Bundle-UpdateLocation
标记Bundle-UpdateLocation用于给出Bundle的网络更新地址。如果Bundle需要更新版本,将使用这个地址。
(18)Bundle-Vendor
标记Bundle-Vendor用于给出该Bundle的发行者信息。
(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版本的大小。
另外,对于限定字符串的处理,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。
(21)Export-Package
标记Export-Package用于描述被导出的Package。
(22)Export-Service
标记Export-Service用于描述被导出的服务,这个标记在OSGi规范中目前已经被声明为Deprecated,不推荐继续使用此标记。
(23)Fragment-Host
当该Bundle是一个Fragment Bundle时,标记Fragment-Host指明它的宿主Bundle。
(24)Import-Package
标记Import-Package用于描述该Bundle需要导入的Package。
(25)Import-Service
标记Import-Service用于描述导入的服务。这个标记在OSGi规范中目前已经被声明为Deprecated,不推荐继续使用此标记。
(26)Provided-Capability
标记Provided-Capability用于描述该Bundle提供的服务特性(Capability)。
(27)Require-Capability
标记Require-Capability用于描述该Bundle所需要的服务特性。
备注:服务特性是在OSGi R4.3规范中加入的新概念,通过Provided-Capability和Require-Capability来声明Bundle所需要和能够提供的特性。
(28)Require-Bundle
标记Require-Bundle用于描述该Bundle所依赖的其他Bundle,一旦声明了依赖某个Bundle,就意味着可以直接使用所有从这个Bundle中导出的Package。
备注:除非特别说明,否则上述所列举的标记项都是可选的。
2.1.3 Bundle的组织与依赖
既然是以模块化方式开发一个系统,那么必不可少的步骤是根据业务和技术的需要,将系统划分为多个模块,通过这些模块互相协作完成系统的功能。
2.1.3.1 导入导出Package
对于Bundle的导入导出,最简单常用的就是通过使用Import-Package、Export-Package以及Require-Bundle标记进行配置。但在开发过程中总会遇见各种不同的需求,要根据特定规则去选择适合的Package。OSGi规范中定义了几种对导入导出进行筛选的过滤方式:
(1)根据类名过滤
如果仅在Package层次上控制导出的粒度不够精细,无法满足应用需求,那么可以使用附加参数include和exclude进一步描述要导出Package中哪一部分内容。
include和exclude参数的具体使用方法如下:
❑附加参数include用于列举需要导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只有符合规则的类才会被导出。
❑附加参数exclude用于列举禁止导出Package中哪些类,类名使用逗号分隔,可以使用通配符。如果加入这个参数,那么只要符合规则的类就不会被导出。
备注:include和exclude的限制是在导出时处理的,导入时无需对应用做任何特殊声明,Import-Package标记也无法与这两个参数搭配使用。
(2)根据版本过滤
在OSGi系统中,同一个名称的Package可能存在多个不同版本,对于同一个名称但不同版本的Package进行导入导出控制时,可以使用version参数。
version参数的具体使用方法如下:
在导出Package时,此参数声明导出Package的版本号,如果不设置,默认值为0.0.0;
在导入Packag时,此参数声明要导入Package的版本范围,如果不设置,默认值为[0.0.0, ∞)。
备注:在声明版本范围时,方括号“[”和“]”表示“范围包含此数值”,圆括号“(”和“)”表示“范围不含此数值”。
在OSGi R3.x及之前的版本中,version标记原本叫做specification-version,在R4.x规范中新增了version标记,但依然保留了specification-version这个别名,但是已将它声明为Deprecated,如果同时设置了version和specification-version的值,那么这两个值必须相等。(3)根据提供者过滤
根据提供者过滤一般使用在测试Bundle的时候,Import-Package标记提供了两个附加参数bundle-symbolic-name和bundle-version来完成这项功能,它们分别用于对Bundle的名称和版本进行过滤。
bundle-symbolic-name和bundle-version参数的具体使用方法如下:
❑附加参数bundle-symbolic-name:参数值为某个Bundle的名称,只有符合该名称的Bundle所导出的Package才会被导入。
❑附加参数bundle-version:参数值为导入Bundle(注意不是Package)的版本范围,只有版本在该范围之内的Bundle所导出的Package才会被导入。
(4)根据属性过滤
对于Package导入/导出的过滤除了上述三种方式外,还支持根据自定义的扩展属性进行过滤,以满足某些根据开发人员自己加入的属性进行过滤的需求。
mandatory附加参数可用于强制要求导入导出时必须存在指定的扩展属性才能成功匹配。如果没有在mandatory中指定属性名称,那这种属性被默认为“optional”,即可选的,在导入时没有声明这个属性也不影响正常导入。
mandatory参数的具体使用方法如下:
mandatory参数只适用于Export-Package,用于声明哪些属性是必选的,只有导入Package时提供了必选的属性,才能正确匹配到导出的Package。多个属性用逗号分隔,用双引号包裹。
示例:Export-Package: org.osgi.simple;filter=”true” 即在导出org.osgi.simple时加入了自定义属性filter,其值为“true”。那么在进行导入时,如下三个语句配置,只有Bundle A和Bundle B能够成功导入org.osgi.simple包,Bundle C会因为自定义属性冲突而导致匹配失败。
Bundle A:
Import-Package: org.osgi.simple
Bundle B:
Import-Package: org.osgi.simple; filter="true"
Bundle C:
Import-Package: org.osgi.simple; filter="false"
若导出配置改为:Export-Package: org.osgi.simple;filter=”true”;mandatory:=”filter”,则只有BundleB可以导入成功。
(5)可选导入与动态导入
动态导入和可选导入实现的功能有些类似,它们的共同特征是在Bundle解析期间即使找不到要导入的依赖,也不会导致解析失败。它们的区别是,动态导入每次加载包中的类都会尝试去查找动态导入包,而可选导入包只有在Bundle解析时才进行连接尝试。
1)可选导入可以通过Import-Package标记的resolution附加参数来实现。
resolution参数的具体使用方法如下:
附加参数resolution只适用于Import-Package标记,用于确定某个Package是可选的还是必须的。可选值为“mandatory”和“optional”, 默认值为“mandatory”。
使用场景:导入某个Package是为了实现一些不影响Bundle正常运行的附加功能。比如为某个英文软件开发了一个实现中文语言支持的插件,不导入这样的Package也不应当影响整个系统正常运行,只不过软件仍以英文形式显示而已。
示例:Import-Package:org.osgi.simple; resolution:=”optional” 此时无论是否有模块导出org.osgi.simple包,都不会造成导入依赖解析失败。
2)动态导入可以通过DynamicImport-Package标记进行实现。
DynamicImport-Package标记的语义和Import-Package标记很类似,其不同点在于DynamicImport-Package标记不在Bundle的解析阶段进行处理,无论它要求导入的Package是否存在,都不会影响Bundle的解析,只有在真正使用相应Package时才会去查找是否存在。另外,与Import-Package有所不同的是,DynamicImport-Package可以使用通配符。
使用场景:某个被导入的Package在使用中是必需的,但是提供这个Package的Bundle并不会在系统启动时就被安装。在这种情况下,只有真正使用到这个Package的类时,才会去导入这个Package,而不是在启动时就查找是否有提供Package的Bundle存在。
(6)导出Package的依赖限制
Package之间的关系可以通过使用Export-Package标记中的uses附加参数来描述。当一个系统中同时存在不同版本的Package时,uses参数对于协调依赖关系很有用。
uses参数的具体使用方法如下:
附加参数uses只适用于Export-Package标记,用于说明导出Package的依赖关系。如果同时依赖多个Package,使用逗号分隔,并用双引号包裹。
示例:包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"
(7)导入整个Bundle
除了进行Package的导入导出,OSGi中对于实现Bundle级别的依赖关系可以使用Require-Bundle标记来声明。Require-Bundle标记后面应跟随一个或多个其他Bundle的名称(由Bundle-SymbolicName定义的名称),多个名称之间使用逗号分隔。但是,一般来说,OSGi提倡使用Import-Package和Export-Package来构成模块之间的依赖关系,不提倡使用Require-Bundle,从实践经验来说,依赖的粒度总是越小越好。
备注:在默认设置下,通过Require-Bundle标记导入了某个Bundle,仅表示在本Bundle中可以访问被导入Bundle中声明导出的Package,除非明确用Export-Package声明过,否则这些Package在本Bundle中默认不会再次被导出。如果有必要,可以使用Import-Package标记的visibility附加参数来改变这种行为。visibility参数的具体使用方法如下:
附加参数visibility仅用于Require-Bundle标记,描述来自被导入Bundle中的Package是否要在导入Bundle中重新导出。默认值为private,代表不会在导入Bundle中重新导出;如果值为reexport,则说明需要重新导出。
拆分包(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。
2.1.3.2 校验Bundle有效性
在将Bundle安装到OSGi系统的时候,必须对其进行有效性校验。要确定某个JAR包是否是一个合法的OSGi Bundle,首先要根据其元数据信息中的Bundle-ManifestVersion(注意,不是ManifestVersion,ManifestVersion是JAR文件规范中定义的)来确定元数据信息的版本号。如果在元数据信息中没有指定Bundle-ManifestVersion,那么其默认属性是1,表示遵循OSGi R3规范;如果Bundle-ManifestVersion为2,则代表遵循OSGi R4/R5规范。
一些常见的会导致Bundle有效性校验失败的问题有:
❑无法根据Bundle-RequireExecutionEnvironment的值找到一个可匹配执行环境。
❑在Bundle中没有设置Bundle-SymbolicName标记。
❑存在重复的标记。
❑对某个Package重复导入。
❑Bundle导入或导出了以java.*开头的Package。
❑在Export-Package标记中用mandatory参数指明了某些强制属性,却没有定义这些属性。❑尝试安装某个和OSGi框架中某个已经安装好的Bundle具有同样名称和版本的Bundle。
❑将某个Bundle更新为和OSGi框架中一个已经安装好的Bundle具有同样名称和版本的Bundle。
❑元数据信息中存在语法错误(例如,不合法的版本格式或Bundle名称)。
❑同时使用Specification-version和version参数,但是为它们指定了不一样的值
❑在元数据信息中列出了权限文件:OSGI-INF/permission.perm,但此文件不存在。
2.1.4 OSGi的类加载架构
OSGi为Java平台提供了动态模块化的特性,但是它并没有对Java的底层实现如类库和Java虚拟机等进行修改,OSGi实现的模块间引用与隔离、模块的动态启用与停用的关键在于它扩展的类加载架构。OSGi的类加载架构并未遵循Java所推荐的双亲委派模型(Parents Delegation Model),它的类加载器通过严谨定义的规则从Bundle的一个子集中加载类。除了Fragment Bundle外,每一个被正确解析的Bundle都有一个独立的类加载器支持,这些类加载器之间互相协作形成了一个类加载的代理网络架构,因此OSGi中采用的是网状的类加载架构,而不是Java传统的树状类加载架构。如下图:
2.1.4.1类加载器的分类
在OSGi中,类加载器可以划分为3类:
❑父类加载器:由Java平台直接提供,最典型的场景包括启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。在一些特殊场景中(如将OSGi内嵌入一个Web中间件)还会有更多的加载器组成。它们用于加载以“java.*”开头的类以及在父类委派清单中声明为要委派给父类加载器加载的类。
❑Bundle类加载器:每个Bundle都有自己独立的类加载器,用于加载本Bundle中的类和资源。当一个Bundle去请求加载另一个Bundle导出的Package中的类时,要把加载请求委派给导出类的那个Bundle的加载器处理,而无法自己去加载其他Bundle的类。
❑其他加载器:譬如线程上下文类加载器、框架类加载器等。它们并非OSGi规范中专门定义的,但是为了实现方便,在许多OSGi框架中都会使用。例如框架类加载器,OSGi框架实现一般会将这个独立的框架类加载器用于加载框架实现的类和关键的服务接口类。
备注:不同类加载器所能完成的(无论是自己完成加载,还是委派给其他类加载器来加载)加载请求的范围构成了该Bundle的类名称空间(Class Name Space)。在同一个类名称空间中,类必须是一致的,也就是说不会存在完全重名的两个类。但是在整个OSGi的模块层,允许多个相同名称的类同时存在,因为OSGi模块层是由多个Bundle的类名称空间组成的。
单独一个Bundle的类名称空间由如下内容组成:
❑父类加载器提供的类(以java.*开头的类以及在委派名单中列明的类);
❑导入的Package(Import-Package);
❑导入的Bundle(Require-Bundle);
❑本Bundle的Classpath(私有Package,Bundle-Classpath);
❑附加的Fragment Bundle(fragment-attachment);
❑动态导入的Package(DynamicImport-Package)。
(1)父类加载器
OSGi框架必须将以java.*开头的Package交给父类加载器代理,这一点是无须设置且不可改动的。除此之外,OSGi框架也允许用户通过系统参数“org.osgi.framework.bootdelegation”自行指定一些Package委派给父类加载器加载,这个参数被称为“父类委派清单”(Boot Delegation List)。它的值应为一系列的包名,用逗号分隔,支持通配符,例如:org.osgi.framework.bootdelegation=sun.*,com.sun.*
备注:以java.*开头的Package是默认被隐式导出的,在所有Bundle中无需导入便可以直接使用,并且OSGi规范明确禁止在Bundle中导入或导出以java.*开头的Package。与前面提到的父类委派清单类似,OSGi也定义了添加隐式导出Package的参数“org.osgi.framework.system.packages”。这个参数使用标准的Export-Package语法描述,例如:org.osgi.framework.system.packages=javax.crypto.interfaces
(2)Bundle类加载器
OSGi框架为每一个Bundle(不包括Fragment Bundle)生成了一个Bundle类加载器的实例,这些类加载器负责处理其他Bundle委派的加载请求,根据元数据信息确定这些加载请求的类是否与该Bundle的导出列表相符合,然后对合法的加载请求进行响应,返回该Bundle的类供其他Bundle使用。
Bundle-Classpath这个元数据标记与Bundle类加载器密切相关,它描述了Bundle加载器的Classpath范围,即Bundle加载器应该到哪里去查找类。Bundle-Classpath标记有默认值“.”,它代表该Bundle的根目录,或者说代表该Bundle的JAR文件。如果不在元数据信息中显式定义这个标记,那么Bundle类加载器就在整个Bundle的范围内查找类。但是要注意,在这种默认配置下,如果Bundle存在其他JAR文件,类加载器只能把它当作一个普通资源来读取,而无法查找到这些JAR文件内部包含的类。
如果Bundle-Classpath标记的值是多个Classpath路径,那么它们之间还有优先级关系,例如下面这个定义:Bundle-Classpath: required.jar,optional.jar,default.jar该定义中required.jar是必须出现在Bundle中的类和资源;optional.jar是某个可选的JAR包,其中存放着可选的类和资源;default.jar中存放着optional.jar不可用时这些类和资源的默认值,如果optional.jar中有可用的内容便会对其覆盖。
Bundle类加载器收到类加载请求时,会优先委托给导入包的其他Bundle类加载器处理,只有其他导入包的Bundle类加载器都无法处理时才会尝试自己处理。即可以通俗地理解为“Import-Package”和“Require-Bundle”的优先级高于“Bundle-Classpath”,如果能在前者中找到所需的类,后者就不会起作用。
对于在Bundle中发生的加载请求而言,当前Bundle的Bundle类加载器是使用到的类的初始类加载器(Initiating Classloader,它表示加载请求最先发送到的类加载器),而哪个类加载器是定义类加载器(Defining Classloader,它表示加载请求被不断委派后,最终执行加载动作的类加载器)则要根据OSGi类加载顺序来判定。
备注:“初始类加载器”和“定义类加载器”的概念在《Java虚拟机规范》之中有定义,它们分别指“触发加载某个类”的类加载器和“真正加载某个类”的类加载器。引导类加载器和定义类加载器可能是同一个,但更多情况下是由引导类加载器委派给定义类加载器去加载某个类。
(3)其他类加载器
在OSGi中还可能使用到其他的类加载器,比如OSGi实现框架中一般都会有框架类加载器(Framework Classloader)。OSGi框架为每个Bundle创建Bundle类加载器的实例,而OSGi框架自身的代码——至少涉及OSGi框架启动的代码就没法使用Bundle类加载器来加载,因此需要一个专门的框架类加载器来完成这个任务。这个框架类加载器是各个OSGi实现框架自己定义的。
另外一个在OSGi中比较常见的类加载器是线程上下文类加载器(Thread ContextClassLoaser),
这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时未设置,那么它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器就默认是应用程序类加载器。
2.1.4.2 类加载顺序
当一个Bundle类加载器遇到需要加载某个类或查找某个资源的请求时,搜索过程必须按如下指定步骤执行:
(1)如果类或资源在以java.*开头的Package中,那么这个请求需要委派给父类加载器;否则,继续下一个步骤搜索。如果将这个请求委派给父类加载器后发现类或资源不存在,那么搜索终止并宣告这次类加载请求失败。
(2)如果类或资源在父类委派清单(org.osgi.framework.bootdelegation)所列明的Package中,那么这个请求也将委派给父类加载器。如果将这个请求委派给父类加载器后,发现类或资源不存在,那么搜索将跳转到一个步骤。
(3)如果类或资源在Import-Package标记描述的Package中,那么请求将委派给导出这个包的Bundle的类加载器,否则搜索过程将跳转到下一个步骤。如果将这个请求委派给Bundle类加载器后,发现类或资源不存在,那么搜索终止并宣告这次类加载请求失败。
(4)如果类或资源在Require-Bundle导入的一个或多个Bundle的包中,这个请求将按照Require-Bundle指定的Bundle清单顺序逐一委派给对应Bundle的类加载器,由于被委派的加载器也会按照这里描述的搜索过程查找类,因此整个搜索过程就构成了深度优先的搜索策略。如果所有被委派的Bundle类加载器都没有找到类或资源,那么搜索将转到下一个步骤。(5)搜索Bundle内部的Classpath。如果类或资源没有找到,那么这个搜索将转到下一个步骤。
(6)搜索每个附加的Fragment Bundle的Classpath。搜索顺序将按这些Fragment Bundle的ID升序搜索。如果这个类或资源没有找到,那么搜索转到下一个步骤。
(7)如果类或资源在某个Bundle已声明导出的Package中,或者包含在已声明导入(Import-Package或Require-Bundle)的Package中,那么这次搜索过程将以没有找到指定的类或资源而终止。
(8)如果类或资源在某个使用DynamicImport-Package声明导入的Package中,那么将尝试在运行时动态导入这个Package。如果在某个导出该Package的Bundle中找到需要加载的类,那么后面的类加载过程将按照步骤(3)处理。
(9)如果可以确定找到一个合适的完成动态导入的Bundle,那么这个请求将委派给该Bundle的类加载器。如果无法找到任何合适的Bundle来完成动态导入,那么搜索终止并宣告此次类加载请求失败。当将动态导入委派给另一个Bundle类加载器时,类加载请求将按照步骤(3)处理。
2.1.5 本地化
OSGi规范定义了Bundle应该如何自动根据系统语言、国家等参数自动翻译这些信息,即Bundle的本地化能力。Bundle的本地化信息必须遵循特定的命名规则,存放在Bundle的指定目录下,如果没有通过Bundle-Localization特别指定,那么这个目录默认为“OSGI-INFO/l10n”。为了方便实现框架查找存在的本地化信息,OSGi规范规定了这些信息必是以“bundle”开头,以语言、国家、其他参数为内容,以下划线(‘-’,\u005F)分隔,以“.properties”为扩展名来命名的文本文件,即遵循以下格式命名:OSGI-INF/l10n/bundle-[语言]-[国家]-[其他参数].properties文件名中所使用到的语言、国家等会从java.util.Locale获取。比如:OSGI-INF/l10n/bundle-en.properties。
2.2 生命周期层规范与原理
OSGi规范把模块化“静态”的一面,比如如何描述元数据、如何加载模块中的类和资源等内容定义于模块层规范之中;而把模块化“动态”的一面,比如模块从安装到解析、启动、停止、更新、卸载的过程,以及在这些过程中的事件监听和上下文支持环境等定义于生命周期层(Life Cycle Layer)之中。
2.2.1 Bundle标识
对于生命周期层,依然可以采用Bundle-SymbolicName和Bundle-Version标记来确定唯一的Bundle。不过,基于API使用方便的考虑,在运行期还可以采用其他Bundle标识进行定位,包括:
❑Bundle ID(Bundle Identifier)。
Bundle ID是运行期最常用的标识符。它是由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接口的getSymbolicName()方法获取当前Bundle的符号名称,通过getVersion()方法获取Bundle的版本号。
2.2.2 Bundle状态及转换
2.2.2.1 Bundle状态
“状态”是Bundle在运行期的一项动态属性,不同状态的Bundle具有不同的行为。生命周期层规范定义了Bundle生命周期过程之中的6种状态,分别是:UNINSTALLED(未安装)、INSTALLED(已安装)、RESOLVED(已解析)、STARTING(启动中)、STOPPING(停止中)、ACTIVE(已激活),目前 Bundle的状态值采用由整型数存储的位掩码进行表示。各状态介绍如下:
❑UNINSTALLED,未安装状态。
处于未安装状态的Bundle导出的Package和包含的其他资源都是不可使用的。但是OSGi容器中代表这个Bundle的对象实例仍然可以操作,在某些场景,比如自省(Introspection)中这个对象还是可用的。UNINSTALLED的状态值为整型数1。
❑INSTALLED,已安装状态。
Bundle处于已安装状态就意味着它已经通过OSGi框架的有效性校验并产生了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目前的状态。
2.2.2.2 Bundle的状态转换
Bundle状态是动态可变的,在特定条件下6种状态可以互相转换,其过程如下图:
(1)安装过程
OSGi规范定义了BundleContext接口的installBundle()方法来安装新的Bundle,方法参数为要安装的Bundle的Bundle Location。但是OSGi规范没有详细规定Bundle的安装过程应当如何进行,只是很笼统地要求OSGi框架在实现这个方法时,至少要完成生成新的Bundle ID、对元数据信息进行有效性校验、生成Bundle对象实例这些工作。
一个Bundle的安装过程是个原子过程,即要么Bundle已经安装了,要么Bundle还没有安装,不会观察到Bundle“正在安装之中”的状态,也不会出现Bundle安装了一半的情况。并且,Bundle的INSTALLED状态是一个持久状态,如果没有外部作用(改变启动级别、调用start()方法或卸载Bundle),Bundle将一直维持这个状态。
(2)解析过程
解析过程是OSGi框架根据Bundle的MANIFEST.MF文件中描述的元数据信息分析处理Bundle依赖关系的过程。
大致流程有:1)对已安装的Bundle进行优先级排序。2)对要解析的Bundle进行检查校验。3)对Fragment Bundle进行宿主依赖处理。4)对所有Bundle元数据信息中声明的依赖项进行校验。5)将Bundle状态调整为已解析状态。
(3)启动过程
启动过程即执行Bundle的Activator.start()方法的过程,在此方法执行期间,Bundle的状态为STARTING。如果成功执行完这个方法,那么Bundle的状态会转变为ACTIVE,而且将一直保持这个状态直到Bundle被停止。在Bundle启动时,OSGi框架需要通过调用Class.newInstance()方法来创建Activator类的实例,因此Bundle的Activator类必须保证有一个默认的(即不带参数的)构造函数。
(4)更新过程
Bundle的更新过程其实就是重新从Bundle文件加载类、生成新的Bundle对象实例的过程。OSGi规范在Bundle接口中定义了以下两种方法来更新Bundle。
❑Bundle.update():从Bundle原来的位置进行更新。
❑Bundle.update(InputStream):通过一个指定的输入流获取Bundle新的内容进行更新。
(5)停止过程
Bundle的停止过程是启动过程的逆向转换,此时OSGi框架会自动调用Bundle的Activator类的stop()方法。在stop()方法执行期间,Bundle的状态为STOPPING。当stop()方法成功执行完毕后Bundle的状态转变为RESOLVED。我们一般用stop()方法来释放Bundle中申请的资源、终止Bundle启动的线程等。在执行完Bundle的stop()方法后,其他Bundle就不能再使用该Bundle的上下文状态(BundleContext对象)。
备注:需要注意的是,即使Bundle已经停止,它导出的Package仍然是可以使用的,无论对停止前还是停止后安装的Bundle都是如此。这意味着其他Bundle可以执行停止状态的Bundle中的代码,Bundle的设计者需要保证这样做是符合预期、没有危害的。一般来说,停止状态的Bundle只导出接口是比较合理的做法。为了尽可能保证不执行代码,导出接口的类构造器(<clinit>方法)中也不应该包含可执行的代码。
(6)卸载过程
调用Bundle.uninstall()方法可以实现Bundle的卸载,此时该Bundle的状态会转变为UNINSTALLED。
2.2.3 启动级别与活动启动级别
OSGi规范中定义了“启动级别”以满足对Bundle启动的控制。启动级别是一个非负的整数,值为0时表示OSGi框架还没有运行或框架已经停止(需根据具体上下文环境来区分这两种情况),只有Bundle ID为0的System Bundle的启动级别可以为0,除此以外,其他Bundle的启动级别都大于0(最大值为Integer.MAX-VALUE)。启动级别的数值越高,所代表的启动阈值就越高,即启动顺序会越靠后。另外,OSGi框架还有一个活动启动级别(Active Start Level),用于确定目前的状态下应该启动哪些Bundle。
OSGi框架的活动启动级别和Bundle的启动级别都可以在框架启动时设定,也可以在运行期间更改。在编码过程中,开发人员可以使用StartLevel接口中的以下方法来获取和调整Bundle的启动级别以及OSGi框架的活动启动级别:
❑setInitialBundletartLevel():设置Bundle的初始启动级别(Bundle初次安装时的启动级别)。❑getInitialBundletartLevel ():获取Bundle的初始启动级别。
❑setBundleStartLevel():运行时调整Bundle的启动级别。
❑getBundleStartLevel():获取Bundle当前的启动级别。
❑setStartLevel():设置OSGi框架的活动启动级别。
❑getStartLevel():获取OSGi框架的活动启动级别。
备注:OSGi容器活动启动级别的调整是一个渐进的过程。如果要将活动启动级别修改为一个新的值,我们将这个新值称为“请求启动级别”(Requested Start Level)。在OSGi容器启动或停止某些Bundle的期间,活动和请求的启动级别是不相等的,活动启动级别必须以步长为1的速度来增加或减少,逐渐接近并最后与请求启动级别相等。在活动启动级别向请求启动级别趋近变化的过程中,如果活动启动级别在逐渐增加,那么启动级别与框架活动启动级别相等的Bundle都会被启动(执行Activator.start()方法),直到达到请求启动级别为止(启动级别与请求启动级别相等的Bundle也会被启动)。如果活动启动级别在逐渐减少,那么启动级别与框架活动启动级别相等的Bundle都会被停止(执行Activator.stop()方法),直到达到请求启动级别为止(启动级别与请求启动级别相等的Bundle不会停止)。如果某个Bundle在启动或停止过程中抛出了异常,也不能中断活动启动级别的变化过程,但框架会广播一个FrameworkEvent.ERROR事件通知所有注册了框架监听器(FrameworkListener)的对象。当活动启动级别与请求启动级别相等之后,OSGi框架会广播出一个FrameworkEvent.STARTLEVEL-CHANGED事件通知所有框架监听器启动级别调整已经完成。
若以“A”代表活动启动级别的值,“R”代表请求启动级别的值。则活动启动级别向请求启动级别渐进调整的过程如下图:
2.2.4 事件监听
事件监听在OSGi中是一种很常见的设计模式,在Bundle生命周期的不同状态相互转换时,OSGi框架会发布出各种不同的事件供事先注册好的事件监听器处理,这些事件被称为“生命周期层事件”。OSGi框架支持的生命周期层事件包括继承于BundleEvent类的,用于报告Bundle的生命周期改变的Bundle事件,以及继承于FrameworkEvent类的,用于报告框架的启动、启动级别的改变、包的更新或捕获错误的框架事件。
2.2.4.1 事件类型
BundleEvent和FrameworkEvent中都定义了一个返回值为int的getType()方法,用于说明该事件对象代表的事件类型。相关类型及描述如下:
(1)Bundle事件所包含的事件类型和描述:
(2)框架事件所包含的事件类型和描述:
2.2.4.2 事件分派
OSGi规范定义了BundleListener和FrameworkListener接口来描述Bundle事件和框架事件的监听器。这两个监听器接口中分别包含了BundleListener.bundleChanged()和FrameworkListener.frameworkEvent()方法,接收到事件广播之后在这两个方法中进行相关处理,这些方法的处理动作都是默认异步执行的,不会阻塞Bundle和框架的状态转换过程。
OSGi分别通过BundleContext.addBundleListener()和BundleContext.addFrameworkListener()添加Bundle事件和框架事件的监听器到OSGi框架的事件监听列表之中,添加监听器的动作一般在Bundle的Activator类中实现。
2.2.5 系统Bundle
OSGi框架本身也会以一个Bundle的形式向其他Bundle提供资源、Package和服务,比如已经在书中多次出现的Bundle、BundleContext、FrameworkListener等接口,以及EventAdmin、PackageAdmin等服务都是由系统Bundle提供的。OSGi规范规定了系统Bundle的Bundle ID固定为0,Bundle的getLocation()方法返回固定字符串“System Bundle”,这些特征使得任何Bundle都可以很方便地从BundleContex.getBundle(0)或BundleContex.getBundle("System Bundle")方法中获取到系统Bundle的对象实例。在OSGi容器中,系统Bundle可以认为是一定存在的,每一个Bundle都默认依赖这个系统Bundle。
系统Bundle的生命周期和其他Bundle有所不同,OSGi规范对系统Bundle生命周期各过程执行的动作规定如下:
❑启动过程:Bundle的start()方法为空操作,因为OSGi框架一启动,系统Bundle就已经启动。
❑停止过程:Bundle的stop()方法会立即返回并在另外一条线程中关闭OSGi框架。
❑更新过程:Bundle的update()方法会立即返回并在另外一条线程中重启OSGi框架。
❑卸载过程:系统Bundle无法卸载,如果执行了Bundle的uninstall()方法,那么框架会抛出一个BundleException异常。
2.2.6 Bundle上下文
OSGi容器中运行的各个Bundle共同构成了一个微型的生态系统,Bundle的许多行为都无法孤立进行,必须在特定的上下文环境中才有意义,因为要与上下文的其他Bundle或OSGi框架进行互动。在代码中使用BundleContext对象来代表上下文环境,当Bundle启动的时候,OSGi框架就创建这个Bundle的BundleContext对象,直到Bundle停止运行从OSGi容器卸载为止。Bundle的许多操作都是需要通过BundleContext对象来完成的。常用方法有:
❑ BundleContext.installBundle():此方法用于安装一个新的Bundle到当前OSGi容器之中。
❑ BundleContext.getBundle():此方法用于从当前OSGi容器中获取已有的Bundle。
❑ BundleContext.registerService():此方法用于在OSGi容器中注册服务。
❑ BundleContext.getService():此方法用于从当前OSGi容器中获取已有的服务。
❑ BundleContext.addBundleListener()和Bundle-Context.addFrameworkListener():这两个方法用于在OSGi容器中注册Bundle和OSGi框架的事件监听器。
❑ BundleContext.getDataFile():此方法用于从Bundle的持久储存区中获取文件。
❑ BundleContext.getProperty():此方法用于获取Bundle所处环境的环境属性。
备注:每个Bundle的BundleContext对象都是在调用Bundle.start()方法时由OSGi框架创建并以方法参数的形式传入到Bundle中,实现Bundle时一般会把这个对象的引用保留起来,以便在Bundle其他代码中使用。
2.3 服务层规范与原理
服务层规范描述了OSGi框架下用户自定义服务和框架标准服务的注册、查找和使用等操作。
本质上一个服务就是一个普通的Java对象实例,一般来说,这个Java对象还实现了某个或某些接口。OSGi服务层的目的在于支持模块间对象级别的交互操作,而前面介绍的模块层和生命周期层则支持模块间包(类)级别的交互。
2.3.1 基本概念
❑服务(Service):服务是一个普通Java对象(POJO),这个对象很可能实现了一个或多个接口,并在OSGi的服务注册表中注册,其他Bundle可以通过服务注册表查找和使用它们。
备注:OSGi中默认的服务都是单例服务。
❑服务注册表(Service Registry):由OSGi框架提供,是所有Bundle共享的数据区域,保存了所有在系统中注册过的服务对象和相关信息,如服务属性、服务引用次数等。
❑服务属性(Service Properties):OSGi规范预定义了一些服务的标准属性用于进行服务标识,通常以Key-Value的形式存在(与Java语言中的哈希表键值约定不同,这里的Key值是不区分大小写的),在进行服务注册时可以由开发人员自主确定。
❑服务引用(Service Reference):指向服务对象的引用对象,它并不是真正的服务对象,而是在服务注册表中用来找到服务对象的一个指引,包含与该服务相关的属性。使用服务的Bundle通过该引用对象从自己的Bundle上下文中获得真正的服务对象。
❑服务注册(Service Registration):Bundle向服务注册表注册一个服务之后,会获得一个服务注册对象(BundleContext.registerService()方法的返回值),Bundle可以用利用这个对象来进行更新服务属性、注销服务等操作。
❑服务事件(Service Event):当服务对象被注册、修改、注销时,会产生相应的服务事件,并分派给相应的服务事件监听者。
❑服务事件监听者(Service Listener):用于监听发生的服务事件,在事件发生时进行相应的逻辑处理。
2.3.2 预定义属性
OSGi中预定义了一些标准属性,有的包含特定的用途;有的是用户根据需要在程序中加入的;有的由OSGi框架自动维护,用户获取服务时无需事先赋值就能直接使用。简介如下:
(1)“service.id”和“service.ranking”
这两个属性定义了在多个可选服务同时满足条件时,按照服务的优先级别选择。service.ranking的值是一个整数,它代表服务的优先级,如果开发人员明确设置了service.ranking,那么在查找服务时框架必须返回优先级更高(数值更大)的服务。如果没有对服务设置service.ranking属性或者对多个服务设置了相同的service.ranking属性,那么框架将使用service.id作为第二项优先级排序,但是这项优先级是按照升序排序的,越小的service.id优先级越高。service.id属性的值是一个长整数,它由OSGi框架按照服务注册的先后次序赋予,并且保证在OSGi框架中是唯一的。越小的service.id意味着服务越早注册。
(2)服务持久化ID
服务持久化ID(Persistent Identifier,PID)的作用与服务ID(service.id)类似,它们都是在OSGi框架中唯一定位服务的标识符号。与服务ID不同的是,服务持久化ID不是由OSGi框架自动产生,而是一个由用户直接指定的字符串,因此即使在提供服务的Bundle或整个OSGi框架重启甚至更换了不同的OSGi框架之后,服务持久化ID依然是不变的。服务持久化ID的Key值为“service.pid”,它的Value值必须是一个与其他服务持久化ID不重复的字符串。使用实现类名来作为服务持久化ID是比较常见的做法,例如Equinox所提供的PackageAdmin服务的持久化ID为:service.pid= 0.org.eclipse.osgi.framework.internal.core.PackageAdminImpl
(3)服务接口
OSGi框架会把服务实现类实现的所有接口自动存放在Key值为objectClass的预定义属性中,该属性的Value值是一个String[]数组,每个数组元素是一个接口的全限定名称,eg:example.service.IHelloService。服务接口的属性值不需要人工设置,OSGi框架会自动赋值。
(4)“service.description”和“service.vendor”
这两个属性用于描述该服务与服务提供者的信息,其Value值都是字符串,由用户设置,仅供人工阅读,框架程序本身不会直接使用。
备注:在注册服务的BundleContext.registerService()方法中,服务提供者可以通过最后一个参数Dictionary来传递保存自定义的服务属性配置。而该接口方法的返回值是一个ServiceRegistration接口,该接口中有个setProperties(Dictionary properties)方法,可通过该方法对注册时的服务属性进行修改(不过一般不这么搞~~)。
2.3.3 服务过滤
为了便于用户根据属性选择最佳的服务,OSGi框架提供了基于RFC 1960语法的属性过滤器支持。RFC 1960过滤器最初是用于支持LDAP查找,所以通常也被称为LDAP过滤器。其语法定义含义如下:
每个过滤器(<filter>)由一对小括号内的一个或多个过滤器组合(<filtercomp>)构成,每个过滤器组合又由逻辑符号(“&”代表与,“|”代表或,“!”代表非)开头的单个或多个过滤器构成。这是一组递归的定义,递归的出口是直到过滤器分解为最基本的过滤项(<item>)为止。过滤项可以表示为属性(<attr>)加比较符号(< filtertype >,包含等于“=”,大于“>=”,小于“<=”和相似于“~=”符号)再加属性值(<value>),其中属性值可以使用“NULL”或通配符“*”表示“不存在”或“任意的字符”。
例如: (&(objectClass=example.osgi.IHelloworld)(service.vendor=Fenixsoft*)):此表达式表示过滤出“objectClass”属性值为“example.osgi.IHelloworld”且service.vendor属性值是以“Fenixsoft”开头的服务。
(&(objectClass=org.osgi.service.http.HttpService)(port=80)):此表达式表示过滤出实现了HTTP Service服务接口并且开放了80端口的服务。
2.3.4 服务跟踪
OSGi提供的服务跟踪器由org.osgi.util.tracker.ServiceTracker类实现的,它的作用是监视服务何时被添加、何时被移除(在添加和移除服务时框架会自动调用addingService()和removedService()方法,用户可继承ServiceTracker类覆盖这两个方法以实现服务变动时的处理逻辑)以及在服务可用的时候获取服务实例(getService()、getServices()和waitForService()方法用于返回服务实例)。
备注:在使用ServiceTracker前,需要在BundleServiceUser的MANITEST.MF文件中引入org.osgi.util.tracker包。
2.3.5 服务层事件
(1)事件类型
OSGi框架中在ServiceEvent类中定义了一个返回值为int的getType()方法,用于表明事件对象所代表的事件类型。服务层事件所包含的事件类型及含义如下表:
(2)事件分派
服务层的事件分派过程与生命周期层有一个显著区别:生命周期层的事件分派是异步的,而服务层所有的事件分派都是同步的。事件被设计为同步还是异步,主要取决于框架是否要依赖事件监听器的处理结果来决定下一步的执行逻辑。OSGi规范定义了ServiceListener接口的serviceChanged()方法来实现监听到服务变化时的执行逻辑。
备注:ServiceTracker能感知到服务添加和移除的变化,这项功能就是依靠服务层事件来完成的。ServiceTracker的内部类Tracked和AllTracked实现了ServiceListener接口的服务事件监听器,在它的open()和close()方法中,分别调用BundleContext.addServiceListener()和BundleContext.removeServiceListener()把监听器添加到OSGi框架中和从OSGi框架中移除
2.3.6 远程服务
远程服务过去叫做分布式OSGi(Distributed OSGi),在RFC 119中定义。它是一项用于连接多个运行着OSGi框架的Java虚拟机进程的交互技术。远程服务(Remote Service)并非服务层规范的内容,在目前OSGi核心规范中,主要是围绕单个Java虚拟机进程内的系统架构进行定义的。涉及分布式等内容的一般放在服务纲要规范(Services Compendium Specification)或企业级规范(Enterprise Specification)之中,这里仅以实用性优先为原则,将相关内容合并到服务层规范中进行简单介绍。
远程服务属性:
与本地服务有预定义属性一样,远程服务也有若干个预定义的属性,列举如下:❑service.exported.interfaces是远程服务的标志性属性。OSGi框架通过此属性得知开发人员希望将此服务发布为远程服务,它同时标识着远程服务开放给外界的接口。例如将此属性设置为“example.osgi.IHelloworld”,在导入服务时,就只有符合过滤器规则“(objectClass=example.osgi.IHelloworld)”的服务才会匹配成功。当此参数的值被设置为“*”时,就意味着OSGi框架会自动导出服务实现类继承树上的所有类型,包括它实现的所有接口和它各个层次的父类。
❑service.exported.configs指明了导出服务时使用哪一种配置类型(Configuration Type)。配置类型是OSGi从SCA(Service Component Architecture,它是SOA架构的关键组成部分)中借鉴的概念,它用于确定使用哪一个Endpoint来发布远程服务。例如在Apache CXF-DOSGi启动时就注册了“org.apache.cxf.ws”的配置类型,它确定了要使用Apache CXF WebService来进行交互的Endpoint类型。后面跟随的第三个参数“org.apache.cxf.ws.address”并非OSGi远程服务的预定义参数,它是一个用户自定义参数,仅有CXF-DOSGi会使用它,用于告知框架发布Web服务的具体地址。
❑service.intents、service.exported.intents和service.exported.intents.extra
这三个是关于“服务意向”(Intents)的预定义属性。与配置类型一样,服务意向也是OSGi从SCA策略规范中引进的概念,它是某项能力的抽象描述,其作用是在分布式系统中使用一个单词来代表服务所能提供或所需要的一项能力(Capability)。例如某服务可以声明自己能够提供日志记录的能力,而从另一个角度讲,其他服务可以提出约束:框架中必须有服务提供日志记录的能力才能够使其正常运作和导出,这两个服务就可以分别在自己的service.intents和service.exported.intents中声明所能提供和所需要的服务意向。
❑service.imported属性由框架自动设置,用于确定一个服务是否已经被导入。
2.3.7 服务钩子
服务钩子(Service Hooks)是开发人员干预OSGi框架和具体服务间交互过程的工具,最初在RFC 126中定义,在OSGi R4.2版中正式列入OSGi规范。
每一种服务钩子本质上都是一个OSGi服务,也就是说注册钩子的API和过程与注册一个普通OSGi服务没有区别,都是通过调用BundleContext.registerService()方法来实现的。服务钩子和普通服务的区别在于它们分别实现了特定接口的服务。目前OSGi规范中定义了3种类型的服务钩子:
(1)EventListenerHook
EventListenerHook通常用于处理服务状态变化。
它要求必须实现org.osgi.framework.hooks.service.EventListenerHook接口。
当一个服务注册为EventListenerHook之后,如果OSGi框架中有服务注册、修改和注销操作,那么这个服务钩子的event()方法将会被框架自动调用,并且调用会在所有服务事件分派之前发生。
EventListenerHook的event()方法有两个参数,分别为:
❑event,一个ServiceEvent对象,表示将要分派给各个监听器的服务事件。
❑listeners,一个由Key为BundleContext和Value为Collection<ListenerInfo>而构成的Map对象,表示服务事件将要分派到的事件监听器。
在EventListenerHook.event()方法的实现代码中可以移除Map或Collection(Map的Value值)中的某些元素,这样事件将不会分派至被移除的监听器中;但是不允许再向Map或Collection对象中添加新的元素,即不允许调用Map.put()、Map.putAll()、Collection.add()和Collection.addAll()等方法,如果这样做,那么将会收到由OSGi框架抛出的UnsupportedOperationException异常。
(2)FindHook
FindHook通常用于获取服务请求。
它要求实现org.osgi.framework.hooks.service.FindHook接口。
当一个服务注册为FindHook之后,如果OSGi框架中有Bundle请求服务(比如调用了BundleContext.getServiceReferences()方法),那么框架将会自动调用这个服务钩子的find ()方法,并且该调用会先于ServiceReference对象的返回。
FindHook的find()方法有5个参数,分别为:
❑context,BundleContext对象,表示调用getServiceReference()等方法的Bundle的上下文。❑name,字符串,表示Bundle请求服务时传入的类(接口)名。
❑filter,字符串,表示Bundle查找服务时传入的过滤器值。
❑allServices,布尔值,值为真时表示Bundle请求服务使用的是BundleContext.getAllServiceReferences()方法。
❑references,Collection <ServiceReference< ? >>对象,表示最终返回给请求服务的Bundle 的ServiceReference对象集合。
FindHook.find()方法的实现代码中可以根据需要移除其中某些对象以实现对请求服务的Bundle隐藏某些服务的需求;但是不可以在集合中添加任何对象,否则将会收到由OSGi框架抛出的UnsupportedOperationException异常。
(3)ListenerHook
ListenerHook通常用于服务监听器变化。
它要求实现org.osgi.framework.hooks.service.ListenerHook接口。
当将一个服务注册为ListenerHook之后,如果OSGi框架中有Bundle注册或注销了服务事件监听器(比如调用了BundleContext.addServiceListener()或removeServiceListener()方法),那么这个服务钩子的added()或removed()方法将会被框架自动调用。有一点需要特别说明,对于added()方法,一旦ListenerHook注册到OSGi框架中,它便后会立刻被框架调用,从而得到在注册这个服务钩子之前就已经存于OSGi框架中的服务监听器对象。
ListenerHook的added()和removed()方法都只有一个参数:listeners,它是一个Collection<ListenerInfo>对象,表示刚加入OSGi框架或刚从OSGi框架中删除的一组监听器。与前面两个服务钩子不同的是,ListenerHook的added()和removed()方法的实现代码中既不可以在集合中添加任何对象,也不能从集合中删除任何对象,否则将会收到由OSGi框架抛出的UnsupportedOperationException异常。
备注:服务钩子的用途:服务钩子最直接的用途就是隐藏各种服务相关的对象(ServiceEvent与ServiceReference对象)。另外,除了简单的隐藏服务对象之外,服务钩子还有很多更深层次的应用,例如对服务实现一些类似AOP(Aspect Oriented Programming,面向切面编程)的功能:首先将原有服务隐藏,然后建立该服务的动态代理,再为请求服务的Bundle返回经过代理后的服务,这样便可在不侵入原有服务的前提下对服务实现一些增强逻辑。
3. 附录
3.1 Java类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。
从Java虚拟机的角度来讲,只存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(此处只限于HotSpot),是虚拟机自身的一部分;
另外一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,可以更细致的划分为三种类加载器:
❑启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在<JAVA-HOME>\lib目录中的、或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名称不符合的类库即使放在lib目录中也不会被加载)类库到虚拟机内存中。启动类加载器无法被Java程序直接引用。
❑扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA-HOME>\lib\ext目录中或者被java.ext.dirs系统变量所指定的路径中所有的类库,开发者可以直接使用扩展类加载器。
❑应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现。因为这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载在用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
备注:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。通俗来说就是,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义;否则,即使这两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就必定不相等。这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等情况。
3.2 双亲委派模型
类加载器的双亲委派模型在JDK 1.2期间被引入并被广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的一种类加载器实现方式。
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,其大致逻辑为:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器;如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(其搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
备注:因为双亲委派模型的存在,所以当自己写一个与rt.jar类库中已有类重名的Java类时,即使可以正常编译,但永远无法被加载运行。可能会由虚拟机抛出“ java.lang.SecurityException: Prohibited package name: java.lang”异常。
双亲委派模型的三次“被破环”:
(1)第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader在JDK 1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者们引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass()。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
(2)第二次“被破坏”是由这个模型自身的缺陷所导致的。双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被用户代码调用的API,但如果基础类又要调用回用户的代码,使用双亲委派模型是无法解决的。一个典型的例子就是JNDI服务,JNDI的代码由启动类加载器去加载(在JDK 1.3时代是放进去rt.jar的),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但双亲委派模型下的启动类加载器是无法识别这些代码的。为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,如:JNDI、JDBC、JCE、JAXB、JBI等。
(3)双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。这里所说的“动态性”指的是当前一些非常“热”门的名词:代码热替换(HotSwap)、模块热部署(Hot Deployment)等。OSGi实现模块化热部署的关键就是它自定义的类加载机制,其类加载过程(见2.1.4.2)中很多都是在平级的类加载器中进行查找的,都是不符合双亲委派模型的。
主要参考资料:
《深入理解OSGi:Equinox原理、应用与最佳实践》---周志明;谢小明
《Java应用架构设计:模块化模式与OSGi》--- Kirk Knoernschild
https://docs.osgi.org/