了解 JAVA 类加载器
2024年03月12日
什么是ClassLoader?在流行的商业化编程语言中,Java语言由于在Java虚拟机(JVM)上运行而差异化。这意味着已编译的程序是一种特殊的、独立于平台的格式,并非依赖于它们所运行的机器。在很大程度上,格式不同于传统的执行程序格式。
与C或C++编写的程序这种不同,Java程序不是一个执行文件,而是由许多独立的执行程序的类文件组成,每个文件对应一个Java类。
另外,这些类文件并不是立即全部都导入内存,而是根据程序需要导入内存。ClassLoader是JVM中将类导入内存的那部分。
而且,Java ClassLoader就是用Java语言编写的。这意味着创建自己的ClassLoader非常容易,不一定要了解JVM的微小细节。为什么要
编写ClassLoader?
如果JVM已经有一个ClassLoader,那么为什么还要编写另一个呢?好。重构的ClassLoader只知道如何从本地文件系统导入类文件。不过这只适合于常规情况语言,即已全部编译完成Java程序,并且计算机处于等待状态。
但Java最新意的事就是JVM可以非常容易地从那些非本地硬盘或从网络上获取类。例如,浏览者可以使用定制的ClassLoader从Web站点安装安装内容。
还有许多其他方式可以获取类文件。除了简单地从本地或网络上获取类文件之外导入文件以外,可以使用定制的ClassLoader完成以下任务:
在执行非设置信代码之前,自动验证数字签名
使用用户提供的密码透明地
创建解密代码动态地可以符合用户特定需要的定制化构建类
任何您认为的生成Java字节码的内容都可以集成到应用程序中。
定制ClassLoader示例
如果使用通过JDK或任何基于Java浏览器中的Applet查看器,那么您肯定会使用过定制的ClassLoader。
Sun最初发布Java语言时,其中最令人兴奋的一件事是观看这项新技术是如何在运行时从远程的Web服务器插入代码。(此外,还有更令人兴奋的事情--Java技术提供了种编写代码的强大语言。)更令人兴奋的是它可以从远程Web服务器执行通过HTTP连接传输过来的字节码。
这样的功能栈Java语言可以安装定制ClassLoader。Applet查看器包含一个ClassLoader,它在本地文件系统中寻找类,而是访问远程服务器上的Web站点,经过HTTP读取原始的字节码文件,并将它们转换成JVM内的类。浏览器和
Applet查看器中的类ClassLoaders还可以做其他事情:它们支持安全性以及使不同的Applet在不同的页面上运行而互不干扰。
Luke Gorrie编写的Echidna是一个开放源码包,它可以使您在单个虚拟机上运行多个Java应用程序。它使用定制的ClassLoader,通过向每个应用程序提供该类文件的自身副本,以防止应用程序相互干扰。我们的ClassLoader示例了解了ClassLoader如何工作
以及
如何编写ClassLoader之后,我们将创建名称作CompilingClassLoader(CCL)的类加载器。CCL为我们编译Java代码,而无需我们介入这个过程。它基本上只是构建到运行时系统中的“make”程序。注:
进一步了解,之前应注意在JDK版本1.2中已经改进了ClassLoader系统的某些方面(即Java 2平台)。本教程是按JDK版本1.0和1.1写的,但也可以在更高的版本中运行。Java 2中ClassLoader的
描述有了Java版本1.2中的震动,并提供了一些详细信息,以便修改ClassLoader来利用这些震动。ClassLoader
的基本目标是对类的请求提供服务。当JVM根据需要使用类时,它的名称是向ClassLoader请求这个类,然后ClassLoader尝试返回一个表示该类的Class对象。通过覆盖该过程不同阶段的方法,可以创建定制的ClassLoader。在本文的
其余部分,您会学习Java ClassLoader的关键方法。您将了解每一个方法的作用以及它是如何适合导入类文件这个过程的。您也知道,创建自己的ClassLoader时,需要编写什么代码。在接下来的中,您将利用这些知识来使用我们的
ClassLoader示例--CompilingClassLoader方法
loadClass
ClassLoader.loadClass()是ClassLoader的入口点。其特征如下:
Class loadClass(String name,boolean resolve);
name参数指定了JVM需要的类的名称,该名称以包表示方式表示,如Foo或java.lang.Object。resolve参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。总是需要解析的。如果JVM要知道该类是否存在或查找该类的超类,那么就不需要解析。在
Java版本1.1及之前的版本中,loadClass方法是定制的ClassLoader创建时唯一需要覆盖的方法。(Java 2中ClassLoader的闹钟提供了关于Java 1.2中findClass()方法的信息。)
方法DefineClass
DefineClass方法是ClassLoader的主要技巧。该方法接受由原始字节组成的负载并将其转换为Class原始导入包含如从文件系统或网络导入的数据。
定义类管理JVM的许多复杂、神秘和依赖于实现的方面--它将字节码分析成运行时数据结构、校验有效性等等。不必担心,您需要花钱编写它。事实上,即使您想
findSystemClass方法
findSystemClass方法从本地文件系统导入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将原始字节转换生成Class对象,以转换文件转换成类。当运行Java应用程序时,这是JVM正常导入类的队列机制。(Java 2中ClassLoader的波动提供了关于Java版本1.2这个过程的详细信息。)
对于定制的ClassLoader,只有在尝试其他方法导入类之后,再使用findSystemClass。原因很简单:ClassLoader是负责执行导入类的特殊步骤,而不是负责所有类。例如,ClassLoader即使来自远程的Web站点导入了某些类,仍然需要在本地机器上导入大量的基本Java库。而这些类不是我们所关心的,所以要JVM以搜索的方式导入它们:从本地文件系统。这就是findSystemClass的用途。
其工作流程如下:
请求定制的ClassLoader导入类。
检查远程Web站点,查看是否有需要的类。
如果有,那就好;抓取这个类,完成任务。
如果没有,假设这个类在基本Java库中,那么调用findSystemClass,以便从文件系统导入该类。
在大多数定制ClassLoaders中,首先调用findSystemClass以节省在本地就可以导入的许多Java库类而要在远程Web站点上查找所花的时间。但是,正如在下一章节所看到的,直到确信能够自动编译我们的应用程序时代码,才让JVM从本地文件系统导入类。方法
resolveClass
正如前面所提到的,可以不完全地(不带解析)导入类,也可以完全地(带解析)导入类。当编写我们自己的loadClass时,可以调用resolveClass,这取决于loadClass的resolve参数的值。
方法findLoadedClass
findLoadedClass等待一个缓存:当请求loadClass导入类时,它调用该方法来查看ClassLoader是否已导入这个类,这样可以避免重新导入已存在类造成的麻烦。应首先调用该
方法
。一下如何构建所有方法。
我们的loadClass实现示例执行以下步骤。(这里,我们没有指定生成类文件是采用哪种技术--它可以是从网络上导入、或者从归档文件中提取、或者实时编译它。无论是哪一种,那是一种特殊的神奇方式,使我们获得了数十亿的原始类文件。)
CCL揭秘
我们的类加载器(CCL)的任务是确保代码被编译和更新。
下面描述了的工作方式:
当请求一个类时,先查看它是否在磁盘的当前目录或相应的子目录中。
如果该类不存在,但源码中有,那么调用Java编译器来生成类文件。
如果该类已存在存在,检查它是否比源码旧。如果是,调用Java编译器来重新生成类文件。
如果编译失败,或者由于其他原因不能从现有的源码中生成类文件,返回ClassNotFoundException。
如果仍然没有该类,也许在其他库中,所以调用findSystemClass来寻找该类。
如果还是没有,则返回查看ClassNotFoundException。
否则,返回该类。
调用findLoadedClass来是否存在已导入的类。
如果没有,则采用那种特殊的类神奇的方式来获取原始字节。
如果有原始字节,调用defineClass将它们转换成类对象。
如果没有原始字节,则调用findSystemClass查看是否从本地文件系统获取类。
如果resolve参数为true,则调用resolveClass解析Class对象。
如果还没有类,返回ClassNotFoundException。
否则,将类返回给调用程序
。Java编译的工作方式
退在深入讨论之前,应该先一步,讨论Java编译。通常,Java编译器不只是编译要求它编译的类。它还会编译其他类,如果这些类是你要求编译的类所需要的类。CCL
逐个编译应用程序中的每一个类都需要编译。但一般来说,在编译器编译做完第一个类后,CCL会找到所有需要编译的类,然后比较它。为什么?Java编译器类似于我们正在使用的规则:如果类不存在,或者与它的源码,它比较旧,现在需要编译。其实,Java编译器在CCL之前的一个步骤中,它会完成大部分工作。
当CCL编译它们时,会报告它正在编译哪个应用程序上的类。在大多数情况下,CCL会在程序中的主类上调用编译器,它会做所有要做的事情——编译器的单一调用已经足够了。
然而,有一种情况,在第一步时不会完成某些类的编译。如果使用Class.forName方法,通过名称来导入类,Java编译器会不知道这个类时候需要什么。在这种情况下,你会看到CCL再次运行Java编译器来编译这个类。来源代码中演示了这个过程。
使用CompilationClassLoader
要使用CCL,必须以特殊方式调用程序。不能直接运行该程序,如:%java Foo arg1 arg2
应按以下方式运行:
%java CCLRun Foo arg1 arg2
CCLRun是一个特殊的存根程序,它创建CompilingClassLoader并用它来导入程序的主类,以确保通过CompilingClassLoader来导入整个程序。CCLRun使用Java Reflection API来调用特定类的主方法并将参数传递给它。有关详细信息,请参见源代码。
运行示例
源码包括一组小类,它们演示了工作方式。主程序是Foo类,它创建类Bar的实例。类Bar创建另一个类Baz的实例,它在baz包内,这是为了展示CCL是如何处理子包里的代码。Bar也是通过名称插入的,其名称为Boo,这个用来展示它也能与CCL工作。
每类都声明已被安装进入并运行。现在用源代码来试一下。编译CCLRun和CompilingClassLoader。确保不要编译其他类(Foo、Bar、Baz和Boo),否则将不会使用CCL,因为这些类已经编译过了。
%java CCLRun Foo arg1 arg2
CCL:正在编译Foo.java...
foo!arg1 arg2
吧!arg1 arg2
巴兹!arg1 arg2
CCL:Compiling Boo.java...
Boo!
请注意,首先编译器,Foo.java管理Bar和baz.Baz。直到通过Bar名称来插入Boo时,被调用它,接下来CCL会再次调用CompilingClassLoader.java以下
是
CompilingClassLoader.java的源代码
//$Id$
import java.io.*;
/*
CompilingClassLoader即时编译您的Java源代码。它检查
不存在的.class文件或比
相应源代码更旧的.class文件。
*/
public class CompilingClassLoader extends ClassLoader
{
//给定一个文件名,从磁盘读取整个文件
//并将其作为字节数组返回。
private byte[]getBytes(String filename)throws IOException{
//找出文件的长度File
file=new File(filename);
长长度=file.length();
//创建一个大小正好适合文件
内容的数组
byte raw[]=new byte[(int)len];
//打开文件
FileInputStream fin=new FileInputStream(file);
//将其全部读入数组;如果我们没有得到全部,
//那么这是一个错误。
int r=fin.read(原始);
if(r!=len)
throw new IOException("无法读取全部,"+r+"!="+len);
//不要忘记关闭文件!
fin.close();
//最后以数组形式返回文件内容
return raw;
}//生成一个进程来编译//在“javaFile”参数中指定的
java源代码文件。//如果编译成功
则返回true,否则返回false。private boolean compile(String javaFile)throws IOException{//让用户知道发生了什么System.out.println("CCL:Compiling"+javaFile+"...");//启动编译器进程p=Runtime.getRuntime().exec("javac"+javaFile);//等待它运行完毕try{p.waitFor();
}catch(InterruptedException ie){System.out.println(ie);}
//检查返回码,以防出现编译错误
int ret=p.exitValue();
//判断编译是否成功
return ret==0;
}
//ClassLoader的核心-
在查找类文件时根据需要自动编译//源
public Class loadClass(String name,boolean resolve)
throws ClassNotFoundException{
//我们的目标是获取一个Class对象
Class clas=null;
//首先,看看我们是否已经处理过这个
clas=findLoadedClass(name);
//System.out.println("findLoadedClass:"+clas);
//根据类名创建路径名
//例如java.lang.Object=>java/lang/Object
String fileStub=name.replace('.','/');
//构建指向源代码(.java)和对象
//代码(.class)的对象
String javaFilename=fileStub+".java";
String classFilename=fileStub+".class";
文件javaFile=new File(javaFilename);
文件类文件=新文件(类文件名);
//System.out.println("j"+javaFile.lastModified()+"c"+
//classFile.lastModified());
//首先,看看我们是否要尝试编译。如果(a)
//是源代码,并且(b0)没有目标代码,
//或(b1)有目标代码,但它比源代码旧
if(javaFile.exists()&&
(!classFile.exists()||
javaFile.lastModified()>classFile.lastModified())){
try{
//尝试编译它。如果这不起作用,那么
//我们必须声明失败。
(使用//和已经存在但过时的类文件还不够好)
if(!compile(javaFilename)||!classFile.exists()){
throw new ClassNotFoundException("Compile failed:"+java文件名);
}
}catch(IOException ie){
//如果编译失败,我们可能会遇到的另一个地方
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 class;
CCRun.java
以下是
CCRun.java
的源代码
//$Id$
import java.lang.reflect.*;
/*
CCLRun通过
CompilingClassLoader加载Java程序来执行它。
*/
public class CCLRun
{
static public void main(String args[])throws Exception{
//第一个参数是用户
要运行的Java程序(类)//
String progClass=args[0];
//该程序的参数只是
//参数1..n,因此将它们分成
//它们自己的数组
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);
//使用反射调用其main()方法,并
//传入参数。
//获取一个表示main方法参数类型的类
Class mainArgType[]={(new String[0]).getClass()};
//查找类中的标准main方法
Method main=clas.getMethod("main",mainArgType);
//创建一个包含参数的列表-在本例中,
//一个字符串数组
Object argsArray[]={progArgs};
//调用方法
main.invoke(null,argsArray);
}
}
Foo.java
以下是Foo.java的源代码
//$Id$
public class Foo
{
static public void main(String args[])throws Exception{
System.out.println("foo!"+args[0]+""+args[1]);}
新的酒吧(参数[0],参数[1]);
Bar.java
以下是
Bar.java
的源代码
//$Id$
import baz.*;
public class Bar
{
public Bar(String a,String b){
System.out.println("bar!"+a+""+b);}
新巴兹(a,b);
尝试{
Class booClass=Class.forName("Boo");
对象boo=booClass.newInstance();
}catch(异常e){
e.printStackTrace();
baz/Baz.java以下是baz/Baz.java
的源代码//$Id$package baz;public class Baz{public Baz(String a,String b){System.out.println("baz!"+a+""+b);}}}Boo.java以下是Boo.java的源代码//$Id$public class Boo{public Boo(){System.out.println("Boo!");}}