许多Bundle可以共享一个虚拟机(VM),具体可参考[1] Java Virtual Machine Specification, Second Edition。在VM内部,Bundle可以与其他Bundle之间隐藏package/class,以及共享package。
隐藏和共享package的关键因素由java类加载器实现,类加载器则通过bundle-space详细定义的规则加载类。每一个Bundle只会有一个单独的类加载器,类加载的代理网络结构模型如图3.4。
图 3.4 类加载代理模型
类加载器可以通过如下方式加载class和resource:
类空间是指一个给定的bundle类加载器可以访问到的所有的类。故,一个指定的Bundle类空间包含如下内容:
类空间必须具有一致性,也就是说不能存在同名的2个class(为了防止类声明错误),但是,在OSGi框架中,不同的类空间可以存在同名的类。在模块层,支持不同版本的类,加载到相同的虚拟机中。
图3.5展示了BundleA的类空间,BundleA的右侧顶部不在类空间内,因为这部分描述被export替代的内容BundleA自身不可访问。
图3.5 类空间
在类加载过程中,Framework需要完成一系列的职责,在使用Bundle之前,必须对共享的package约束关系进行解析,选择一个最合适的类并创建wiring,具体可以参考3.8解析过程一节,在3.9运行期类加载一节中也描述了一些运行期的特性。
Framework必须支持Bundle解析。解析过程是指如何处理import包和export包的wire关系。解析过程需要满足约束条件(manifest中的Import/Export Package, Require-Bundle, Fragment-Host),而且resolving必须在Bundle中的代码加载或运行之前。
Wire是指exporter 和importer(都是Bundle)之间的实际关联关系,wire还关联到一系列的约束条件,这些约束都在manifest中定义,一个有效的wire必须满足对应所有约束,下图3.6描述了wiring模型的类结构
图3.6 wiring模型的类结构示例
这一节定义了manifest header为resolver提供的metadata。
Bundle manifest必须指定version,而且需要遵循OSGi manifest header语法,于是这个version由Bundle-ManifestVersion header提供。使用本规范以及后续版本的Bundle必须指定Bundle-ManifestVersion,语法如下:
Bundle-ManifestVersion ::= number // See 1.3.2
Framework中Bundle的manifest版本必须是’2’,以前版本Bundle的manifest版本为’1’,通常在这样的情况manifest中是无法表描述的。因此,如果版本值不为’2’时,Framework必须明确指明支持后续版本,否则都是无效的标记。
OSGi Framework的实现也可以支持没有Bundle-ManifestVersion的manifest,这样可以与Framework 1.2兼容。
版本为’2’的Bundle必须指定Bundle的Symbolic Name,而且不需要指定Bundle的版本,这个header头可以有默认值。
Bundle-SymbolicName manifest header必须指定。通过Bundle的symbolic name和bundle version确定一个唯一的Bundle;也就是说2个Bundle有同样的symbolic name和bundle version,那么这2个Bundle在Framework中是同一个Bundle。具体参考4.4.1 Bundle标识符
唯一的symbolic name由开发者确定(manifest中的Bundle-Name设计需要具有可读性),Bundle-SymbolicName语法如下:
Bundle-SymbolicName ::= symbolic-name
( ';' parameter ) * // See 1.3.2
Framework必须识别如下Bundle-symbolicName中的指令:
Bundle-Version是一个可选header,默认值是0.0.0
示例:Bundle-Version ::= version // 参考 3.2.5
如果小版本号(minor)和微版本号(micro)没有指定,那么默认值为0。如果限定部分没有指定,那么默认值是空字符串("")。
版本是可以进行比较的,比较规则按主版本号、小版本号、微版本号的顺序依次进行。最后是字符串的限定符比较。
示例:
Bundle-Version: 22.3.58.build-345678
Import-Package header定义了共享包的import约束。Import-Package的语法如下:
Import-Package ::= import ( ',' import )*
import ::= package-names ( ';' parameter )*
package-names ::= package-name
( ';' package-name )* // 参考 1.3.2
在header中可以定义多个import package,每条import定义描述了一个bundle中单独的package,多个package以分号方式分割。
Import package指令如下:
为了允许import package(除java.开头的package),Bundle必须有PackagePermission[, IMPORT]。更多信息参考package权限一节。
当出现以下任意一个错误时将会终止安装或者更新:
3.6.5 Export-Package
Export-Package头语法与Import-Package头的语法类似,只是一些指令和属性不同。
Export-Package ::= export ( ',' export )*
export ::= package-names ( ';' parameter )*
package-names ::= package-name ( ';' package-name )* // 参考 1.3.2
这个header允许多个package被export。每条export定义描述了一个bundle中单独的package,多个package以分号方式分割。多个重复定义的export package是允许的(不同的import属性都需要这些不同的export 版本)。
Export指令:
以下属性是这节规范的一部分:
另外,特定的匹配属性可以被指定。Framework会自动关联每个export package,export package定义如下属性:
export定义并不是自动import定义。一个export package但是没有import package的Bundle,将会通过bundle的class path获得package。对于只有export package的Bundle,这个Bundle只能被其他Bundle使用,不会接受从另外一个Bundle来的package,这个Bundle要import package优先从自己的class path中选择。
对于export package,Bundle必须有PackagePermission[, IMPORT]。
示例:
Export-Package: com.acme.foo;com.acme.bar;version=1.23
Bundle之间协作要求使用相同的classloader用于协作类型。如果多个Bundleexport的package放在不相交的类空间,那么他们之间无法协作。当Bundle需要import exported 的package时需要明显改善协作,而这些import允许framework来代替export和import。
不能通过替换来增强协作,导入export package只能工作于没有实现细节API的环境,故只在以下几种情况Import of exported packages:
在实际中,importing exported packages只能使用干净的API实现分离。OSGi服务都是尽可能的设计成独立的。很多库的API和实现混合在一起并且在同一个package下以至不能很好替换API package。导入一个export过的package必须根据兼容性要求提供一个版本范围,无论是consumer或者provider的API。具体参考3.7.3语义版本一节
如果Bundles的Bundle-ManifestVersion的值不是2或比2大,则必须按照版本3中定义的header头。框架必须将版本3中的头映射为版本4中合适的header:
OSGi Framework package resolver提供了一系列的匹配import和export的机制,本节详细描述这些机制。
线(wire)和节点(node)组成图标,和Bundle一样,包含了大量重要信息。在下一节中,采用以下约定来阐述详细信息:
Bundle的名字为A,B,C…(即从A开始的大写字母)。Package名字则使用p, q, r, s, t,...(即从p开始的小写字母)。如果version很重要,那么在后面在一个短横线,如q-1.0。语法A.p表示Bundle A中定义了(import或export) package p。
import采用白色框来表示,export则使用黑色框来表示,没有import和export的包称之为private package,用斜线网格背景表示。
Bundle是一系列关联方框的集合。Bundle之间的关联用线(wire)表示,而约束条件则写在这些线上。
图3.7
例如:
A: Import-Package: p; version="[1,2)"
Export-Package: q; version=2.2.2; uses:=p
Require-Bundle: C
B: Export-Package: p; version=1.5.1
C: Export-Package: r
图 3.8 描述了A,B,C这3个Bundle的关系
图 3.8 Bundle图示例
版本约束定义import采用精确的版本号或者版本范围描述来匹配export。
一个import定义必须指定一个(import)版本范围,就像属性version一样,并且输出者必须指定一个(export)版本属性。版本范围匹配规则具体参考3.2.6版本范围一节。
例如以下的import和export定义可以被正常解析:
A: Import-Package: p; version="[1,2)"
B: Export-Package: p; version=1.5.1
如图3.9描述了这个约束关系
图3.9 版本约束
版本范围通过编码考虑兼容性,OSGi framework并没有定义具体兼容性编码原则,强烈建议使用下面的语义。
传统意义上,兼容性是指2者之间的相容性,一个是指consumer的代码,另一个是指provider的代码,API兼容性基础设计具体为3部分:
Provider API是一个密切相关的API,几乎所有的API变化,都会使得provider需要实现不兼容的新版本的API,然而,API的变化从consumer角度则会有更多的回旋余地。许多API可以改变consumer的向后兼容性,但几乎没有任何API可以改变provider的向后兼容。
provider API和consumer API的兼容规则如下:
Consumers和Providers都应该使用他们的编译版本作为基础版本,建议忽略微版本号,当修复部署最近的bug微版本号时会使得系统变得生硬,例如,编译版本4.2.1.V201007221030,基础版本应该为4.2
一个Consumer API应该import基础版本的起始和主版本号的结束,如[4.2,5)。Provider应该import到下一个次版本号的结束,如[4.2,4.3)。
在语义版本中提到客户端API package有2种角色:
API consumers 和API providers。API消费者为API接口或者抽象类,API提供者则是API实现。对API提供者来说清晰的API文档描述非常重要,因为API提供者需要根据文档来实现API。
Bundle可以指明它不需要正确解析的package,但是这个package如果可用则使用之。例如,登录过程是很重要的,但是如果没有登录服务,Bundle也是可以正常运行的。
可选import package可以通过以下方式指明:
下面的示例显示了Bundle B没有提供正确可匹配版本的情况下,Bundle A也能够被解析.
A: Import-Package: p; resolution:=optional; version=1.6
B: Export-Package: p; q; version=1.5.0
图 3.10 可选import
在Bundle实现中,需要考虑可选package没有加载的情况,如果抛出一个找不到package的异常信息,这可以帮助在Bundle classpath中对package做回退处理。当可选package不能被resolve时,任何通过这个package尝试加载的import package都是不存在的。
Bundle的package类可以通过Bundle classpath或者动态加载。
一个类可以依赖于其他package中的类。例如,继承自其他package中的类,或者这些类出现在方法声明中,这种情况可以称为一个package使用其他package。这种package之间的关系通过在Export-Package中使用uses指令实现。
例如,org.osgi.service.http依赖使用javax.servlet,他们之间的这个关系描述成org.osgi.service.http;uses:= "javax.servlet"
为了保证类空间的一致性,需要设置Bundle中的每一个package只有对应的一个export。例如,Http服务实现需要继承javax.servlet.http.HttpServlet这个基础类,如果Http服务Bundle需要import版本2.4而实际import了2.1版本时,必然会发生class cast异常。如图3.11
图 3.11 Uses 指令A,B需要使用D中的javax.servlet
如果Bundle从exporter中import package,那么export可以通过uses指令定义一些对其他package的约束。uses指令列出了exporter中依赖的一系列package,这些约束保证了这一系列Bundle对于相同的package共享同一个classloader。
如果importer导入了含有uses约束的package,resolver必须通过约束来建立import和export之间的连接。Exporter也有同样的约束。单个import package和exporter之间可以有很多约束。
implied package约束是指通过递归循环构建的约束集合,Implied package约束不是自动导入。更进一步,implied package约束只包括了必须解析的内容(在import中定义描述的内容)。
如图3.12,Bundle A import package p,这里假设定义的p连接到Bundle B。由于uses指令(这里省略uses指令描述)隐含了对package q的约束。进一步说,假设对package q的import关联到Bundle C,那么也就隐含了对package r和s的约束。接下来,假设C.s和C.r分别连接到Bundle D和E,那么这两个Bundle都将package t添加到Bundle A的依赖包集合中。
图3.12 Implied Packages
为了维护类空间的一致性,Framework必须确保Bundle的import不能和任何Bundle的implied package存在冲突。
例如,Framework必须保证A.t的import定义连接到D.t。这时如果import定义连接到package F.t,这就违背了类空间一致性。由于在Bundle A中同时存在来自Bundle D和F的类,这会导致ClassCastException。或者,如果让所有Bundle都连接到F.t,则也可以解决这个问题。
另一种情况如图3.11,Bundle A import了B中的Http服务,Bundle B包含org.osgi.service.http和the javax.servlet,而Bundle A和Bundle B都有连接到javax.servlet的约束。
下面的uses指令的设置导致不能正常解析示例:
A: Import-Package: q; version="[1.0,1.0]"
Export-Package: p; uses:="q,r",r
B: Export-Package: q; version=1.0
C: Export-Package: q; version=2.0
当A.q连接到B.q,而不是C.q时,这个时候能够解析。如果添加Bundle D则会导致不能解析:
D: Import-Package: p, q; version=2.0
D.q必须连接A.p,但是,A.p中包含uses指令,以至A.q连接到B.q-1.0,而D.q需要2.0版本,这违背了类空间约束。
图3.13描述了这种情况
图3.13 uses指令和解析
属性匹配允许importer和exporter通过说明性方式影响匹配过程处理。为了使得import定义可以解析为export定义,import中定义的属性值必须要和export中定义的属性值匹配。在默认情况下,如果export中包含的属性没有出现在import定义中,匹配过程将会继续进行。如果在export定义中指定了mandatory指令(强制),Framework会在import中强制匹配这些属性。在resolve阶段,对于出现在字段DynamicImport- Package中的任何信息都是会被忽略处理的。
例如,下例的语句是匹配的:
A: Import-Package: com.acme.foo;company=ACME
B: Export-Package: com.acme.foo; company="ACME"; security=false
所有属性值都是通过字符串方式进行比较(忽略属性中开始和结尾部分的空格),除了version和bundle- version属性。version和bundle- version采用版本范围的比较方法。
属性有两种类型:mandatory(强制) 和optional(可选)。mandatory属性表示必须匹配的属性。optional属性表示在import时可以不考虑的属性,属性默认值为optional。
exporter可以在export定义中通过mandatory指令指定mandatory属性(指令包含了一个逗号分割的属性名称列表,表示在import描述中必须强制匹配的属性)。如下所示,import package和export package是不匹配的,由于在Bundle A中没有指定security属性:
A: Import-Package: com.acme.foo;company=ACME
B: Export-Package: com.acme.foo; company="ACME"; security=false; mandatory:=security
exporter可以在export定义使用include和exclude指令限制类的访问范围。这两条指令的值都是逗号分割的类名列表。需要注意的是,逗号需要用双引号包括起来。类名不能包含package name,而且不能含有后缀.class。即类com.acme.foo.Daffy在指令中名称为Daffy。类名中可以含有通配符(’*’ \u002A)。
include指令,默认值为通配符(’*’ \u002A)(表示匹配所有的名称),即所有的类和资源。
exclude指令,默认值为空(表示没有匹配的名称)。如果指定了这两个指令的值,那么默认值就被覆盖了。
如果满足以下条件,表示类是可见的:
package org.acme.open;
public class Specified {
static Specified implementation;
public void foo() { implementation.foo(); }
}
package org.acme.open;
public class Implementation {
public void initialize(Specified implementation) {
Specified.implementation = implementation;
}
}
对于扩展Bundle的实现类必须是不可见的。这时可以将实现类排除在外,使得只有export Bundle可以访问这个类。export定义的标记描述如:
Export-Package: org.acme.open; exclude:=Implementation
Provider selection允许importer选择一个Bundle作为exporter。如果importer和exporter之间没有任何约束,那么使用Provider selection,而且importer与一个特定的exporter紧密关联,一般情况下是用于Bundle测试。为了降低连接的脆弱性,importer可以指定一个可选的版本范围。
Importer可以通过import的bundle-symbolic-name、bundleversion属性来选择一个exporter,并且在Framework中能够自动为每个export定义提供这些属性,但是这些属性不能在export定义中进行指定。
Export中的bundle-symbolic-name属性是指Bundle的符号名称,对应在header头中的Bundle-SymbolicName。bundle-version对应header头中的Bundle-Version值,默认值是0.0.0。
bundle-symbolic-name采用属性匹配方式,bundle-version则是以版本范围方式比较。Import必须是一个版本范围而export是一个版本号。
示例,下面的定义是可以匹配的:
A: Bundle-SymbolicName: A
Import-Package: com.acme.foo; bundle-symbolic-name=B; bundle-version="[1.41,2.0.0)"
B: Bundle-SymbolicName: B
Bundle-Version: 1.41
Export-Package: com.acme.foo
下面的示例是不能匹配的,因为Bundle B没有指定版本号,默认版本为0.0.0:
A: Bundle-SymbolicName: A
Import-Package: com.acme.foo; bundle-symbolic-name=B; bundle-version="[1.41,2.0.0)"
B: Bundle-SymbolicName: B
Export-Package: com.acme.foo;version=1.42
将package和Bundle强耦合在一起后,通过符号名称来选择一个exporter会导致结果脆弱。例如,如果一个exporter被重构为多个Bundle,那么所有涉及的importer都将需要修改,其他任意属性匹配由于独立性而不存在这个缺陷。
Bundle重构时由符号名称带来的脆弱性问题,可以使用与原Bundle同名的façade Bundle来解决部分问题。
Resolving是指Bundle之间建立连接的处理过程,连接之间的约束由以下条件静态定义:
Bundle如果满足以下条件,则可以被resolve:
解析过程是一个约束求解算法,使用关联关系进行描述,解析过程也是一个求解空间的迭代搜索过程。
如果一个模块(module)在import和export中有相同的package定义,那么Framework需要决定如何选择,首先必须处理重复的import定义,下面是可能的结果:
遇到下面的情况Bundle可以被resolve:
连接建立需要满足以下几个条件:
下面列表定义了优先级别,如果有多个选择,则根据优先级降序排列: