转自:http://hi.baidu.com/geniusian/blog/item/20ff862adb382799033bf685.html
概要
许多Java程序员在用Tomcat5进行WEB开发和部署过程中,都会遇到与ClassLoader有关的问题.例如经常出现的
java.lang.NoClassDefFoundError.在本文下面的叙述中,将首先介绍Class Loader(类装载器)的运行机制,然后再介绍Tomcat5中的
ClassLoader,以及在Tomcat5实际操作中遇到的问题和解决方法.
一. Class Loader(类装载器)
1. 类装载器结构
类装载器(Class Loader)是Java虚拟机的组成部分之一,如图1所示.Java虚拟机有两种类装载器:启动类装载器和用户自定义类装载器.
URL:http://blog.blogchina.com/upload/2004-11-24/20041124104254943028.jpg
图1:Java虚拟机的内部体系结构.
启动类装载器 每个Java虚拟机实现都必须有一个启动类装载器.在Sun的JDK 1.2以后的版本中,启动类装载器只负责在系统类(核心Java API的class文件)的安装路径中查找要装入的类.
用户自定义类装载器 它是普通的Java对象,它的类必须继承自java.lang.ClassLoader类.在Sun的JDK 1.2以后的版本中,用户自定义类装载器负责核心Java API以外的其它class文件的装载.例如,用于安装或下载标准扩展的class文件,在类路径中发现的类库的class文件,用于应用程序运行的class文件,等等.
命名空间 Java虚拟机为每一个类装载器维护一个唯一标识的命名空间.一个Java程序可以多次装载具有同一个全限定名(指类所属的包名加类名,如java.lang.Object就是类Object的全限定名)的多个类(class). Java虚拟机要确定这"多个类"的唯一性,因此,当多个类装载器都装载了同名的类时,为了唯一地标识这个类,还要在类名前加上装载该类的类装载器的标识(指出了类所位于的命名空间).例如:ExtClassLoader 装载了sun.text.resources.DateFormatZoneData_zh_CN类,AppClassLoader装载了sun.text.resources.DateFormatZoneData_zh_HK类, Java虚拟机就认为这两个类位于不同的包中,彼此之间不能访问私有成员.如果AppClassLoader也装载了sun.text.resources.DateFormatZoneData_zh_CN类, 虽然"类名"相同,Java虚拟机也认为它们是不同的类,因为它们处在不同的命名空间中.
2. 委托(Delegation)模型(注:根据本人粗浅的理解,所谓委托模型就是:老子能装载的就不用儿子孙子装载。儿子孙子遇到的时候交给老子,老子再向上交给老子的老子,直到该处理这个装载的人那里。)
当Java虚拟机开始运行时,在应用程序开始启动以前,它至少创建一个用户自定义装载器,也可能创建多个.所有这些装载器被连接在一个Parent-Child的委托链中,在这个链的顶端是启动类装载器,末端是被称为"系统类装载器"的类装载器.
例如,假设你写了一个应用程序,在虚拟机上运行它.虚拟机在启动时实例化了两个用户自定义类装载器:一个"扩展类装载器",一个"类路径类装载器".这些类装载器和启动类装载器一起联入一个Parent-Child委托链中,如图2所示.
URL:http://blog.blogchina.com/upload/2004-11-24/2004112410430176267.jpg
图2:Parent-Child类装载器委托链
类路径类装载器的Parent是扩展类装载器, 扩展类装载器的Parent是启动类装载器.在图2中,类路径类装载器就被实例为系统类装载器.假设你的程序实例化它的网络类装载器,它就指明了系统类装载器作为它的Parent.
下面的例程说明了类装载器的父子关系.
例程1:
package test;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
private static int count = -1;
public static void testClassLoader(Object obj) {
if (count < 0 && obj == null) {
System.out.println("Input object is NULL");
return;
}
ClassLoader cl = null;
if (obj != null && !(obj instanceof ClassLoader)) {
cl = obj.getClass().getClassLoader();
} else if (obj != null) {
cl = (ClassLoader) obj;
}
count++;
String parent = "";
for (int i = 0; i < count; i++) {
parent += "Parent ";
}
if (cl != null) {
System.out.println(
parent + "ClassLoader name = " + cl.getClass().getName());
testClassLoader(cl.getParent());
} else {
System.out.println(
parent + "ClassLoader name = BootstrapClassLoader");
count = -1;
}
}
public static void main(String[] args) {
URL[] urls = new URL[1];
URLClassLoader urlLoader = new URLClassLoader(urls);
ClassLoaderTest.testClassLoader(urlLoader);
}
}
以上例程的输出为:
ClassLoader name = java.net.URLClassLoader
Parent ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent Parent ClassLoader name = BootstrapClassLoader
类装载器请求过程
以上例程1为例.将main方法改为:
ClassLoaderTest tc = new ClassLoaderTest();
ClassLoaderTest.testClassLoader(tc);
输出为:
ClassLoader name = sun.misc.Launcher$AppClassLoader
Parent ClassLoader name = sun.misc.Launcher$ExtClassLoader
Parent Parent ClassLoader name = BootstrapClassLoader
程序运行过程中,类路径类装载器发出一个装载ClassLoaderTest类的请求, 类路径类装载器必须首先询问它的Parent---扩展类装载器---来查找并装载这个类,同样扩展类装载器首先询问启动类装载器.由于ClassLoaderTest不是Java API(JAVA_HOME\jre\lib)中的类,也不在已安装扩展路径(JAVA_HOME\jre\lib\ext)上,这两类装载器都将返回而不会提供一个名为ClassLoaderTest的已装载类给类路径类装载器.类路径类装载器只能以它自己的方式来装载ClassLoaderTest,它会从当前类路径上下载这个类.这样,ClassLoaderTest就可以在应用程序后面的执行中发挥作用.
在上例中, ClassLoaderTest类的testClassLoader方法被首次调用,该方法引用了Java API中的类java.lang.String.Java虚拟机会请求装载ClassLoaderTest类的类路径类装载器来装载java.lang.String.就像前面一样,类路径类装载器首先将请求传递给它的Parent类装载器,然后这个请求一路被委托到启动类装载器.但是,启动类装载器可以将java.lang.String类返回给类路径类装载器,因为它可以找到这个类,这样扩展类装载器就不必在已安装扩展路径中查找这个类,类路径类装载器也不必在类路径中查找这个类.扩展类装载器和类路径类装载器仅需要返回由启动类装载器返回的类java.lang.String.从这一刻开始,不管何时ClassLoaderTest类引用了名为java.lang.String的类,虚拟机就可以直接使用这个java.lang.String类了.
在上述过程中也可能会发生错误,在本文下面的例子中将会涉及.
3. 装载 连接及初始化
在一个Java类的生命周期中,装载,连接和初始化只是其开始阶段.只有开始阶段结束以后,类才可以被实例化并被使用.整个开始阶段必须按以下顺序进行:
1)装载 把二进制形式的Java class读入虚拟机中.
2)连接 把已经读入虚拟机的二进制形式的类数据合并到虚拟机的运行状态中去.连接阶段分为验证,准备和解析三个子步骤.
3)初始化 给类变量赋以适当的初始值.
Java虚拟机允许类装载器(启动或用户自定义类装载器)缓存Java class的二进制形式,在预知某个类将要被使用时就装载它.如果一个类装载器在预先装载时遇到问题,它应该在该类被"首次主动使用"时报告该问题(通过抛出一个java.lang.LinkageError的子类).也就是说,如果一个类装载器在预先装载时遇到缺失或错误的class文件,它必须等到程序首次被主动使用该类时才报告错误.如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误.
二. Tomcat5中的Class Loader
当Tomcat5启动的时候,它会首先创建一组class loader,如commonLoader, sharedLoader, catalinaLoader,webappLoader等.其委托模型如下图3所示:
Bootstrap
|
System
|
Common
/ \
Catalina Shared
\
Webapp1 ...
图3:Tomcat5类装载器委托模型
其中,
1) Bootstrap 该类装载器装载JAVA_HOME\jre\lib和JAVA_HOME\jre\lib\ext两目录上的JAR包.
2) System 该类装载器装载当前CLASSPATH上的JAR包.在Windows系统下, CLASSPATH环境变量会在CATALINA_HOME\bin\setclasspath.bat和CATALINA_HOME\bin\catalina.bat文件中被重新设置.
3) Common 该类装载器装载CATALINA_HOME\common\classes目录中的类, CATALINA_HOME\commons\endorsed和CATALINA_HOME\common\lib目录中的JAR包.
4) Catalina 该类装载器装载CATALINA_HOME\server\classes和CATALINA_HOME\server\lib目录中的类和JAR包.
5) Shared 该类装载器装载CATALINA_HOME\shared\classes和CATALINA_HOME\shared\lib目录中的类和JAR包.
6) WebappX 该类装载器装载WEB-INF\classes和WEB-INF\lib目录中的类和JAR包.
需要补充说明的是,WebappX类装载器独立于上文提到的Java2的委托模型.当WebappX类装载器装载一个类时,它会首先查找本身所辖目录(即WEB-INF\classes和WEB-INF\lib)下的类,而不会启动委托机制.当然对于Bootstrap和System类装载器中存在的类,是要进行委托的.另外,对于下面这些包中的类,如:
javax.*
org.xml.sax.*
org.w3c.dom.*
org.apache.xerces.*
org.apache.xalan.*
WebappX类装载器装载时也要启动委托机制.
例如,假设ojdbc14.jar处在setclasspath.bat中的CLASSPATH下,同时也处在WEB-INF\lib目录下.类装载器系统在请求装载oracle.jdbc.driver.OracleDriver类时,会得到从System类装载器返回的类,而不是WebappX类装载器.
再假设ojdbc14.jar处在CATALINA_HOME\common\lib和WEB-INF\lib目录下,而没有处在setclasspath.bat中的CLASSPATH下,那么类装载器系统就会得到从WebappX类装载器返回的类,而不是Common类装载器.
另外,如果WEB-INF\lib目录下存在包含有servlet API类的JAR包,该JAR包将会被WebappX类装载器忽略.例如,
考虑到应用程序的编译问题,你可能会把servlet-api.jar包Copy到应用程序中的WEB-INF\lib目录下.那么,在Tomcat5启动时,启动屏幕上就会出现如下输出:
2004/09/12 14:53:57 org.apache.catalina.loader.WebappClassLoader validateJarFile
情报: validateJarFile(E:\MyData\myProjects\MyBS_SQL\web\WEB-INF\lib\servlet-api.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
上述信息并不影响你的程序运行.因为WebappX类装载器虽然会忽略掉WEB-INF\lib目录下servlet-api.jar包,但是,Common类装载器已经装载了servlet-api.jar包.如前所述,对于javax.*中的类, WebappX类装载器是要启动委托机制的,所以WebappX类装载器会得到Common类装载器返回的javax.*中的类.
如果不希望Tomcat5启动时输出上述信息,只需将servlet-api.jar包从应用程序中的WEB-INF\lib目录下移走就行了.
总结一下,Tomcat5类装载器系统在请求装载一个类时,它以下面列举的顺序进行:
· Bootstrap (JAVA_HOME\jre\lib和JAVA_HOME\jre\lib\ext)
· System (当前CLASSPATH上)
· \WEB-INF\classes 和\WEB-INF\lib\*.jar
· CATALINA_HOME\common\classes
· CATALINA_HOME\common\endorsed\*.jar
· CATALINA_HOME\common\lib\*.jar
· CATALINA_HOME\shared\classes 和CATALINA_HOME\shared\lib\*.jar
三. Tomcat5与WEB应用
我们在用Tomcat5进行应用开发时,经常需要将第三方JAR包添加到应用中.通常的作法是Copy这些JAR包到\WEB-INF\lib目录下.
但是,有些情况可能不允许我们这样Copy JAR包到Tomcat5认可的目录下(如本文上一部分提到的Tomcat5下的目录).这样我们就只有修改CATALINA_HOME\bin\setclasspath.bat和CATALINA_HOME\bin\catalina.bat文件中的CLASSPATH变量了.
假设你需要添加ojdbc14.jar到你的应用中,而又不允许你把它从\oracle\ora92\jdbc\lib目录下Copy到别的目录下,于是你可能会修改CATALINA_HOME\bin\setclasspath.bat文件,新的文件中可能存在如下代码:
set CLASSPATH=%CLASSPATH%;D:\oracle\ora92\jdbc\lib\ojdbc14.jar
这样就满足了应用本身的要求.
1. 出现java.lang.NoClassDefFoundError错误
我们继续上面的讨论,同样是在不允许Copy JAR包的情况下.
当你在Tomcat5中部署一个应用时,发现该应用需要一个coreapi.jar包(这个JAR包是你的开发人员为一个正在开发的工具开发的,你要部署的应用是该工具的产品),由于该工具正在开发过程中,你和开发人员没有进行充分地沟通,所以不知道这个JAR包中的某一个类A.class继承了javax.servlet.Servlet类,于是你只在CATALINA_HOME\bin\setclasspath.bat文件添加如下代码,就没有把servlet-api.jar添加到CLASSPATH上.
set CLASSPATH=%CLASSPATH%;D:\myTool\lib\coreapi.jar
当你测试时,发现运行某一个URL时,会出现如下错误:
root cause
java.lang.NoClassDefFoundError: javax/servlet/Servlet
...
下面我们就分析一下为什么会出现这个运行错误.
按照你的配置,coreapi.jar现处在CLASSPATH上,所以A.class被System类装载器装载. System类装载器在装载A.class时,发现它引用了javax.servlet.Servlet类,按照类装载器系统的委托模型,System类装载器会首先请求Bootstrap类装载器,Bootstrap类装载器不能返回javax.servlet.Servlet类给System类装载器,System类装载器自己也不能装载javax.servlet.Servlet类.也就是说,此时的类装载器系统在预装载时遇到一个缺失的javax.servlet.Servlet 类文件.但是,如本文第一部分中的装载 连接及初始化所述,在类装载器系统工作过程中开始阶段发生的这个问题并不会被马上报告,而是在javax.servlet.Servlet类被"首次主动使用时"抛出java.lang.NoClassDefFoundError(java.lang.LinkageError的子类之一).
解决该错误的方法当然就是把servlet-api.jar也添加到CLASSPATH上.如
set CLASSPATH=%CLASSPATH%;D:\myTool\lib\coreapi.jar;%BASEDIR%\common\lib\servlet-api.jar
2. Tomcat5中的UserDatabase
Tomcat5默认配置了一个全局JNDI资源UserDatabase, 默认配置如下:
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved">
factory
org.apache.catalina.users.MemoryUserDatabaseFactory
pathname
conf/tomcat-users.xml
利用它,我们可以修改pathname下的值conf/tomcat-users.xml(该值可配,如改为conf/tomcat-zzz.xml)文件中的数据.
为了修改tomcat-users.xml文件中的数据,我们在WEB应用中可以先取得Object,例程如下:
例程2:
1 try {
2 InitialContext initCtx = new InitialContext();
3 Object obj = initCtx.lookup("java:comp/env/userDatabase");
4 UserDatabase udb = (UserDatabase) obj;
5 } catch (Exception e) {
6 e.printStackTrace();
7 }
当例程2运行时,会产生一个错误,如:
root cause
java.lang.NoClassDefFoundError: org/apache/catalina/UserDatabase
...
上一错误是例程2中的第4行抛出的.因为UserDatabase类处在CATALINA_HOME\server\lib\catalina.jar包中,被Catalina类装载器装载.例程2所在的类处在WebappX类装载器,根据类装载器系统的委托机制, WebappX类装载器不会得到从Catalina类装载器返回的UserDatabase类.所以程序运行到例程2中的第4行时,会抛出java.lang.NoClassDefFoundError.
假设你把CATALINA_HOME\server\lib目录中的catalina.jar包及其相关JAR包Copy到\WEB-INF\lib目录下(当然这仅仅是一种测试情况),程序运行到第4行时,则会出现java.lang.ClassCastException. 因为WebappX类装载器得到的UserDatabase类是其本身装载的,与从JNDI中得到的UserDatabase类处在不同的命名空间中,是不同的类.
你还可以把catalina.jar包及其相关JAR包设置到CLASSPATH下.但还是强烈建议你不要这么做.
既然不能显式地声明UserDatabase类,我们可以通过反射(Reflection)的方式进行方法调用.
例程3:
try {
InitialContext initCtx = new InitialContext();
Object obj = initCtx.lookup("java:comp/env/userDatabase");
Class[] cl =
new Class[] { String.class, String.class, String.class };
Method cm = obj.getClass().getMethod("createUser", cl);
String[] s = new String[] { "zzz", "zzz", "zzz" };
cm.invoke(obj, s);
Method sm = obj.getClass().getMethod("save", null);
sm.invoke(obj, null);
} catch (Exception e) {
e.printStackTrace();
}
这样上述程序就在tomcat-users.xml文件中插入了这样一行数据: