深入剖析 Eclipse 类装入器
2007 年 5 月 28 日
Eclipse 提供了一个强大的开发平台,越来越多的应用基于 Eclipse 来开发。但是由于 Eclipse 作为一个灵活的平台,其类装入器具有一定的特殊性,在开发 Eclipse 插件时我们经常遇到类找不到的问题,尤其是当我们开发的应用使用了第三方的软件包时。本文深入剖析了 Java 的类装入器机制以及 Eclipse 的类装入器的原理与模型,并总结了Eclipse 插件应用开发常见的与类装载器相关的问题,同时给出了相应的解决方法。
Eclipse 提供了一个强大的开发平台,越来越多的应用基于 Eclipse 来开发。但是由于 Eclipse 作为一个灵活的平台,其类装入器具有一定的特殊性,在开发 Eclipse 插件时我们经常遇到类找不到的问题,尤其是当我们开发的应用使用了第三方的软件包时。本文深入剖析了 Java 的类装入器机制以及 Eclipse 的类装入器的原理与模型,并总结了 Eclipse 插件应用开发常见的与类装载器相关的问题,同时给出了相应的解决方法。
注:如不作特殊说明,本文默认的开发和运行环境是:
- IBM JDK 1.4
- Eclipse 3.0.x
- IBM WebSphere Application Server 6.0
|
类装入器是 JVM 用来装入类的类,它对于 Java 编程是非常重要的一个概念。一般情况下,程序员在编写程序的时候都可以忽略类装入器的存在性。但是对于服务器端编程或者是一些特殊情况下时候,深入了解类装入器的机制以及其在不同情况下的实现还是非常必要的。
首先,当一个 JVM 启动的时候,Java 缺省开始使用三个类装入器。它们分别是:
- 引导(Bootstrap)类装入器;
- 扩展(Extension)类装入器;
- 系统(System)类装入器;
它们分别实现如下的功能:
- 引导类装入器是用本地代码实现的类装入器。它负责将 /lib 下面的类库加载到内存中。
- 扩展类装入器是由 Sun 的 ExtClassLoader 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。
- 系统类装入器又叫应用程序类装入器,是由 Sun 的 AppClassLoader 实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。
当 应用程序需要加载某个类到内存中的时候,类装入器是如何工作的呢?这就设计到类装入器的一个重要方面:代理机制。每一个类装入器,除了引导类装入器以外, 都有一个父类装入器。对于系统缺省定义的三个类装入器,引导类装入器是扩展类装入器的父类装入器,而扩展类装入器是系统类装入器的父类装入器。当然,应用 程序也可以使用自己的类装入器来使用特定的方法来装载类,因此,整个系统中的类装入器就形成一个树状结构。
当使用某个类装入器来试图装载某个类的时候,该类装入器会首先使用其父类装入器来试图装载该类。对于每一个装载进来的类,JVM 都会给其分配一个唯一的 ID。因此,不同类装入器可以装载同一个类到 JVM 中。例如,对于如下图结构的 ClassLoaderA 和 ClassLoaderB:
图 1 类装入器的结构
假设类 C 在系统类装入器指定的类路径中,则无论是使用 ClassLoaderA 还是使用 ClassLoaderB,都只会得到同样一个类 C。
但是如果类 C 分别在 ClassLoaderA 以及 ClassLoaderB 指定的类库中,则使用 ClassLoaderA 得到到类 C 实例会不同于 ClassLoaderB 得到的类 C 实例。尽管两个类装入器在同一个 JVM 中。
上面的类装入器的向上代理结构看上去很完美了,但是,当系统变得复杂的时候,就还是显得不够用了。
例 如,当 Java 引入了 JNDI 以后,JNDI 核心部分是通过引导 类装入器在 JVM 启动的时候装载进入 JVM 的。而 JDNI 核心部分是通过配置信息来在运行时候装载定义在用户的类路径中的特定类来完成特定需要。而这是上面定义的类装入器的向上代理模式所不能支持的。
为了解决这个问题,Java 2 中引入了线程上下文(Thread Content)类装入器的概念,每一个线程有一个 Context 类装入器。这个 Context 类装入器是通过方法 Thread.setContextClassLoader() 设置的,如果当前线程在创建后没有调用这个方法设置 Context 类装入器,则当前线程从他的父线程继承 Context 类装入器。如果整个应用都没有设置 Context 类装入器,则系统类装入器被设置为所有线程的 Context 类装入器。
对 于我们上面所说 JNDI 的情况,引导 类装入器装载进入的 JNDI 核心类会使用 Context 类装入器来装载其所需要的 JNDI 实现类,而不是将该装载任务代理给其父类装入器来完成。这样,就解决了上面的问题。可以认为 Context 类装入器在传统的 Java 向上代理机制上打开了一个后门。Context 类装入器在 J2EE 中使用的很广泛,比如 Java 命名服务(JNDI),Java API for XML Parsing(JAXP)(注:在 Java1.4 中 JAXP 才作为 Java 的核心类的一部分,它才开始使用 Context 类装入器来加载不同的实现类)等。
简单而言,Java 中的类装入器就是上面几种,但是,在具体使用中,还是有很多变化,我们下面分别对于一些情况进行说明。
|
在典型的 Java 应用程序中,我们要加载一个类或资源时,通常可以使用三种类装入器:
- 当前类的类装入器;
- 当前线程的 Context 类装入器;
- 系统类装入器;
在实际的应用开发中比较常用到前两个类装入器是,下边重点介绍在 Eclipse 插件运行环境中前两种类装入器的结构和原理以及和典型的 Java 应用的不同之处。
下图是 Eclipse 插件在运行时当前插件类的类装入器的体系结构图。
图 2 Eclipse 插件类装入器的结构
其 中 EclipseClassloader 类装入器实现了 OSGi 规范的 BundleClassLoader,它用来装载 Bundle (也就是插件)中的类 OSGi ParentClassloader 类装入器是 Eclipse 插件类装入器的父类装入器,可以通过在启动 Eclipse 时设置系统属性 osgi.parentClassloader 来改变它,这个改变类会影响所有的 Eclipse 插件。如果在启动时没有设置系统属性 osgi.parentClassloader, Eclipse 使用一个默认的空的类装入器。
BundleLoader 是 EclipseClassloader 类装入器的代理,它是用来加载插件相关的资源的。
Eclipse 插件类装入器加载类或资源的过程如下:
- 首先试图从父类装入器加载类,其过程是先从 OSGi ParentClassloader 类装入器加载类,OSGi ParentClassloader 类装入器使用传统的 Java 装入器的委托模式依次从父类装入器加载类。
- 如果无法从 OSGi ParentClassloader 类装入器加载类,则试图通过代理 BundleLoader 来加载类。
- BundleLoader 首先试图从此插件的需求依赖插件("require"指定的插件)中去加载需要的类,如果找不到,则通过 EclipseClassloader 类装入器来从插件本地加载需要的类。
- 如果还是找不到要加载的类,就会抛出类找不到异常。
在 Eclipse 中并没有设置 Context 类装入器,所以默认情况下当前线程的 Context 类装入器为系统的类装入器,其体系结构如 图 3 所示。
图 3 Eclipse 插件当前线程 Context 类装入器的结构
在 Eclipse 中,每个插件都有自己的类装入器,每个线程有自己的类装入器。插件和线程之间没有统一的映射关系,所以 Eclipse 框架没有将线程的 Context 类装入器设置成有意义类装入器。
|
Eclipse 已经越来越多的被用来作为一个平台使用,基于 Eclipse 的插件应用开发也越来越多,尤其是使用 Eclipse 插件作为富客户端访问服务器的应用。其中有两种比较典型的应用:
- 在 Eclipse 插件中调用 EJB
- 在 Eclipse 插件中调用 Web 服务
在以上这些典型的 Eclipse 插件的开发过程中,通常会遇到两中类找不到的问题:
- 在 JNDI 中查找命名服务时抛出类找不到异常
- 在使用 HTTPS/SSL 调用 Web 服务时抛出类找不到异常
|
在 Eclipse 中使用 "Hello,World" 模版开发一个简单插件应用,然后再 Action 的实现中添加下面的方法,并在 run 方法中调用它。
清单 1 查找 JNDI 服务的方法
public Object ejbLookup(String ejbJNDIName) throws NamingException{ |
以上方法用来在 JNDI 中根据传入的 JNDI 名字查找 EJB 对象,在找到 EJB 对象后打印这个对象。
在 运行以上简单的 Eclipse 插件应用前,需要确保所有作为独立的 Java 应用调用 EJB 时需要的 WebSphere 库文件到插件根目录下,同时修改插件的 plugin.xml 把这些库文件加入到 "Run-time libraries" 列表中.
同时还要确保查找的 EJB 已经部署到 WebSphere 上并且已经启动。
注:在本例中笔者使用了 WebSphere 6.0 自带的 Plants by WebSphere 样例应用程序中的 EJB 对象。这个样例应用程序可以通过以下命令来安装:
WAS_install_rootsamplesininstall -samples PlantsByWebSphere
运行我们刚刚开发的 Eclipse 插件应用,点击 "Hello,Eclipse world" 按钮来调用以上方法,我们会在控制台上看到以下异常信息:
清单 2 查找 JNDI 对象的方法运行结果
|
从 以上的异常可以看到 Eclipse 没有找到类 com.ibm.websphere.naming.WsnInitialContextFactory,这个类位于 WebSphere 库文件 naming.jar 中,事实上这个库文件已经作为运行时库加入到了插件的 plguin.xml 文件的 runtime 节了,但是 Eclipse 插件在运行时却找不到位于这个库文件中的类。下面分析其原因。
以 上异常是在初始化 JNDI 的初始上下文(Initial Context)时出现的,我们知道 Java 类的加载使用委托的机制,它总是会先加载父类装入器中的类。核心的 JNDI 类是在引导(Bootstrap)类装入器装入的,而具体的 JNDI 类在厂商提供的库文件中(这里是 IBM 提供的 naming.jar 文件)。当 JNDI 初始化的时候,位于引导类装入器中的核心 JNDI 类会使用当前线程的 Context 类装入器来装入 JNDI 具体的实现类,因为这些 JNDI 具体的实现类位于 naming.jar 库文件中,而这个库文件位于插件的运行时库文件列表中,从 图 3 Eclipse 插件类当前线程 Context 类装入器的结构中我们不难看出,在类装入器的树状结构中并没有包含加载 Eclipse 插件本地库文件的类转入器,所以 Eclipse 插件是找不到库文件 naming.jar 的。
我 们可以通过在插件程序中动态的设置线程的 Context 类装入器来解决以上问题。在调用 JNDI 查找方法之前,我们可以把当前线程的 Context 类装入器设置为当前类的类装入器,这样在当前线程的类装入器的体系结构中就包含了加载 Eclipse 插件本地库文件的类装入器了。
修改调用方法 ejbLookup 的代码如下:
清单 3 在调用 ejbLookup 前设置 Context 类装入器
ClassLoader currentClassLoader = this.getClass().getClassLoader(); |
首先保存当前线程的 Context 类装入器,然后设置当前线程的 Context 类装入器为 Eclipse 插件的类装入器,接着调用 ejbLookup 方法,最后要确保将当前线程的类装入器还原为原来的类装入器,将还原语句放到 finally 块中是个不错的方法。
再一次运行 Eclipse 插件应用,异常不会出现了,EJB 的查找成功。结果如下:
清单 4 查找 JNDI Name 成功运行
*** Creating initial context ... |
当 我们在 Eclipse 插件中调用第三方的库文件,而这个库文件又使用了线程的 Context 类装入器来加载类,这时,尽管我们把这些库文件放在了插件的根目录下,并在 plugin.xml 中将其声明为运行时库文件,插件在运行时仍然可能会找不到这些库文件中的类。这种情况通常发生于使用 factory 机制的库文件。
在这种情况下,我们可以在 Eclipse 插件程序中通过手工设置当前线程的 Context 类装入器为 Eclipse 插件的类装入器来解决这个问题。
值得一提的是,在Eclipse的最新版本 3.2 中解决了这个问题,在 Eclipse 3.2 的插件程序中我们不需要再在程序中手工设置当前线程的 Context 类装入器。
|
在使用 HTTPS/SSL 调用 Web 服务时抛出类找不到异常
在 Eclipse 中开发一个插件作为 Web 服务的客户端,当我们使用 HTTP 协议调用 Web 服务时,Eclipse 插件工作很正常,但是当 Web 服务要求客户端必须使用基于 HTTPS 传输协议(SSL)来调用 Web 服务时,我们通常会遇到和安全相关的类文件找不到的问题。
首先我们使用 HTTP 的方式来调用 Web 服务。
1.调用 Web 服务的代码如下:
清单 5 调用 Web 服务的方法
|
2.调用以上方法的代码如下:
清单 6 调用callWebSerivce方法
|
注:本文代码使用了 WebSphere 6.0 自带的 WebService 样例应用程序。
你可以通过以下命令来安装这个样例程序:
install_rootsamplesininstall -samples WebServicesSamples
注意,在调用方法 callWebSerivce 时必须设置当前线程的 Context 类装入器为 Eclipse 插件的类装入器,否则会遇到类找不到的问题,其原因和上文讲到的相同。
3.运行 Eclipse 插件并触发以上方法的调用,如果正常的话会得到以下输出。
清单 7 使用 HTTP 调用 Web 服务的结果
|
下面我们修改以上程序,通过 SSL 来调用 Web 服务。
1.修改调用方法 callWebSerivce 的代码中的 Web 服务的端点地址。
清单 8 修改 Web 服务的端点地址
… |
2.然后重新运行 Eclipse 插件应用,在运行之前,确保在 run-time 工作台启动配置中添加下列和安全相关的 Java 虚拟机参数。
清单 9 调用 Web 服务的 Java 虚拟机参数
-Djavax.net.ssl.trustStore=/profiles/default/etc/DummyClientTrustFile.jks |
3.运行结果如下。
清单 10 使用 HTTPS 调用 Web 服务的结果
*** Getting address from address book ... |
应用程序找不到类 com.ibm.crypto.fips.provider.IBMJCEFIPS,这个类位于扩展目录 (jre/lib/ext) 下库文件 ibmjcefips.jar 中,这个库文件是用来提供 Java 加密扩展功能的,属于 Java 虚拟机实现提供的类。
从 图 2 Eclipse 插件类装入器的体系结构中我们可以看到 Eclipse 插件在加载类时不能从默认扩展 (jre/lib/ext目录中的库文件) 中查找类,默认情况下,Eclipse 插件在加载类时其查找顺序如下:
- Java内核引导库文件(rt.jar)
- 需求依赖插件
- 当前插件中的类
- 位于当前插件运行时节的运行时库文件
Java 的默认扩展目录(通常位于 jre/lib/ext 目录)通常存放一些 Java 公共的扩展,例如 Java Cryptography Extensions(JCE)。不同的安全扩展通常放在扩展目录下,这样应用就不需要修改用户的类路径即可实现安全扩展的功能。
但 是有时我们的插件应用会用到默认扩展目录下的库文件,例如当我们在 Eclipse 插件中使用 SSL 传输协议调用 Web 服务时,就需要使用安全扩展相关的库文件。各厂商实现的方式是不同的,比如在 IBM 的 JDK 运行环境中会使用到位于扩展目录下的 IBMJCE provider 库文件,以上问题正是因为当我们使用 SSL 传输协议来调用 Web 服务,使用到了 IBM 提供的 JCE 实现类引起的。
有两个方法可以解决这个问题:
1. 在启动 Eclipse 时设置 Java 虚拟机参数 osgi.parentClassloader 为 "ext",如 图 4 所示.
图 4 Eclipse 插件运行时 Java 虚拟机参数设置
Eclipse 支持在启动的时候设置系统参数 osgi.parentClassloader 为下列类装入器:
- boot:设置为 Java 的引导(Bootstrap)类装入器;
- app:设置为 Java 的系统(System)类装入器;
- ext:设置为 Java 的扩展(Extension)类装入器;
- fwk:设置为 OSGi framework 的类装入器;
当我们将值设为 "ext" 时,Eclipse 插件在加载类时其查找顺序更改为:
- Java 内核引导库文件(rt.jar);
- JRE 默认扩展目录下的库文件;
- 需求依赖插件;
- 当前插件中的类;
- 位于当前插件运行时部分的库文件;
这样当我们运行以上程序时,Eclipse 插件会从默认扩展目录的库文件中查找类,从而成功调用 Web 服务。
但 是这样做的一个问题是:类加载顺序的改变会影响所有的插件,并且由于 Java 扩展机制会加载所有位于扩展目录下的库文件,这样可能会潜在的引起库文件的冲突,因为扩展目录是优先加载的,如果某个 Eclipse 插件运行时库文件列表中和扩展目录下包含一个相同的库文件,Eclipse 将使用扩张目录下的库文件,这也是 Eclipse 为什么没有缺省把扩展目录放在类加载路径中,Eclipse 鼓励每个插件使用自己的类路径来加载类同时避免库文件之间冲突。
2.把 IBMJCE provider 库文件拷贝到 Eclipse 插件目录下,然后把他们添加到运行时库文件列表中。这种方法是把这些 JVM 的库文件看成第三方的库文件,这样在运行时不会影响到其它的 Eclipse 插件。
当我们在插件中使用的类存在于 Java 的扩展目录(jre/lib/ext 目录)下时,这些类默认情况下没有位于插件的类加载路径中,比较常见的一种情况是基于 SSL 传输协议调用 Web 服务时,一些需要的安全加密扩展库文件会位于 Java 扩展目录下。
在这种情况下,我们可以通过设置系统参数 osgi.parentClassloader 或拷贝需要的库文件到 Eclipse 插件目录下并加入到运行时库文件列表中来解决这个问题。
|
在 Eclipse 的最新版本 3.2 中引入了 ContextFinder 类并在系统启动的时候将线程的 Context 类装入器设置为 ContextFinder。
ContextFinder 是用来解决依赖线程 Context 类装入器来加载类问题的。所以在基于 Eclipse 3.2 的插件应用中,我们不用在插件中手工设置当前线程的 Context 类装入器,本文中的 EJB 查找问题不会再出现了。
|
Java 世界中,Classloader 是一个稍微有些神秘的地方,对于系统编程而言,对于 Classloader 的了解还是很有必要的。我们希望本文能够抛砖引玉,使得读者在 Eclipse 平台上编程时可以更好地处理和 Classloader 相关的问题。
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-130068/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/374079/viewspace-130068/