尽管 Java 类路径看上去是个很简单的概念,但它也经常是困惑和麻烦的源泉。本文将向您展示一个简单的工具,它可以清楚地确定类装载器从您的类路径中载入了什么 Java 类。(1,000 字)
开发人员在处理 Java 类路径时经常会遇到一些尴尬:他们不总是很清楚类装载器将要载入什么类,尤其是在应用程序的类路径被大量的路径和文件充斥的情况下更是如此。在本文中,我将介绍一个工具,它可以显示被载入的类文件的绝对路径。
类路径简介
Java 虚拟机(JVM)使用一个类装载器根据应用程序的需要载入所需的类。CLASSPATH 环境变量告诉类装载器去哪里找第三方和用户自定义的类。您也可以利用 -classpath JVM 命令行参数为每个应用程序指定类路径,这个路径将重载 CLASSPATH 环境变量指定的类路径。
类路径的内容可以是各种目录,例如没有包含在包中的类文件所在的目录,某个包的根目录,或者包含类的压缩文件(.zip 或 .jar 文件等)。类路径中的各个路径在 Unix 类型的系统中用冒号分隔,在 MS Windows 系统中用分号分隔。
类装载器按照一种委托树的形式组织,其中每个类装载器都有一个父类装载器。当系统要求某个类装载器查找一个类时,它在查找类之前首先把该请求委 托给它的父类装载器。您系统中的 JDK 或 JRE 提供的默认类装载器是系统类装载器,它利用 CLASSPATH 环境变量或 -classpath JVM 命令行参数载入第三方和用户自定义的类。系统类装载器委托扩展类装载器载入使用 Java 扩展机制的类。扩展类装载器委托自引导类装载器(委托到此为止)载入 JDK 的核心类。
您可以开发特定的类装载器指导 JVM 如何动态地载入类。例如,大多数 Servlet 引擎使用自定义的类装载器,当指定的类路径中的类发生改变后,它可以动态地重新调入这些 servlet 类。
这里,十分重要而且很让人吃惊的一点是,类装载器根据类在类路径中的存放顺序进行载入。从第一个路径开始,类装载器访问每个指定的目录或压缩文件来查找要载入的类。它载入第一个名字匹配的类,而剩下的路径将被忽略。
听起来很简单,是吗?
类路径中的圈套
无论那些 Java 开发人员承认与否,他们中的新手和老手都时常(通常是最糟糕的时候)被麻烦的类路径所愚弄。随着应用程序中的第三方和用户自定义类的增加,类路径变得拥挤 不堪,所以类装载器首先载入哪个类并不总是那么显而易见的,这一点在类路径中包含路径双重拷贝的情况下尤为突出。请记住,类装载器载入第一个名字匹配的 类,并将其他同名而优先级较低的类“有效”地隐藏起来。
无论那些 Java 开发人员承认与否,他们中的新手和老手都时常(通常是最糟糕的时候)被麻烦的类路径所愚弄。随着应用程序中的第三方和用户自定义类的增加,类路径变得拥挤 不堪,所以类装载器首先载入哪个类并不总是那么显而易见的,这一点在类路径中包含路径双重拷贝的情况下尤为突出。请记住,类装载器载入第一个名字匹配的 类,并将其他同名而优先级较低的类“有效”地隐藏起来。
这一切看上去都太简单,以至于我们好像不可能落入类路径的这个圈套。那么,就让我们看一看。在经过了一整天繁重的键盘操作后,您想在类路径中添 加一个目录,以便应用程序可以调入某个类的最新版本。然而,您没有注意到这个类的另一个版本已经位于某个优先级较高的目录中。就这样,您中招了!
JWhich:一个简单的类路径工具
扁平型路径声明中的优先级问题并不是 Java 类路径所独有的。要解决这一问题,您只需站在传说中的软件巨人的肩膀上即可。Unix 操作系统的 which 命令接受一个名字做为参数,只要这个名字是一个命令,它就显示与之相关的可执行文件的路径。这个命令首先扫描 PATH 环境变量,确定要查找的命令出现的第一个位置。这听起来也是一个管理 Java 类路径的强有力的工具。受这个思路的启发,我开始编写一个 Java 程序。这个程序可以根据一个 Java 类名显示需要载入的类文件的绝对路径,它和类路径中指定的路径一致。
扁平型路径声明中的优先级问题并不是 Java 类路径所独有的。要解决这一问题,您只需站在传说中的软件巨人的肩膀上即可。Unix 操作系统的 which 命令接受一个名字做为参数,只要这个名字是一个命令,它就显示与之相关的可执行文件的路径。这个命令首先扫描 PATH 环境变量,确定要查找的命令出现的第一个位置。这听起来也是一个管理 Java 类路径的强有力的工具。受这个思路的启发,我开始编写一个 Java 程序。这个程序可以根据一个 Java 类名显示需要载入的类文件的绝对路径,它和类路径中指定的路径一致。
下面的 JWhich 示例显示了类 com.clarkware.ejb.ShoppingCartBean 的绝对路径,它是类装载器载入该类时遇到的第一个路径,属于目录:
> java JWhich com.clarkware.ejb.ShoppingCartBean
Class 'com.clarkware.ejb.ShoppingCartBean' found in
'/home/mclark/classes/com/clarkware/ejb/ShoppingCartBean.class'
'/home/mclark/classes/com/clarkware/ejb/ShoppingCartBean.class'
下面的 JWhich 示例显示了类 javax.servlet.http.HttpServlet 的绝对路径,它是类装载器载入该类时遇到的第一个路径,属于压缩文件:
> java JWhich javax.servlet.http.HttpServlet
Class 'javax.servlet.http.HttpServlet' found in
'file:/home/mclark/lib/servlet.jar!/javax/servlet/http/HttpServlet.class'
'file:/home/mclark/lib/servlet.jar!/javax/servlet/http/HttpServlet.class'
JWhich 如何工作
如果要完全确定类路径中的哪个类会被载入,您就必须搞清楚类装载器的“想法”。这并不像听上去那么难--直接问它就可以了!下面是与 JWhich 相关的代码。有关全部代码,请参见资源。
1: public class JWhich {
2:
3: /**
4: * Prints the absolute pathname of the class file
5: * containing the specified class name, as prescribed
6: * by the current classpath.
7: *
8: * @param className Name of the class.
9: */
10: public static void which(String className) {
11:
12: if (!className.startsWith("/")) {
13: className = "/" + className;
14: }
15: className = className.replace('.', '/');
16: className = className + ".class";
17:
18: java.net.URL classUrl =
19: new JWhich().getClass().getResource(className);
20:
21: if (classUrl != null) {
22: System.out.println("\nClass '" + className +
23: "' found in \n'" + classUrl.getFile() + "'");
24: } else {
25: System.out.println("\nClass '" + className +
26: "' not found in \n'" +
27: System.getProperty("java.class.path") + "'");
28: }
29: }
30:
31: public static void main(String args[]) {
32: if (args.length > 0) {
33: JWhich.which(args[0]);
34: } else {
35: System.err.println("Usage: java JWhich <classname>");
36: }
37: }
38: }
首先,您需要稍微调整一下类的名字,以便类装载器可以接受它(第 12-16 行)。在类名前加上“/”而不是载入该类的包的名称,这样可以指示类装载器在类路径中逐字核对这个类名。将每个“.”换成“/”可以将类的名称转变为一个 有效的 URL 地址,类装载器需要使用这种格式的地址。
接下来,系统向类装载器查询与给定格式的类名称相符的资源(第 18-19 行)。每个 Class 对象都有对它进行载入的 ClassLoader 对象的引用,所以系统会查询载入 JWhich 类的类装载器对象。Class.getResource() 方法实际上委托载入该类的类装载器执行操作,它会返回类文件资源的 URL 地址。如果带有指定类名称的类文件资源不在当前类路径中,它将返回 null。
最后,如果带有指定类名称的类文件资源在当前的类路径中,那么系统将显示它的绝对路径(第 21-24 行)。做为调试手段,如果类文件不在当前类路径中,那么您将获得 java.class.path 系统属性的值,即显示当前的类路径(第 24-28 行)。
在使用 servlet 引擎类路径的 Java servlet 或使用 EJB 服务器类路径的 Engerprise JavaBean (EJB)中,我们可以很容易地想象出上面这些简单代码是如何被载入的。例如在 servlet 引擎中,如果一个自定义的类装载器载入 JWhich 类,那么系统将使用 servlet 引擎的类装载器查找类。如果 servlet 引擎的类装载器不能找到某个类,那么它将委托给它的父类装载器。通常来说,当某个类装载器载入 JWhich 时,它的类装载器或某个父类装载器需要载入的类都可以被找到。
结论
如果需求是发明之母,那么帮助管理 Java 类路径的工具就来得太迟了。与 Java 相关的新闻组和邮件列表中充满了有关类路径的问题。我们需要降低新的开发人员进入这个领域所面临的障碍,以便我们都可以继续工作在一个更高的层次上。 JWhich 是一个简单而功能强大的工具,它可以帮助您掌握任何环境下的 Java 类路径。
如果需求是发明之母,那么帮助管理 Java 类路径的工具就来得太迟了。与 Java 相关的新闻组和邮件列表中充满了有关类路径的问题。我们需要降低新的开发人员进入这个领域所面临的障碍,以便我们都可以继续工作在一个更高的层次上。 JWhich 是一个简单而功能强大的工具,它可以帮助您掌握任何环境下的 Java 类路径。
关于作者
Mike Clark 是 Clarkware Consulting 公司的独立顾问,专门负责使用 J2EE 技术进行基于 Java 的结构设计和开发。他最近完成了一个 B2B XML exchange 服务器的开发和发布,目前是一个项目的顾问,该项目要构建一个 J2EE 性能管理产品。