2.5 OSGi的类加载架构
OSGi为Java平台提供了动态模块化的特性,但是它并没有对Java的底层实现如类库和Java虚拟机等进行修改,OSGi实现的模块间引用与隔离、模块的动态启用与停用的关键在于它扩展的类加载架构。
OSGi的类加载架构并未遵循Java所推荐的双亲委派模型(Parents Delegation Model),它的类加载器通过严谨定义的规则从Bundle的一个子集中加载类。除了Fragment Bundle外,每一个被正确解析的Bundle都有一个独立的类加载器支持,这些类加载器之间互相协作形成了一个类加载的代理网络架构,因此OSGi中采用的是网状的类加载架构,而不是Java传统的树状类加载架构,如图2-14所示。
在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: BundleA
此时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所示。
这里假设Bundle A导出的p中存在ClassA这个类,这样Bundle B的ClassA就无法派上用场。如果情况更极端一些,Bundle A导出的p不存在ClassA这个类,那Bundle B的ClassA依然不会被使用,而会直接收到ClassNotFoundException异常,异常信息类似如下所示:
Caused by: java.lang.ClassNotFoundException: p.ClassA
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所示。