深入理解OSGI:第二章 模块层规范与原理(2)

 由  IcyFenix 发布

2.5 OSGi的类加载架构

OSGi为Java平台提供了动态模块化的特性,但是它并没有对Java的底层实现如类库和Java虚拟机等进行修改,OSGi实现的模块间引用与隔离、模块的动态启用与停用的关键在于它扩展的类加载架构。

OSGi的类加载架构并未遵循Java所推荐的双亲委派模型(Parents Delegation Model),它的类加载器通过严谨定义的规则从Bundle的一个子集中加载类。除了Fragment Bundle外,每一个被正确解析的Bundle都有一个独立的类加载器支持,这些类加载器之间互相协作形成了一个类加载的代理网络架构,因此OSGi中采用的是网状的类加载架构,而不是Java传统的树状类加载架构,如图2-14所示。

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

图2-14 OSGi的网状类加载架构

在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)。

下面将介绍Bundle中各种类的加载过程,涉及类加载器,以及类加载的优先级次序。

2.5.1 父类加载器

OSGi框架必须将以java.开头的Package交给父类加载器代理,这一点是无须设置且不可改动的。除此之外,OSGi框架也允许用户通过系统参数“org.osgi.framework.bootdelegation”自行指定一些Package委派给父类加载器加载,这个参数被称为“父类委派清单”(Boot Delegation List)。它的值应为一系列的包名,用逗号分隔,支持通配符,例如: org.osgi.framework.bootdelegation=sun.,com.sun.*

如果org.osgi.framework.bootdelegation的参数值如以上代码中所示,那么以sun.和com.sun.开头的类也会委派给父类加载器去加载。这个设定在特定场景下很有用。

例如某个部署在Web中间件上的OSGi应用需要使用JDBC访问数据库,与大多数应用一样,访问数据库的Connection是由应用服务器的JNDI提供的,这时候就应当把JDBC驱动设置为由父类加载器加载,而不是由OSGi中的某个Bundle包提供。因为Web中间件通常会带有连接池实现,为了实现事务控制和连接监视等功能,从JNDI中查到的DataSource是被中间件服务器包装过的,并非直接由原生的JDBC驱动所提供。为了保证中间件服务器中一些需要把Connection、Statement、ResultSet等从接口转型为具体实现类的代码(大多数是操作大字段的代码)能正常执行,必须保证中间件服务器和OSGi应用所使用的JDBC驱动是同一个—不仅是同一个文件,还要是由同一个类加载器加载的,这样才能保证转型成功。

以java.开头的Package是默认被隐式导出的,在所有Bundle中无需导入便可以直接使用,并且OSGi规范明确禁止在Bundle中导入或导出以java.开头的Package。与前面提到的父类委派清单类似,OSGi也定义了添加隐式导出Package的参数“org.osgi.framework.system.packages”。这个参数使用标准的Export-Package语法描述,例如: org.osgi.framework.system.packages=javax.crypto.interfaces

这里定义的Package将由系统Bundle(ID为0的Bundle)导出,由父类加载器加载。这样导出的Package与普通的导出方式没有太大区别,可以带有属性和版本号,也可以使用uses参数描述依赖。

2.5.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中有如下路径:

Bundle:
    lib/log4j.jar
    org/fenixsoft/osgi/Example.class

log4j.jar
    org/apache/log4j/Logger.class

Bundle类加载器可以访问到Example.class,但是无法访问到Logger.class,最多只能把log4j.jar当作与图片、音频等类似的二进制资源整体提供出去。 要读取到Logger.class,必须设置Bundle-Classpath标记为:

Bundle-Classpath: lib/log4j.jar,.

注意不要遗漏了后面的“,.”,这里有两个Classpath路径,它们之间使用逗号分隔,如果没有了后面的“.”,那么Bundle类加载器就只能处理log4j.jar中的类而无法处理本Bundle的Example.class了。

如果Bundle-Classpath标记的值是多个Classpath路径,那么它们之间还有优先级关系,例如下面这个定义:

Bundle-Classpath: required.jar,optional.jar,default.jar

该定义中required.jar是必须出现在Bundle中的类和资源;optional.jar是某个可选的JAR包,其中存放着可选的类和资源;default.jar中存放着optional.jar不可用时这些类和资源的默认值,如果optional.jar中有可用的内容便会对其覆盖。

如果一个Bundle被另一个Fragment Bundle附加,那么Bundle-Classpath也会相应叠加,例如下面定义:

Bundle A:
Bundle-Classpath: required.jar,optional.jar,default.jar

Bundle B:
Bundle-Classpath: fragment.jar
Fragment-Host: Bundle

此时Bundle A的Bundle类加载器能搜索到的Classpath依次为:required.jar、optional.jar、default.jar、fragment.jar。

