9.9.2 Equinox框架的类加载实现机制
目前存在不少OSGi规范的实现,这里使用Eclipse Equinox作为介绍时的OSGi实现,运行环境在Eclipse IDE中。每个模块在运行时都会有一个对应的类加载器对象。由这个类加载器对象负责加载模块本身包含的Java类和资源。Eclipse Equinox在启动模块时使用org.eclipse.osgi.internal.loader.BundleLoader类的对象来加载一个模块。每个模块都有自己对应的BundleLoader类的对象。BundleLoader类的对象中都封装了一个类加载器对象来加载模块中的Java类。这个类加载器对象需要实现org.eclipse.osgi.framework.adaptor.BundleClassLoader接口,并且继承自ClassLoader类。默认的类加载器实现类是org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader。DefaultClassLoader类的对象在创建时需要提供一个org.eclipse.osgi.framework.adaptor.ClassLoaderDelegate接口的实现对象。ClassLoaderDelegate接口表示的是实际完成类加载工作的代理对象,其中声明了用来加载Java类的findClass方法。DefaultClassLoader类在其loadClass方法的实现中,只是简单地把加载请求代理给其中包含的ClassLoaderDelegate接口的实现对象的findClass方法。DefaultClassLoader类的对象本身并不会尝试去加载类,也不像其他类加载器一样会把加载请求代理给双亲类加载器对象。这是另外一种形式的代理模式。BundleLoader类本身实现了ClassLoaderDelegate接口,所以BundleLoader类既负责创建加载Java类时使用的BundleClassLoader接口的实现,又负责完成具体的类加载任务。当模块中的代码需要加载Java类时,可以通过代码“this.getClass().getClassLoader()”来得到加载当前类的类加载器对象。
BundleLoader类中负责加载Java类的findClass方法中封装了比较复杂的Java类的查找机制。对于以“java.”开头的Java类,会直接代理给双亲类加载器对象来完成。对于其他的Java类,则在模块内部和所导入的包中进行查找。基本的类查找步骤如下:
1)检查要加载的Java类是否出现在被配置为由双亲类加载器对象负责加载的包名列表中。如果是,则直接代理给双亲类加载器对象来完成。通过OSGi框架的属性“org.osgi.framework.bootdelegation”可以配置这个列表中包含的包名。这个步骤的作用是允许某些Java类被双亲类加载器对象来加载。
2)搜索该模块中通过Import-Package属性声明导入的Java包的列表。如果找到,由提供该Java包的模块对应的BundleLoader类的对象负责该Java类的加载。
3)搜索该模块中通过Require-Bundle属性声明所依赖的其他模块的列表。如果找到可以提供该Java类的模块,则由该模块对应的BundleLoader类的对象来负责加载该Java类。
4)在模块内部包含的Java类中进行查找。
5)搜索模块中通过DynamicImport-Package属性声明动态导入的Java包列表。如果找到,则由提供该Java包的模块对应的BundleLoader类的对象负责加载该Java类。
6)如果仍然找不到Java类,在某些情况下代理给双亲类加载器对象进行加载。
从上面的类加载流程可以看出,BundleLoader类在尝试加载Java类时主要依赖两个外部来源来代理类加载的请求,一个是BundleClassLoader接口实现对象的双亲类加载器,另外一个是其他模块对应的BundleLoader类的对象。BundleClassLoader接口的实现类可以选择不同的双亲类加载器。可以通过OSGi框架的属性配置使用不同的双亲类加载器选择方式。默认的双亲类加载器是Java平台的启动类加载器。还可以选择使用加载OSGi框架本身的类加载器对象、系统类加载器或扩展类加载器作为BundleClassLoader接口的实现对象的双亲类加载器。模块calculator.impl导入了calculator.common导出的com.java7book.calculator.common包。当在calculator.impl模块中使用com.java7book.calculator.common包中的Java类时,实际的类加载工作是由calculator.common模块对应的BundleLoader类的对象来完成的。通过这种代理模式,在一个模块中只有通过Export-Package属性声明的Java包才对其他模块可见。如果一个模块A中的某个Java包没有包含在Export-Package属性声明的列表中,而另外一个模块B又试图引用该Java包中的类,模块B会找不到对应的类。因为该Java类没有出现在模块A的Export-Package属性声明的Java包列表中,不会考虑代理给模块A来进行查找,从而无法找到该Java类。
在OSGi运行环境中,如果在模块中使用SPI的实现类,可能会出现问题,因为SPI的代码中通常使用线程上下文类加载器来加载SPI接口的实现类。在一般的运行环境中,SPI的实现类是可以从程序运行时的CLASSPATH中找到的。但是在OSGi运行环境中,SPI的实现类一般作为模块所依赖的库出现在模块本身的CLASSPATH中。如果在程序的其他部分对当前线程的上下文类加载器进行了修改,有可能造成SPI的实现类无法被加载。这个时候可以使用当前线程的setContextClassLoader方法把线程上下文类加载器设置成加载模块Java类的类加载器对象,从而保证可以加载模块的CLASSPATH中的SPI实现的Java类。代码清单9-17给出了通过修改线程上下文类加载器来加载SPI实现类的一般做法。在完成加载之后,要把线程上下文类加载器的值恢复为之前的值,以免对程序的其他部分造成影响。
代码清单9-17 通过修改线程上下文类加载器来加载SPI实现类的一般做法
ClassLoader oldContextLoader=Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
//加载SPI实现类
Thread.currentThread().setContextClassLoader(oldContextLoader);
在OSGi中使用这种类加载器的实现方式,可以对一个模块中的Java类按照所在的包设置其可见性,从而带来了访问控制上的灵活性。不过这种方式也可能在实际开发中带来一些麻烦,尤其在使用第三方库或嵌入到其他容器中时。某些第三方库或容器可能同样使用了复杂的类加载器实现,会与OSGi框架本身的类加载器实现交织在一起,造成难以解决的问题。在OSGi模块中使用第三方库时,可以考虑下面的建议:
1)如果一个库只被一个模块使用,把该库的jar包放在模块中,并在模块清单文件中使用Bundle-ClassPath属性进行声明。
2)如果一个库被多个模块共用,则可以为这个库创建一个新的模块。对于库中的Java包,如果其他模块会用到,那么在清单文件中通过Export-Package属性进行声明。其他模块只需要通过Import-Package属性声明所需要的包即可。
3)如果出现了找不到Java类的情况,则检查当前线程的上下文类加载器是否正确。一般可以通过把当前线程的上下文类加载器设置为模块的类加载器对象来解决这个问题。