第一部分. 提示
我需要读这篇文章吗?
Java类加载器对Java系统的运行是至关重要的,但是却常常被我们忽略。Java类加载器负载
在运行时查找和加载类。自定义类加载器可以完全改变类的加载方式,以自己喜欢的方式来
个性化你的Java虚拟机。本文简要的介绍Java类加载器,然后通过一个构造自定义类加载器
的例子来说明,这个类加载器在加载类前会自动编译代码。你将学到类加载器到底是干什么
的,如何创建你自己的类加载器。只要你有一些基本的Java知识,知道如何创建、编译、运
行一个命令行Java程序以及一些Java类文件的基本概念,你就可以理解本文的内容了。读完
本文,你应该能够:
* 扩张Java虚拟机的功能
* 创建一个自定义的类加载器
* 如何把自定义的类加载器整合到你的应用程序中
* 修改你的类加载器以兼容Java2
获得帮助
对本文有任何问题,可以联系作者Greg Travis,油箱:mito@panix.com 。
第二部分. 简介
类加载器是什么?
Java和其他语言不同的是,Java是运行于Java虚拟机(JVM)。这就意味着编译后的代码是以
一种和平台无关的格式保存的,而不是某种特定的机器上运行的格式。这种格式和传统的可
执行代码格式有很多重要的区别。具体来说,不同于C或者C++程序,Java程序不是一个独
立的可执行文件,而是由很多分开的类文件组成,每个类文件对应一个Java类。 另外,这
些类文件并不是马上加载到内存,而是当程序需要的时候才加载。 类加载器就是Java虚拟
机中用来把类加载到内存的工具。而且,Java类加载器也是用Java实现的。这样你就不需要
对Java虚拟机有深入的理解就可以很容易创建自己的类加载器了。
为什么要创建类加载器?
既然Java虚拟金已经有了类加载器,我们还要自己创建其他的呢?问得好。默认的类加载器
只知道如何从本地系统加载类。当你的程序完全在本机编译的话,默认的类加载器一般都工
作的很好。但是Java中最激动人心的地方之一就是很容易的从网络上而不只是本地加载类。
举个例子,浏览器可以通过自定义的类加载器加载类。 还有
很多加载类的方式。除了简单的从本地或者网络外,你还可以通过自定义Java中最激动人心
的地方之一:
* 执行非信任代码前自动验证数字签名
* 根据用户提供的密码解密代码
* 根据用户的需要动态的创建类
你关心的任何东西都能方便的以字节码的形式集成到你的应用中
自定义类加载器的例子
如果你已经使用过JDK(Java软件开发包)中的appletviewer(小应用程序浏览器)或者其他
Java嵌入式浏览器,你就已经使用了自定义类加载器了。Sun刚刚发布Java语言的时候,最
令人兴奋的一件事就是观看Java如何执行从远程网站下载的代码。执行从远程站点通过HTT
P连接传送来的字节码看起来有点不可思议。之所以能够工作,因为Java有安装自定义类加
载器的能力。小应用程序浏览器包含了一个类加载器,这个类加载器不从本地找Java类,而
是访问远程服务器,通过HTTP加载原始字节码文件,然后在Java虚拟机中转化为Java类。当
然类加载器还做了其他的很多事情:他们阻止不安全的Java类,而且保持不同页面上的不同
小程序不会互相干扰。Luke Gorrie写的一个包Echidna是一个开放的Java软件包,他允许在
一个Java虚拟机中安全的运行多个Java应用程序。它通过使用自定义类加载器给每个应用程
序一份类文件的拷贝来阻止应用程序之间的干扰。
我们的类加载器例子
我们知道了类加载器是如何工作的,也知道如何定义自己的类加载器了,接下来我们创建一
个名字为CompilingClassLoader (CCL)的自定义类加载器。CCL为我们做编译工作,我们就
不用自己手动编译了。 这基本上相当于有一个"make"程序构建到我们的运行环境。
注意:我们进行下一步之前,有必要搞清楚一些相关的概念。
系统在JDK版本1.2(也就是我们说的Java 2平台)得到很到改进。本文是在JDK1.0和1.1的
版本下写的,但是所有的东西都能在后来的版本工作。ClassLoader也在Java2种有所改进,
第五部分有详细介绍。
第三部分.ClassLoader的结构
总揽
类加载器的基本目的是服务于对Java类的请求。Java虚拟机需要一个类的时候,就把一个类
名给类加载器,然后类加载器试图返回一个对应的类实例。可以通过在不同的阶段覆盖相应
的方法来创建自定义的类加载器。接下来我们将了解到类加载器的一些主要方法。你会明白
这些方法是干什么的,他们在加载类文件的时候是如何工作的。你还将知道创建自定义类加
载器的时候需要写哪些代码。在下一部分,你将利用这些知识和我们自定义的CompilingCl
assLoader一起工作。
方法 loadClass
ClassLoader.loadClass() 是ClassLoader的入口点。方法签名如下:
Class loadClass( String name, boolean resolve);
参数name指定Java虚拟机需要的类的全名(含包名),比如Foo或者java.lang.Object。
参数 resolve指定该类是否需要解析
你可以把类的解析理解为完全为运行做好准备。解析一般都不需要。如果Java虚拟机只想知
道这个类是否存在或者想知道它的父类的话,解析就完全没有必要了。 在Java1.1和它以前
的版本,如果要自定义类加载器,loadClass方法是唯一需要在子类中覆盖的方法.
(ClassLoader在Java1.2中有所改变,提供了方法findClass())。
方法 defineClass
defineClass 是ClassLoader中一个很神秘的方法。这个方法通过一个字节数组来构建类实
例。这个包含数据的原始字节数组可能来自文件系统,也可能是来自网络。defineClass 表
明了Java虚拟机的复杂性,神秘性和平台依赖性-它通过解释字节码把它转化为运行时数据
结构,检查有效性等等。但是不用担心,这些都不用你去实现。其实,你根本不能覆盖它,
因为该方法被关键字final修饰。
方法 findSystemClass
findSystemClass方法从本地系统加载文件。它在本地系统寻找类文件,如果找到了,调用
defineClass把原始字节数组转化成类对象。这是运行Java应用时Java虚拟机加载类的默认
机制。对于自定义类加载器,只有在我们无法加载之后才需要用findSystemClass。 原因很
简单: 我们的类加载器负责执行类加载中的某些特定的步骤,但并不是对所有的类。比如,
即使我们的类加载器从远程站点加载了某些类,仍然有很多基本的类要从本地系统加载。
这些类不是我们关心的,所以我们让Java虚拟机以默认的方式加载他们:从本地系统。这就
是findSystemClass做的事情。整个过程大致如下:
* Java虚拟机请求我们自定义的类加载器加载类。
* 我们检查远程站点是否有这个需要加载的类。
* 如果有,我们获取这个类。
* 如果没有,我们认为这个是类在基本类库中,调用findSystemClass从文件系统中加载。
在大多数自定义类加载器中,你应该先调用findSystemClass来节省从远程查找的时间。
实际上,正如我们将在下一部分看到的,只有当我们确定我们已经自动编译完我们的代码后
才允许Java虚拟机从本地文件系统加载类。
方法resolveClass
正如上面说的,类记载可以分为部分加载(不解析)和完全加载(包括解析)。我们创建自
定义类加载器的时候,可能要调用resolveClass。
方法 findLoadedClass
findLoadedClass实现一个缓存:当要求loadClass来加载一个类的时候,可以先调用这个方
法看看这个类是否已经被加载,防止重新加载一个已经被加载的类。这个方法必须先被调用
,我们看一下这些方法是如何组织在一起的。
我们的例子实现loadClass执行以下的步骤。(我们不指定通过某种具体的技术获得类文件
,-它可能从网络,从压缩包或者动态编译的。无论如何,我们获得的是原始字节码文件)
* 调用findLoadedClass检查这个类是否已经加载。
* 如果没有加载,我们通过某种方式获得原始字节数组。
* 假如已经获得该数组,调用defineClass把它转化成类对象。
* 如果无法获得该原始字节数组,调用findSystemClass 检查是否可以从本地文件系统中记
载。
* 如果参数resolve为true,调用resolveClass来解析类对象。
* 如果还没有找到类,抛出一个ClassNotFoundException异常。
* 否则,返回这个类。
现在我们对类加载器的应用知识有个较全面的了解,可以创建自定义类加载器了。在下一部
分,我们将讨论CCL。
第四部分. CompilingClassLoader
CCL给我们展示了类加载器的功能, CCL的目的是让我们的代码能够自动编译和更新。下面描
述它是怎么工作的:
* 当有一个类的请求时,先检查磁盘的当前目录和子目录上是否存在这个类文件。
* 如果没有类文件,但是却有源代码文件,调用Java编译器编译生成类文件。
* 如果类文件已经存在,检查该类文件是否比源代码文件陈旧。如果类文件比源代码文件陈
旧,调用Java编译器重新生成类文件。
* 如果编译失败,或者由于其他原因导致无法从源文件生成类文件,抛出异常ClassNotFou
ndException。
* 如果还没有获得这个类,可能存在其他的类库里,调用findSystemClass看是否能找到。
* 如果没有找到,抛出异常ClassNotFoundException。
* 否则,返回该类。
Java编译是如何实现的?
在我们进一步讨论前,我们需要先弄清楚Java的编译过程。通常,Java编译器不仅仅编译指
定的那些类。如果指定的那些类需要的话,它还会编译其它的一些相关类。CCL会一个一个
的编译我们在应用程序中需要编译的那些类。不过,一般来说,编译器编译完第一个类后,
CCL将会发现其实其他需要的相关类已经被编译了。为什么呢?Java编译器使用我们差不多
的规则:如果一个了类不存在或者源文件已经被更新,就会编译这个类。Java编译器基本上
比CCL早了一步,大部分工作都被Java编译器完成了。我们看起来就像是CCL在编译这些类。
在大多数情况下,你将发现它是在主函数类中调用编译器,就仅仅这些而已--简单的一个调
用就够了。 不过有一种特殊情况,这些类在第一次出现的时候不编译。如果你根据类名加
载一个类,使用方法Class.forName,Java编译器并不知道是否需要这个类。在这种情况下,
你发现CCL再次调用编译器来编译该类。第六部分的代码说明了这个过程。
使用CompilationClassLoader
为了使用CCL,我们不能直接运行我们的程序,必须以一种特殊的方式运行,就像这样:
% java Foo arg1 arg2
我们这样运行它:
% java CCLRun Foo arg1 arg2
CCLRun是一个特殊的存根程序,它来创建CompilingClassLoader 并且用它来加载我们的主
函数类,这样可以确保所有的整个程序都是由CompilingClassLoader加载的。CCLRun利用Ja
va反射API来调用主函数类的主函数并且给这个函数传递参数。想了解更多,参考第六部分
的源代码。
运行示例
我们演示一下整个过程式怎么工作的。
主程序是一个叫做Foo的类,它创建一个类Bar的实例。这个Bar实例又创建一个类Baz的实例
,类Baz存在于包baz中,这是为了演示CCL如何从子包中加载类。Bar还根据类名加载类Boo
,这个也是CCL完成的。所有的类都加载了就可以运行了。利用第六章的源代码来执行这个
程序。编译CCLRun和CompilingClassLoader。确保你没有编译其它的类(Foo, Bar, Baz, a
nd Boo),否则CCL将不起作用,。
% java CCLRun Foo arg1 arg2
CCL: Compiling Foo.java...
foo! arg1 arg2
bar! arg1 arg2
baz! arg1 arg2
CCL: Compiling Boo.java...
Boo!
注意到为了Foo.java第一次调用编译器,同时也把Bar和baz.Baz一起编译了俄。而类Boo
直道需要加载的时候,CCL才再次调用编译器来编译它。
第五部分.Java2中对类加载器的改进
概览
在Java1.2和以后的版本中, 类加载器有了很大的改进。以前的代码仍然可以工作, 但是新
的系统让我们的实现更容易。这种新模型就是代理委托模型,就是说如果这个类加载器找不
到某个类,它会让他的父类加载器来找。系统类加载器是所有类加载器的祖先, 系统类加载
器通过默认的方式加载类--也就是从本地文件系统中加载。覆盖loadClass方法一般都尝试
几种方式来加载类,如果你写了很多类加载器,你会发现你只是一次又一次在这个复杂的方
法中作一些修改而已。Java1.2种loadClass的默认实现包含了寻找类的最普通的途径,允许
你覆盖findClass方法,loadClass在适当的是否调用findClass方法。这样做的好处是你不
需要覆盖loadClass,你只需要覆盖findClass,这样可以减少工作量。
新增方法: findClass
这个方法被loadClass的默认实现调用。findClass的目标是包含所有类加载器特定的代码,
而不需要重复这些代码(比如在指定的方法失败的时候调用系统类加载器)。
新增方法: getSystemClassLoader
不论你是否覆盖方法findClass和loadClass, 方法getSystemClassLoader都可以直接访问系
统类加载器(而不是通过findSystemClass间接的访问)。
新增方法: getParent
为了把请求委托给父类加载器,通过这个方法可以获得这个类加载器的父类加载器。当自定
义类加载器中的特定方法无法找到类的时候你可能把请求委托给父类加载器。类加载器的父
类加载器包含创建这个类加载器的代码。
第六部分. 源代码
CompilingClassLoader.java
以下是文件CompilingClassLoader.java内容
import java.io.*;
/*
CompilingClassLoader动态的编译Java源文件。它检查.class文件是否存在,.class文件是
否比源文件陈旧。
*/
public class CompilingClassLoader extends ClassLoader
{
// 指定一个文件名,从磁盘读取整个文件内容,返回字节数组。
private byte[] getBytes( String filename ) throws IOException {
// 获得文件大小。
File file = new File( filename );
long len = file.length();
//创建一个数组刚好可以存放文件的内容。
byte raw[] = new byte[(int)len];
// 打开文件
FileInputStream fin = new FileInputStream( file );
// 读取所有内容,如果没法读取,表示发生了一个错误。
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can't read all, "+r+" != "+len );
// 别忘了关闭文件。
fin.close();
// 返回这个数组。
return raw;
}
// 产生一个进程来编译指定的Java源文件,制定文件参数.如果编译成功返回true,否者,
// 返回false。
private boolean compile( String javaFile ) throws IOException {
// 显示当前进度
System.out.println( "CCL: Compiling "+javaFile+"..." );
// 启动编译器
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// 等待编译结束
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// 检查返回码,看编译是否出错。
int ret = p.exitValue();
// 返回编译是否成功。
return ret==0;
}
// 类加载器的核心代码 -加载类在需要的时候自动编译源文件。
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
//我们的目的是获得一个类对象。
Class clas = null;
// 首先,检查是否已经出理过这个类。
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// 通过类名获得路径名 比如:java.lang.Object => java/lang/Object
String fileStub = name.replace( '.', '/' );
// 构建指向源文件和类文件的对象。
String javaFilename = fileStub+".java";
String classFilename = fileStub+".class";
File javaFile = new File( javaFilename );
File classFile = new File( classFilename );
//System.out.println( "j "+javaFile.lastModified()+" c "
//+classFile.lastModified() );
// 首先,判断是否需要编译。如果源文件存在而类文件不存在,或者都存在,但是源文件
// 较新,说明需要编译。
if (javaFile.exists() &&(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// 编译,如果编译失败,我们必须声明失败原因(仅仅使用陈旧的类是不够的)。
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// 可能编译时出现IO错误。
throw new ClassNotFoundException( ie.toString() );
}
}
// 确保已经正确编译或者不需要编译,我们开始加载原始字节。
try {
// 读取字节。
byte raw[] = getBytes( classFilename );
// 转化为类对象
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// 这里并不表示失败,可能我们处理的类在本地类库中,如java.lang.Object。
}
//System.out.println( "defineClass: "+clas );
//可能在类库中,以默认的方式加载。
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// 如果参数resolve为true,根据需要解释类。
if (resolve && clas != null)
resolveClass( clas );
// 如果还没有获得类,说明出错了。
if (clas == null)
throw new ClassNotFoundException( name );
// 否则,返回这个类对象。
return clas;
}
}
CCRun.java
一下是CCRun.java文件
import java.lang.reflect.*;
/*
CCLRun通过CompilingClassLoader加载类来运行程序。
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// 第一个参数指定用户要运行的主函数类。
String progClass = args[0];
// 接下来的参数是传给这个主函数类的参数。
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// 创建CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// 通过CCL加载主函数类。
Class clas = ccl.loadClass( progClass );
// 利用反射调用它的主函数和传递参数。
// 产生一个代表主函数的参数类型的类对象。
Class mainArgType[] = { (new String[0]).getClass() };
// 在类中找到标准的主函数。
Method main = clas.getMethod( "main", mainArgType );
// 创建参数列表 -在这里,是一个字符串数组。
Object argsArray[] = { progArgs };
// 调用主函数。
main.invoke( null, argsArray );
}
}
Foo.java
以下是文件Foo.java内容
public class Foo
{
static public void main( String args[] ) throws Exception {
System.out.println( "foo! "+args[0]+" "+args[1] );
new Bar( args[0], args[1] );
}
}
Bar.java
以下是文件Bar.java内容
import baz.*;
public class Bar
{
public Bar( String a, String b ) {
System.out.println( "bar! "+a+" "+b );
new Baz( a, b );
try {
Class booClass = Class.forName( "Boo" );
Object boo = booClass.newInstance();
} catch( Exception e ) {
e.printStackTrace();
}
}
}
baz/Baz.java
以下是文件baz/Baz.java内容
package baz;
public class Baz
{
public Baz( String a, String b ) {
System.out.println( "baz! "+a+" "+b );
}
}
Boo.java
以下是文件Boo.java内容
public class Boo
{
public Boo() {
System.out.println( "Boo!" );
}
}
第七部分. 总结
总结
通过本文你是否意识到,创建自定义类加载器可以让你深入到Java虚拟机的内部。你可以从
任何资源加载类文件,或者动态的生成它,这样你就可以通过扩展这些功能做很多你感兴趣
的事,还能完成一些强大的功能。
关于ClassLoader的其它话题
就像本文开头说的,自定义类加载器在Java嵌入式浏览器和小应用程序浏览器中起着重要的
作用。下面给出类加载器的其它功能。
* 安全: 自定义的类加载器可以在把这个类交给Java虚拟机之前检查它是否有正确的数字
签名。你也可以自己创建一个"沙箱"来阻止对某些方法的调用,这是通过检查源代码,阻止
该类对沙箱之外的操作来实现的。
* 加密:通过自定义类加载器可以动态的解码,所有你的类文件就无法通过反编译被查看到
代码。用户需要密码才能运行程序,这个密码用来对代码解密。
* 存档:你是否需要将你的代码以某种格式或者压缩形式发布吗?自定义ClassLoader可以
从你想要的任何资源中生成字节码文件。
* 自提取程序:可以把整个应用程序编译到一个可执行的类文件中,这个文件包括压缩过或
者加密过的数据,有了内部类加载器,当程序运行的时候,他把自己解包到内存-不需要事
前安装。
* 动态生成:可以动态的生成那些被引用的类-整个程序需要用的类都可以动态的生成然后
交给Java虚拟机。