3.9运行期类加载(runtime class loading)
每个安装到Framework的Bundle在resolve之后才会关联classloader。Bundle在resolve之后,Framework必须为每一个Bundle(非fragment的)创建classloader。Framework也可以延迟创建classloader,在实际需要的时候才创建。每个Bundle对应各自的classloader这种机制,使得Bundle中所有资源对于同一个package下的其他资源具有访问权限,这个classloader为每个Bundle提供了自己的命名空间,用来避免命名冲突,并允许Bundle之间进行资源共享。Classloader必须通过解析过程中建立的连接关系来寻找适当的exporter。如果一个类没有在import中找到,那么根据manifest中定义的additional place进行查找。
这节定义了影响运行期类加载的因素,类和资源加载时Framework必须遵守的查找顺序。
3.9.1 Bundle Class Path
JAR, ZIP, directories, etc.称为container。Container是包含分层组织结构的入口。在运行期,一个Bundle的入口可以来自不同的container(如fragment Bundle)。入口的查找顺序很重要,因为可以影响到其他入口,对于Bundle,查找入口的顺序如下:
首先,(host)Bundle的container
然后,(optional)id自增的fragment container
查找顺序被称为入口路径(entry path)。resource(或者class)不通过entry path加载,而是通过Bundle classpath加载。Bundle classpath提供了额外间接的entry path,定义了container path的有序列表,每一个container path都能够在entry path中找到。
Bunlde-ClassPath语法如下:
Bundle-ClassPath::= entry ( ’,’ entry )*
entry ::= target ( ’;’ target )*
( ’;’ parameter ) *
target ::= path | ’.’ // See 1.3.2
Framework必须忽略任何不能识别的参数。
如果在Bundle- Classpath中指定的目标(目录或者是JAR文件)不能在需要的时候进行定位,那么框架必须忽略这样的目标,这在bundle解析之后随时可能发生。然而,在这种情况下,框架还应该发出一个类型为INFO的框架事件(Framework Event),带有关于它无法定位的每个条目的信息。
在定位bundle的类路径时,框架必需从bundle的JAR文件的根开始进行相对定位。如果一个类路径不能在bundle中定位,框架必需试图在附加的fragment bundle中进行定位。附加的bundle fragment根据bundle ID升序查找。这就允许fragment将一些条目插入到附主的Bundle- Classpath中。
如下示例:
A: Bundle-Classpath: .,resource.jar
B: Fragment-Host: A
前面的示例使用一个有效的Bundle classpath:
/, resource.jar, B:/
第1个/表示container的根路径,Bundle通常都有根路径。第2个元素是指在host Bundle的container中找,如果没有找到,将在B的container的entry path找。Framework必须使用第一个记录去匹配,最后一个元素表示fragment B有有效的bundle classpath,/是默认值(如果Bundle-ClassPath没有指定),然而,一个fragment能够引用到内部入口。假设,fragment B包含resource.jar,而Bundle A没有resource.jar,查找顺序如下:
A:/
B:resource.jar
B:/
如果container不能被定位时,Framework在处理Bundle resolved之后,任何时候都必须忽略bundle class-path中的container path。然而,Framework会发布一次INFO类型(为不能被查找的entry提供适当的消息)的Framework Event。
Bundle-ClassPath中的每条记录都引用至container中的目录,但是不可能确保目录是存在的。例如,在JAR/ZIP文件中可以没有目录,在这种情况下,Framework必须检查resource中目录的存在性,也就是说,假设这个目录内resource能够被找到,即使container中缺少目录结构也会选择这个resource作为Bundle-ClassPath。
host Bundle能够允许fragment中插入代码,Bundle-ClassPath由这个fragment提供,在主Bundle classpath中Fragment不能单方面插入代码,下面的示例详细说明了可能的Bundle classpath:
A: Bundle-SymbolicName: A
Bundle-ClassPath: /,required.jar,optional,default.jar
content ...
required.jar
default.jar
B: Bundle-SymbolicName: B
Bundle-ClassPath: fragment.jar
Fragment-Host: A
content ...
optional/
content ...
fragment.jar
Bundle classpath中的元素名称明确了他们的意图,required.jar提供了必须的功能,他的package在Bundle A中,optional container是指一个包含了optional类的目录,default.jar是JAR的备份代码,在这个示例中,有效的bundle class path如下:
A:/
A:required.jar
B:optional
A:default.jar
B:fragment.jar
将会按以下逻辑顺序查找资源X.class:
A:/X.class
A:required.jar!X.class
B:optional/X.class
A:default.jar!X.class
B:fragment.jar!X.class
(!) 表示从JAR resource中加载。
3.9.2 动态导入包(Dynamic Import Package)
动态import和export定义进行匹配(形成package连接)是在类加载过程的时候进行的,期间不会影响模块的解析。
动态import只会作用于没有建立连接,而且又找不到其他定义的package,故动态import是最后的解决方法。
Dynamic Import- Package中没有任何指令,但是可以指定一些匹配属性,下面的属性会由Framework进行匹配:
version—版本范围,用于选择export定义的版本。默认值为0.0.0。
bundle- symbolic- name — export bundle的symbolic- name。
bundle- version — 版本范围,用于选择Bundle版本。默认值为0.0.0。
Package命名可以明确指定,也可以含有通配符(如org.foo.* and.)。通配符后缀可以表示任何标记,包括多个子package。如果使用了通配符,那么package前缀标识不能被包含进去。(如org.foo.,包含所有org.foo下的子package,但是不包含org.foo)
Dynamic import必须按指定的顺序查找,在通过通配符指定package名的情况下,按指定的顺序查找很重要。排序器在匹配的时候进行排序,即,越是明确指定的package,出现的也越靠前。例如,下面的例子指定了优先选择由ACME公司提供的包。
DynamicImport-Package: *;vendor=acme, *
如果多个package需要对同一个参数进行动态导入,name可以在参数设置之前指定多个package(多个package之间由分号分割)。
在类加载过程中,类所在的package根据指定的包名(可能包含通配符)进行加载。每一个匹配的包名循环使用,尝试使用Import- Package同样的规则与exporter建立连接。如果连接成功(考虑所有uses约束),搜索结果将转发到export的类加载器,之后继续进行类加载过程。这个连接在之后的过程中都不能被修改,即使类加载失败。即,一旦包被动态解析了,那么随后的类或者资源的加载和普通import是没有区别的。
为了将DynamicImport- Package解析为导出声明,动态导入中定义的所有属性必须和导出声明中的属性匹配。所有的强制任意属性(由exporter指定,参见强制属性)必须在Dynamic import定义中指定和匹配。
一旦建立了连接,在以后的动态导入中必须遵循exporter的任何uses约束。
Dynamic import非常类似于optional package,参见optional package一节,但它是在bundle解析之后处理的。
3.9.3 父级委托代理(Parent Delegation)
Framework必须将java.开头的package委托给parent classloader处理。有一些Java虚拟机(含Oracle公司的虚拟机),经常会错误的委托代理给parent classloader,这种严格分层的classloader delegation通常会造成NoClassDefFound错误。如果虚拟机希望从任意classloader中都能找到自己实现的类,这需要严格要求boot classloader加载java.* package的类,否则会发生NoClassDefFound错误,而其他需要通过boot classloader加载的package可以通过系统属性(org.osgi.framework.bootdelegation)设置,格式如下:
org.osgi.framework.bootdelegation ::= boot-description
( ',' boot-description )*
boot-description::= package-name // See 1.3.2
| ( package-name '.' )
| ''
.表示可以进行深度匹配,如(com.acme.表示将匹配所有com.acme下的子package,但是不包含com.acme这个package)。匹配到的package都将被parent classloader加载(java.前缀的package不需要声明,默认被parent classloader加载)。
单个通配符表示Framework必须先delegate给parent classloader,和R3版本一样,例如,希望运行于Oracle虚拟机之上,可以指定如下的值:
org.osgi.framework.bootdelegation=sun.,com.sun.*
通过这个属性值,Framework必须将java.,sun.和com.sun.* package委托给parent classloader加载。
3.9.4 完整查找顺序
在class和resource加载过程中Framework必须遵守以下规则,当请求一个Bundle classloader进行class加载或者查找resource时,必须按以下顺序执行:
1.如果class或者resource在java.* package中,那个将委托代理给parent classloader处理,否则直接进入第2步;如果parent classloader found class失败,那么查找过程结束。
2.如果class或者resource在boot delegation中有定义(org.osgi.framework.bootdelegation),那么将委托给parent classloader加载,加载成功则结束,否则进入第3步。
3.如果class或者resource在Import-Package定义,或者之前通过动态导入加载的,那么请求转发给export Bundle的classloader加载,否则进入下一步。
4.如果class或者resource所在的package是使用Require- Bundle从一个或多个其他bundle进行导入的,那么请求将转发给那些Bundle的classloader,按照Bundle的manifest中指定的顺序进行查找。如果没有找到class或者resource,搜索继续进行。
5.在Bundle的内部classpath内查找,具体参考3.9.1Bundle class Ptah。如果class或者resource没找到,那么继续下一步。
6.查找每一个附加的fragment的内部classpath,fragment的查找根据bundle ID顺序升序查找。如果没有找到class或者resource,查找过程继续下一步。
7.如果class或者resource的package由Bundle export,或者package由Bundle Import(使用Import-Package或者Require-Bundle),查找结束,class或者resource没有找到。
8.否则,如果class或者resource所在package是通过使用DynamicImport-Package进行导入,那么尝试进行package的动态导入。exporter必须符合包约束。如果找到了合适的exporter,然后建立连接,以后的包导入就可以通过步骤三进行。如果连接建立失败,那么请求失败。
9.如果动态导入建立了,请求将委托给导出Bundle的classloader代理。如果代理查找失败,那么查找过程中止,请求失败。
如果delegation关系转移到另一个bundle classloader,被代理请求将进入到算法第4步。
3.9.5 Parent ClassLoader
隐含import package都是java.* package,因为这些package都是Java运行环境所必须的,对这些package不容易进行多版本控制。例如,所有类都继承Object。
Bundle中绝对不能import或者export java.* package,这样做是错误的,任何这样的Bundle一定是安装失败的。对于正在执行的Bundle,所有通过parent classloader处理的package必须被隐藏。
但是Framework必须明确export和parent classloader关联的package,这种package使用系统属性:
org.osgi.framework.system.packages
包含了系统bundle export package的描述。这个属性采用标准的Export- Package描述语法:
org.osgi.framework.system.packages ::= package-description ( ',' packagedescription)*
在boot classpath上的一些类假定他们可以使用任何classloader来加载位于boot classpath上的其他类,这对于bundle的classloader来说是错误的。Framework实现者应该试图从boot classpath加载这些类。
System bundle(bundle的ID为0)用于从parent classloader导出非java.*的包。System bundle导出定义看作是普通的导出,也就是说他们可以有版本号码,可以作为普通bundle的解析过程的一部分来用于解析导入定义。其他bundle也可以为同样的包提供一个替代的实现。
Parent classloader 中的导出定义序列也可以根据这个属性设置,或者由Framework计算。导出定义必须实现了特定bundle符号名称的实现和system bundle的版本的值。
这种风格的parent classloader中对外暴露的包必须考虑到其下的包的uses指令。例如,包javax.crypto.spec的定义必须声明包javax.crypto.interfaces和包javax.crypto的使用。
3.9.6 资源加载(Resource Loading)
bundle中的资源可以通过bundle的classloader进行访问,也可以通过方法getResource(String), getEntry(String), findEntries(String,String,boolean)进行访问。所有这些方法都返回一个URL对象或者是关于多个URL对象(Enumeration类型),这些方法返回的URL类可以是不一样的,由具体的实现来决定。
通常,bundle的入口URL是由Framework来创建的,然而,在特定情况下,bundle需要操作URL来查找相关资源。这样Framework需要保证:
- bundle入口的URL必须是分层次的(参考[13] Uniform Resource Identifiers URI: Generic Syntax)
- 作为构建其他URL的上下文环境
- java.net.URLStreamHandler类用于bundle入口URL地址,这个类必须是类java.net.URL可以访问的,根据Framework定义的协议来初始化一个URL
- bundle入口URL的getPath方法必须返回Resource或者bundle入口的绝对路径(以'/'开头)。例如,getEntry("myimages/test.gif")方法返回的路径必须包含有/myimages/test.gif。
例如,一个类可以通过一个URL访问bundle资源index.html,也可以是在同一个JAR文件目录下的对其他文件的URL映射表。
public class BundleResource implements HttpContext {
URL root; // to index.html in bundle
URL getResource( String resource ) {
return new URL( root, resource );
}
...
}
3.9.7 循环Bundle
多重需求的Bundle可能会export出一样的package。在package中class和resource的查找过程中,这样做可能会形成循环Bundle。考虑如下定义:
A: Require-Bundle: B, C
C: Require-Bundle: D
如下图描述:
Figure 3.15 Depth First search with Require Bundle
每一个bundle都导出包p,在这个例子中,bundle A需要bundle B,bundle C需要bundle D。当bundle A加载包p中的资源时,按照以下顺序进行查找:B,D,C,A。这是一个深度优先的搜索。如果在搜索路径中存在环,那么深度优先搜索会导致无限循环的搜索。
还是使用前面的例子,如果bundle D需要bundle A,就形成了一个环,如图3.16:
D: Require-Bundle: A
如果bundle A的类加载器从package p中加载类和资源,而且不考虑循环,那么bundle的查找顺序将是:B,B,B……
由于产生了这样的循环,每次查找到达bundle D时,将重新返回到A进行搜索。Framework这样会导致无限循环查找的循环依赖形成。
为了避免无限循环,Framework必须在第一次访问bundle的时候做一个标记,在以后的搜索中,对做了标记的bundle将不再访问。通过这样的访问模式,上述示例的查找路径如下:B,D,C,A。
3.9.8 启动前代码执行
在Bundle resolved之后,bundle中export的package就暴露给其他bundle了。这种状态也就是意味着其他bundle可以在这个export package的bundle启动之前调用这些方法。
3.9.9 查找Bundle Object
在有些情况下,Bundle所需代码没有获得Bundle Context,基于这些原因,framework提供了如下方法:
- Framework Util – 使用FrameworkUtil类的getBundle(Class)方法. framework提供这个方法允许通过Object(没有权限获得这个Object的classloader)查找Bundle,如果这个class不在这个Bundle内的,该method返回null。
- Class Loader –OSGi framework必须确保类的classloader实现自BundleReference接口.通过BundleReference可以获取Bundle对象,示例代码如下:
java ClassLoader cl = target.getClassLoader(); if ( cl instanceof BundleReference ) { BundleReference ref = (BundleReference) cl; Bundle b = ref.getBundle(); ... }
在OSGi系统中,不是所有object都属于framework,因此,也有可能获取一个没有实现BundleReference接口的classloader,如bootClassLoader.
3.10 Native代码加载
依赖的native代码描述在Bundle-NativeCode header中,framework在尝试resolve bundle之前必须验证这个header头并满足其中的依赖。然而,如果header头语法格式正确,可以无异常安装。如果header头包含无效信息或者依赖不能被满足,那么在resolving期间会报告error。
Java虚拟机有特殊的方式处理native代码,当Bundle的classloader尝试通过System.loadLibrary加载native代码时,Bundle classloader的findLibrary方法会被调用并返回file path(这个文件路径在Framework的可用请求的native库中)。findLibrary方法的参数是操作系统独立形式的library name,如http。Bundle classloader能够使用mapLibraryName方法(VM to 操作系统名的map),如libhttp.so
Bundle的classloader试图找到本地代码库时,必须通过测试选择的native代码clause操作,包括Bundle关联的类加载器和每一个附加的fragment,fragment的测试是根据bundle的ID升序进行测试。如果在选择的native代码clause中没有关联到搜索的库,那么将返回一个null值,同时由父类加载器继续进行搜索。
为了在OSGi Framework中加载native代码,Bundle必须有运行权限[loadLibrary.<库名称>],在manifest中的Bundle- NativeCode必须使用以下的语法形式进行描述:
Bundle-NativeCode ::= nativecode
( ',' nativecode )* ( ’,’ optional) ?
nativecode ::= path ( ';' path )* // See 1.3.2
( ';' parameter )+
optional ::= ’*’
当在Bundle内定位路径时,Framework必须尝试在Bundle的跟路径中定位,并在manifest头中包含了相应的native代码子句。
定义了如下属性:
- osname –操作系统名称。这个属性的值必须是本地代码运行的操作系统平台。4.5.3 环境属性一节有详细定义。
- osversion – 操作系统版本. 同3.2.6版本范围一节定义。
- processor –processor架构. 运行native代码的处理器架构名称.具体参考4.5.3 环境属性
- language –ISO代码定义的语言名称。这个属性名称必须是native代码库使用的语言。
- selection-filter –选择过滤器。定义一个filter表达式,描述native代码子句中应该选择或者不应该选择的部分。
如下是一个典型的Bundle native code示例:
Bundle-NativeCode: lib/http.dll ; lib/zlib.dll ; «
osname = Windows95 ; «
osname = Windows98 ; «
osname = WindowsNT ; «
processor = x86 ; «
selection-filter= "(com.acme.windowing=win32)"; «
language = en ; «
language = se , «
lib/solaris/libhttp.so ; «
osname = Solaris ; «
osname = SunOS ; «
processor = sparc, «
lib/linux/libhttp.so ; «
osname = Linux ; «
processor = mips; «
selection-filter = "(com.acme.windowing=gtk)"
如果多个本地代码库需要安装在同一个平台,那么他们必须在同一个clause中进行声明。
如果Bundle-NativeCode clause包含重复的参数实体,使用”OR”来描述值。此功能必须小心使用,示例:
// The effect of this header has probably
// not the intended effect!
Bundle-NativeCode: lib/http.DLL ; «
osname = Windows95 ; «
osversion = "3.1" ; «
osname = WindowsXP ; «
osversion = "5.1" ; «
processor = x86
上面的例子描述了本地代码库可以在Windows XP 3.1以及之后的操作系统上加载,这种描述是不正确的,单个子句应该拆分成如下两个:
Bundle-NativeCode: lib/http.DLL ; «
osname = Windows95 ; «
osversion = 3.1; «
processor = x86, «
lib/http.DLL ; «
osname = WindowsXP ; «
osversion = 5.1; «
processor = x86
如果Bundle不能在resolve时解决native code的问题,那么Framework会抛出NATIVECODE_ERROR异常。
如果在描述子句中有一个可选的’*’号,那么即使在Bundle- NativeCode中没有找到匹配的子句,bundle的安装也不会报错。
如下就是一个典型的有星号的在bundle的manifest中的本地代码声明:
Bundle-NativeCode: lib/win32/winxp/optimized.dll ; «
lib/win32/native.dll ; «
osname = WindowsXP ; «
processor = x86 , «
lib/win32/native.dll ; «
osname = Windows95 ; «
osname = Windows98 ; «
osname = WindowsNT ; «
osname = Windows2000; «
processor = x86 , «
*
3.10.1 native代码算法
在算法描述中,[X]表示Framework中属性X的值,~=表示匹配操作,匹配操作是大小写不敏感的。
某些属性可以使用别名,在这种情况下,在manifest中使用一般的名称,而在Framework中应该尝试使用别名来进行匹配(参考4.5.3环境属性一节)。如果一个属性不是别名,或者有一个错误的值,操作者(Operator)应该将系统属性设置为一般名称或者一个有效的值,这是由于java的系统属性会覆盖Framework构造器的这些属性。例如,如果操作系统返回的版本号码是:2.4.2- kwt,那么操作者应该设置系统属性org.osgi.framework.os.version的值为2.4.2。
Framework必须采用以下算法来选择本地代码子句:
1.只能选择以下所有表达式的值为真的子句:
- osname ~= [org.osgi.framework.os.name]
- processor ~= [org.osgi.framework.processor]
- osversion 包括了 [org.osgi.framework.os.version] 或者没有指定osversion
- language ~= [org.osgi.framework.language] 或者没有指定
- selection- filter使用系统属性的值或者没有指定 2.如果在第一步中没有选择native代码子句时,算法不会中止,而是抛出一个Bundle异常(BundleException)。 3.选择的代码子句按照以下规则进行优先级别的排序: osversion:按照操作系统版本号的降序排列,然后是没有指定的osversion language:按照指定了的语言排前面,没有指定的排后面 在Bundle- NativeCode中出现的顺序,从左到右的顺序 4.步骤3中选出的第一个子句用于作为本地代码子句。
无论是否指定了可选,如果在本地代码库中选择本地代码子句失败,那么bundle的安装失败,抛出bundle异常。
如果对一个选择过滤器计算,它的语法无效,那么bundle的安装过程失败,同时抛出一个bundle异常。如果没有计算出一个选择过滤器的值(有可能在一个操作系统版本或者处理器这些属性不匹配的本地代码子句中),那么这种无效的过滤器不应该导致安装的失败。在指定了可选的情况下,上述规定同样有效。
由于使用了不同的操作系统,代码库和语言,导致了设计bundle本地代码的头标变得非常复杂。可以将所有的参数用一个表格描述,而每一个目标环境都是表格的一行,这也是一种很好的方法。如下表3.2所示:
Bundle-NativeCode头示例:
Bundle-NativeCode:nativecodewin32.dll; v
delta.dll; «
osname=win32; «
processor=x86; «
language=en, «
nativecodegtk.so; «
osname=linux; «
processor=x86; «
language=en; «
selection-filter= "(com.acme.windowing=gtk)", «
nativecodeqt.so; «
osname=linux; «
processor=x86; «
language=en; «
selection-filter = "(com.acme.windowing=qt)"
3.10.2 考虑使用native库
基于类加载器的本质特点,在加载native代码时存在一些约束。为了保持类名称空间的独立性,只允许一个类加载来进行本地代码的加载,native代码通过一个绝对的路径指定。多个类加载来加载native代码(例如多个Bundle)会导致连接错误。
直到加载native代码的类加载器被垃圾回收器回收,加载的本地代码才被释放。
如果卸载或者是更新一个Bundle,任何由Bundle加载的native代码都保留在内存中,直到bundle的类加载器由垃圾回收器回收才释放。而这需要所有的对象引用都已经被回收,而且所有从更新或者卸载的bundle中导入了包的bundle都已经更新完毕。这也就是说,系统类加载器加载的native代码会一直停留在内存中,这是由于系统类加载器是绝对不会被垃圾回收器回收的。