Bundle类加载器收到类加载请求时,会优先委托给导入包的其他Bundle类加载器处理,只有其他导入包的Bundle类加载器都无法处理时才会尝试自己处理。读者可以通俗地理解为“Import-Package”和“Require-Bundle”的优先级高于“Bundle-Classpath”,如果能在前者中找到所需的类,后者就不会起作用。这条规则读起来不复杂,但初接触OSGi的朋友在实际编码时候可能会对此有些不习惯,例如下面这个例子:

在Bundle A、B中都有Package p,两者的Package p中都存在有类ClassA。同时,Bundle B还导入了Bundle A中的Package p。在这个前提下,假设Bundle A中有下列代码: ClassA anA = new ClassA();

这时候ClassA用的都是Bundle A中的类,符合一般思维习惯。但是如果Bundle B中有同样的代码,所使用的ClassA依然是Bundle A中的类,即使Bundle B自己的Classpath中也有这类ClassA,甚至与调用ClassA的代码文件存在于同一个目录下紧紧相邻的就是ClassA,都不会被使用,这就不符合一般的思维习惯了,如图2-15所示。

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

图2-15 加载Bundle类加载器的示例

这里假设Bundle A导出的p中存在ClassA这个类,这样Bundle B的ClassA就无法派上用场。如果情况更极端一些,Bundle A导出的p不存在ClassA这个类,那Bundle B的ClassA依然不会被使用,而会直接收到ClassNotFoundException异常,异常信息类似如下所示:

Caused by: java.lang.ClassNotFoundException: p.Class    at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:467)
    at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:429)
    at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:417)
    at org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader.loadClass(DefaultClass-

Loader.java:107) at java.lang.ClassLoader.loadClass(ClassLoader.java:248)

对于在Bundle中发生的加载请求而言,当前Bundle的Bundle类加载器是使用到的类的初始类加载器(Initiating Classloader,它表示加载请求最先发送到的类加载器),而哪个类加载器是定义类加载器(Defining Classloader,它表示加载请求被不断委派后,最终执行加载动作的类加载器)则要根据OSGi类加载顺序来判定。在类型强制转换和类型比较(譬如instanceOf操作)时理解类加载顺序很重要,因为即使是同一个类文件,由不同定义类加载器加载所形成的类在Java虚拟机中也是完全独立且不可互相转型的。

2.5.3 其他类加载器

在OSGi中还可能使用到其他的类加载器,比如OSGi实现框架中一般都会有框架类加载器(Framework Classloader)。OSGi框架为每个Bundle创建Bundle类加载器的实例,而OSGi框架自身的代码——至少涉及OSGi框架启动的代码就没法使用Bundle类加载器来加载,因此需要一个专门的框架类加载器来完成这个任务。这个框架类加载器是各个OSGi实现框架自己定义的,有时候可能直接使用Java平台提供的应用程序类加载器(Application ClassLoader)。这个框架类加载器还可能同时充当父类加载器的角色,比如在Equinox框架中就可以选择是使用启动类加载器、扩展类加载器、应用程序类加载器还是使用框架类加载器来作为父类加载器。

另外一个在OSGi中比较常见的类加载器是线程上下文类加载器(Thread ContextClassLoaser),这个类加载器并不是在OSGi中才出现的,它在普通的Java应用中有广泛应用。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时未设置,那么它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器就默认是应用程序类加载器。有了线程上下文类加载器,就可以做一些“舞弊”的事情,例如直接加载没有经过导入和导出的类,或者让由框架类加载器加载的OSGi框架代码在运行期得以访问一些系统Bundle中的类。

OSGi中其他的类加载器与具体实现密切相关,后面我们将会在确定具体OSGi实现框架和具体上下文的场景下再进行介绍,此处不再赘述。

2.5.4 类加载顺序

当一个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-16所示。

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

图2-16 加载OSGi类的全过程

2.6 定义执行环境

某些Bundle必须在特定的执行环境之下才能正常运作,例如为大型服务端应用而设计的Bundle一般不能运行在嵌入式设备之中,而利用JDK1.6开发的Bundle无法运行在JDK1.5的应用服务器之中等。为了确保Bundle可用性,元数据信息中提供了Bundle-RequiredExecutionEnvironment标记来描述Bundle对执行环境的要求,示例如下: Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0

OSGi对执行环境定义的命名是直接继承于Java平台的执行环境名称,如图2-17所示为Eclipse配置界面中执行环境与JRE关联的设置对话框。

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

图2-17 Eclipse执行环境设置

对于每一个执行环境,在OSGi框架中应该对应一套系统运行参数的默认配置。前面提到过的org.osgi.framework.bootdelegation、org.osgi.framework.system.packages等参数的默认值都由执行环境决定。以Equinox框架为例,对于JavaSE-1.6这个执行环境,在Equinox框架中对应的配置存储在JavaSE-1.6.profile文件中,内容为:

org.osgi.framework.system.packages = \
javax.accessibility,\
 javax.activation,\
 javax.activity,\
 javax.annotation,\
……//版面关系,省略其余的Package
org.osgi.framework.bootdelegation = \
javax.*,\
org.ietf.jgss,\
org.omg.*,\
org.w3c.*,\
org.xml.*,\
sun.*,\
com.sun.*
org.osgi.framework.executionenvironment = \
OSGi/Minimum-1.0,\
OSGi/Minimum-1.1,\
OSGi/Minimum-1.2,\
JRE-1.1,\
J2SE-1.2,\
J2SE-1.3,\
J2SE-1.4,\
J2SE-1.5,\
JavaSE-1.6
org.osgi.framework.system.capabilities = \
osgi.ee; osgi.ee="OSGi/Minimum"; version:List<Version>="1.0, 1.1, 1.2",\
osgi.ee; osgi.ee="JavaSE"; version:List<Version>="1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6"
osgi.java.profile.name = JavaSE-1.6
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error

在OSGi R4.3规范发布后,元数据对执行环境的描述能力被进一步增量,引入了通用Capability的概念,通过Require-Capability和Provide-Capability两个新的标记也可以定义执行环境,例如: #R4.3之前的方式:

Bundle-RequiredExecutionEnvironment: JavaSE-1.6

#R4.3新提供的方式:

Require-Capability: osgi.ee;filter:="(&(osgi.ee="JavaSE")(version>=1.6))"

定义了Require-Capability之后,OSGi框架在解析Bundle之前必须满足必要的Capability需求。典型使用场景是提供OSGi的声明式服务,该服务并不会表示为Package依赖,但为了能够正确解析Bundle,它又是必需的。

2.7 本地化

前面提到过,Bundle的元数据信息中包含一些供人工阅读的信息,如Bundle-Name、Bundle-Vendor等。这些信息可能需要根据用户的语言、国家和其他指定的参数(一般是指定“区域”的参数)翻译成不同的语言。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
OSGI-INF/l10n/bundle_nl_BE.properties
OSGI-INF/l10n/bundle_nl_NL.properties
OSGI-INF/l10n/bundle_sv.properties

实现框架不是通过精确匹配文件名来搜索本地化信息,而是采用一种渐进式的搜索方式。如果最佳匹配的文件没有找到,会先删除参数,然后是国家,最后是语言,直到找到一个包含有效信息的本地化文件。比如,参数为welsh、国家是GB、语言是en的本地化文件将通过以下顺序查找:

OSGI-INF/l10n/bundle_en_GB_welsh.properties
OSGI-INF/l10n/bundle_en_GB.properties
OSGI-INF/l10n/bundle_en.properties
OSGI-INF/l10n/bundle.properties

另外,查找过程也并不是找到某个可用的本地化文件就停止,而是会一直进行下去,这种策略允许在拥有更具体区域、语言信息的本地化文件时覆盖更少信息的本地化文件。

本地化文件中包含了以Key-Value值对表示的本地化信息。Bundle的元数据信息文件中所有信息都可以进行本地化。但是,对于带有程序语义而非人工阅读的信息,OSGi实现框架必须使用非本地化版本,也就是只以MANIFEST.MF文件中的内容为准。

我们可以通过两种方式来使用本地化文件中的信息,第一种是直接覆盖元数据标记,例如:

Bundle-Name : The ACME Bundle
Bundle-Vendor : The ACME Corporation
Bundle-Description : The ACME Bundle provides all of the ACME

第二种是在MANIFEST.MF中使用本地化变量,然后在本地化文件中定义这些变量的值,例如: #在MANIFEST.MF文件中:

Bundle-Name : %acme_bundle
Bundle-Vendor : %acme_corporation
Bundle-Description : %acme_description
Acme-Defined-Header : %acme_special_header

#在OSGI-INF/l10n/bundle.properties文件中:

acme_bundle=The ACME Bundle
acme_corporation=The ACME Corporation
acme_description=The ACME Bundle provides all of the ACME services
acme_special_header=user-defined Acme Data

本地化文件中定义的变量中间允许空格存在,把上面例子中的“_”替换成空格也是允许的。另外,在MANIFEST.MF文件中由用户自定义的非OSGi的标记也可以被本地化,例如上面的“Acme-Defined-Header”。

2.8 本章小结

OSGi的模块层定义了一个模块化的Java模型,针对Java部署模式的一些缺点进行了改进,对哪些Package可以在模块之间交互、如何交互、版本管理等都有严格规定。 在OSGi中,模块层独立于生命周期层和服务层,这意味着它在使用时可以不需要生命周期层和服务层的支持,但是,这样的模块是“静态的”。生命周期层提供了对模块层的Bundle进行管理的各种API,而服务层提供了Bundle之间的通信模型。在后面我们将继续探索模块层与生命周期层中的知识。

查看评论