教程提示
我应该学习本教程吗?
Java ClassLoader是Java运行时系统的关键但经常被忽略的组件。 它是负责在运行时查找和加载类文件的类。 创建自己的ClassLoader可以使您以有用且有趣的方式自定义JVM,从而完全重新定义将类文件带入系统的方式。
本教程概述了Java ClassLoader,并引导您构造示例ClassLoader,该示例在加载代码之前自动编译您的代码。 您将确切地了解ClassLoader的功能以及创建自己的类所需的操作。
对Java编程有基本的了解,包括创建,编译和执行简单的命令行Java程序的能力,以及对类文件范例的了解,足以构成本教程的背景。
完成本教程后,您将知道如何:
- 扩展JVM的功能
- 创建一个自定义的ClassLoader
- 了解如何将自定义ClassLoader集成到Java应用程序中
- 修改您的ClassLoader以适应Java 2版本
什么是ClassLoader?
在商业上流行的编程语言中,Java语言通过在Java虚拟机(JVM)上运行而与众不同。 这意味着已编译的程序以特殊的,与平台无关的格式表示,而不是以运行它们的计算机的格式表示。 此格式在许多重要方面与传统的可执行程序格式不同。
特别是,与用C或C ++编写的Java程序不同,Java程序不是单个可执行文件,而是由许多单独的类文件组成,每个文件对应于一个Java类。
此外,这些类文件不会一次全部加载到内存中,而是根据程序需要按需加载。 ClassLoader是JVM的一部分,它将类加载到内存中。
此外,Java ClassLoader是用Java语言本身编写的。 这意味着创建自己的ClassLoader很容易,而无需了解JVM的详细信息。
为什么要编写ClassLoader?
如果JVM具有ClassLoader,那么为什么还要编写另一个? 好问题。 默认的ClassLoader仅知道如何从本地文件系统加载类文件。 当您完全编译Java程序并在计算机上等待时,这对于常规情况是很好的。
但是,关于Java语言的最具创新性的事情之一是,它使JVM可以轻松地从本地硬盘驱动器或网络之外的其他地方获取类。 例如,浏览器使用自定义的ClassLoader从网站加载可执行内容。
还有许多其他获取类文件的方法。 除了简单地从本地磁盘或网络加载文件之外,您还可以使用自定义的ClassLoader来:
- 在执行不受信任的代码之前自动验证数字签名
- 使用用户提供的密码透明地解密代码
- 创建根据用户的特定需求定制的动态构建的类
您可以想到的任何可以生成Java字节码的内容都可以集成到您的应用程序中。
自定义ClassLoader示例
如果您曾经使用过JDK或任何支持Java的浏览器中包含的appletviewer,则几乎可以肯定使用了自定义的ClassLoader。
Sun最初发布Java语言时,最激动人心的事情之一就是观察这项新技术如何执行从远程Web服务器动态加载的代码。 (这是在我们意识到更激动人心的东西之前-Java技术提供了一种出色的语言来编写代码。)执行它是通过远程Web服务器通过HTTP连接发送的字节码,这让人有些激动。
使这一壮举成为可能的是Java语言安装自定义ClassLoader的能力。 appletviewer包含一个ClassLoader,而不是在本地文件系统中查找类,而是访问远程服务器上的网站,通过HTTP加载原始字节码文件,并将其转换为JVM中的类。
浏览器和appletviewer中的ClassLoader也会做其他事情:它们负责安全性,并防止不同页面上的不同applet相互干扰。
Luke Gorrie的Echidna是一个开源软件包,使您可以在一个虚拟机中安全地运行多个Java应用程序。 (请参阅进一步的阅读和参考 。)它使用一个自定义的ClassLoader,通过为每个应用程序提供自己的类文件副本来防止应用程序相互干扰。
我们的示例ClassLoader
了解了ClassLoader的工作方式和编写方式之后,我们将创建自己的自定义ClassLoader,称为CompilingClassLoader(CCL)。 CCL为我们编译我们的Java代码,以防万一我们不用自己去做。 基本上就像直接在我们的运行时系统中内置一个简单的“ make”程序一样。
注意:在继续进行之前,必须注意,ClassLoader系统的某些方面已在JDK 1.2版(也称为Java 2平台)中得到了改进。 本教程在编写时考虑了JDK 1.0和1.1版本,但其中的所有内容也适用于更高版本。
Java 2中的ClassLoader更改描述了Java 1.2版中的更改,并提供了有关修改我们的ClassLoader以利用这些更改的详细信息。
ClassLoader结构
ClassLoader结构概述
ClassLoader的基本目的是为一个类的请求提供服务。 JVM需要一个类,因此它通过名称向ClassLoader询问该类,并且ClassLoader尝试返回代表该类的Class
对象。
通过覆盖与该过程的不同阶段相对应的不同方法,可以创建自定义ClassLoader。
在本节的其余部分,您将学习Java ClassLoader的关键方法。 您将了解每个文件的功能以及它们如何适合于加载类文件的过程。 您还将发现创建自己的ClassLoader时需要编写哪些代码。
在下一部分中,您将把这些知识与我们的示例ClassLoader(CompilingClassLoader)一起使用。
方法loadClass
ClassLoader.loadClass()
是ClassLoader.loadClass()
的入口点。 其签名如下:
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
对象。 原始数组包含例如从文件系统或通过网络加载的数据。
defineClass
处理JVM的许多复杂,神秘且与实现相关的方面-它将字节码格式解析为运行时数据结构,检查有效性等等。 但是不用担心,您不必自己编写它。 实际上,即使您愿意也无法覆盖它,因为它被标记为最终的。
方法findSystemClass
findSystemClass
方法从本地文件系统加载文件。 它在本地文件系统中查找类文件,如果存在,则使用defineClass
将其转换为类,以将原始字节转换为Class
对象。 这是运行Java应用程序时JVM通常如何加载类的默认机制。 ( Java 2中的ClassLoader更改提供了Java 1.2版中对此过程进行更改的详细信息。)
对于我们的自定义ClassLoader,只有在尝试了所有其他加载类之后,才使用findSystemClass
。 原因很简单:我们的ClassLoader负责执行特殊步骤来加载类,但不是所有类。 例如,即使我们的ClassLoader从远程网站加载了某些类,本地计算机上仍然有很多基本Java库也必须加载。 这些类与我们无关,因此我们要求JVM以默认方式从本地文件系统加载它们。 这就是findSystemClass
所做的。
该过程如下:
- 我们的自定义ClassLoader被要求加载一个类。
- 我们检查远程网站,以查看该类是否存在。
- 如果是的话,很好; 我们上课,我们就完成了。
- 如果不存在,则假定该类是基本Java库中的一个,并调用
findSystemClass
从文件系统中加载它。
在大多数自定义ClassLoader中,您希望首先调用findSystemClass
来节省在远程网站上查找通常加载的许多Java库类所花费的时间。 但是,正如我们将在下一节中看到的那样,在确定已经自动编译了应用程序的代码之前,我们不想让JVM从本地文件系统中加载类。
方法resolveClass
如前所述,加载类可以部分(无分辨率)或完全(有分辨率)完成。 在编写loadClass
版本时,可能需要调用resolveClass
,具体取决于loadClass
的resolve
参数的值。
方法findLoadedClass
findLoadedClass
用作缓存:当要求loadClass
加载一个类时,它可以调用此方法以查看该类加载器是否已加载该类,从而省去了重新加载已加载的类的麻烦。 应该首先调用此方法。
放在一起
让我们看看所有这些方法如何组合在一起。
我们的loadClass
示例实现执行以下步骤。 (我们不会在此处指定将使用哪种特殊技术来获取类文件,它可能是从网络加载的,也可能是从存档中提取的,或者是在运行时进行编译的。不管是什么,这都是特殊的魔术获取我们的原始类文件字节。)
- 调用
findLoadedClass
来查看我们是否已经加载了该类。 - 如果尚未加载该类,则可以进行特殊处理以获取原始字节。
- 如果我们有原始字节,请调用
defineClass
将其转换为Class
对象。 - 如果没有原始字节,则调用
findSystemClass
以查看是否可以从本地文件系统获取该类。 - 如果
resolve
参数为true
,则调用resolveClass
来解析Class
对象。 - 如果我们仍然没有一个类,则抛出
ClassNotFoundException
。 - 否则,将类返回给调用者。
盘点
既然您已经掌握了ClassLoader的使用知识,那么现在就可以开始学习。 在下一节中,我们将实现CCL。
CompilingClassLoader
CCL透露
我们的ClassLoader CCL的工作是确保我们的代码已编译并且是最新的。
这是它的工作方式的描述:
- 当请求一个类时,请查看它是否存在于磁盘上,当前目录中或适当的子目录中。
- 如果该类不可用,但源可用,则调用Java编译器以生成类文件。
- 如果类文件确实存在,请检查它是否早于其源代码。 如果它比源文件旧,请调用Java编译器以重新生成类文件。
- 如果编译失败,或者由于任何其他原因无法从现有源生成类文件,则抛出
ClassNotFoundException
。 - 如果我们仍然没有该类,则可能在其他库中,因此请调用
findSystemClass
来查看是否可以使用。 - 如果我们仍然没有该类,则抛出
ClassNotFoundException
。 - 否则,返回课程。
Java编译的工作方式
在我们进行深入讨论之前,我们应该备份一点并谈论Java编译。 通常,Java编译器不仅会编译您要求的类。 如果您要求它编译的类需要这些类,它还会编译其他类。
CCL将逐一编译需要编译的应用程序中的每个类。 但是,一般来讲,在编译器编译了第一类之后,CCL会发现实际上所有需要编译的其他类都已被编译。 为什么? Java编译器采用的规则类似于我们正在使用的规则:如果某个类不存在或相对于其源而言已过时,则需要对其进行编译。 从本质上讲,Java编译器比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来调用指定类的main方法,并将参数传递给它。 有关更多详细信息,请参见源代码 。
运行示例
源代码中包含一组小类,用于说明事物的工作方式。 主程序是一个名为Foo
的类,该类创建Bar
类的实例。 Class Bar
创建另一个名为Baz
类的实例,该实例位于称为baz
的包中,以说明CCL与子包中的代码一起使用。 Bar
还通过名称加载了一个类,即class Boo
,以说明此功能也适用于CCL。
每个类都宣布已加载并运行它。 使用源代码并立即尝试。 编译CCLRun和CompilingClassLoader。 确保您不编译其他类( Foo
, Bar
, Baz
和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
。 在Bar
尝试按名称加载它之前,不会调用Boo
,这时我们的CCL必须再次调用编译器进行编译。
Java 2中的ClassLoader更改
总览
Java版本1.2和更高版本中对ClassLoader工具进行了改进。 为旧系统编写的任何代码都可以使用,但是新系统可以使您的生活更轻松。
新模型是委托模型。 ClassLoader可以将对类的请求委派给其父级。 默认实现在尝试加载类本身之前先调用父级,但是可以更改此策略。 所有ClassLoader的根都是系统ClassLoader,它以默认方式(即从本地文件系统)加载类。
loadClass
默认实现
自定义编写的loadClass
方法通常会尝试几种方法来加载请求的类,并且,如果您编写了大量的ClassLoader,您将发现自己在同一个相当复杂的方法上反复编写变体。
Java 1.2中loadClass
的默认实现体现了查找类的最常用方法,并允许您通过覆盖新的findClass
方法(在适当的时间调用loadClass
对其进行自定义。
这种方法的优点是您可能不必重写loadClass
。 您只需要重写findClass
,这会减少工作量。
新方法: findClass
这个新方法由loadClass
的默认实现调用。 findClass
的目的是包含ClassLoader的所有专用代码,而不必重复其他代码(例如,当您的专用方法失败时调用系统ClassLoader)。
新方法: getSystemClassLoader
无论您重写findClass
还是loadClass
, getSystemClassLoader
都可以以实际ClassLoader
对象的形式直接访问系统ClassLoader(而不是通过findSystemClass
调用隐式访问它)。
新方法: getParent
这个新方法允许ClassLoader到达其父ClassLoader,以便将类请求委托给它。 当自定义ClassLoader无法使用您的专用方法查找类时,可以使用此方法。
ClassLoader的父级定义为包含创建该ClassLoader的代码的对象的ClassLoader。
源代码
CompilingClassLoader.java
这是CompilingClassLoader.java的源代码
// $Id$
import java.io.*;
/*
A CompilingClassLoader compiles your Java source on-the-fly. It
checks for nonexistent .class files, or .class files that are older
than their corresponding source code.
*/
public class CompilingClassLoader extends ClassLoader
{
// Given a filename, read the entirety of that file from disk
// and return it as a byte array.
private byte[] getBytes( String filename ) throws IOException {
// Find out the length of the file
File file = new File( filename );
long len = file.length();
// Create an array that's just the right size for the file's
// contents
byte raw[] = new byte[(int)len];
// Open the file
FileInputStream fin = new FileInputStream( file );
// Read all of it into the array; if we don't get all,
// then it's an error.
int r = fin.read( raw );
if (r != len)
throw new IOException( "Can't read all, "+r+" != "+len );
// Don't forget to close the file!
fin.close();
// And finally return the file contents as an array
return raw;
}
// Spawn a process to compile the java source code file
// specified in the 'javaFile' parameter. Return a true if
// the compilation worked, false otherwise.
private boolean compile( String javaFile ) throws IOException {
// Let the user know what's going on
System.out.println( "CCL: Compiling "+javaFile+"..." );
// Start up the compiler
Process p = Runtime.getRuntime().exec( "javac "+javaFile );
// Wait for it to finish running
try {
p.waitFor();
} catch( InterruptedException ie ) { System.out.println( ie ); }
// Check the return code, in case of a compilation error
int ret = p.exitValue();
// Tell whether the compilation worked
return ret==0;
}
// The heart of the ClassLoader -- automatically compile
// source as necessary when looking for class files
public Class loadClass( String name, boolean resolve )
throws ClassNotFoundException {
// Our goal is to get a Class object
Class clas = null;
// First, see if we've already dealt with this one
clas = findLoadedClass( name );
//System.out.println( "findLoadedClass: "+clas );
// Create a pathname from the class name
// E.g. java.lang.Object => java/lang/Object
String fileStub = name.replace( '.', '/' );
// Build objects pointing to the source code (.java) and object
// code (.class)
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() );
// First, see if we want to try compiling. We do if (a) there
// is source code, and either (b0) there is no object code,
// or (b1) there is object code, but it's older than the source
if (javaFile.exists() &&
(!classFile.exists() ||
javaFile.lastModified() > classFile.lastModified())) {
try {
// Try to compile it. If this doesn't work, then
// we must declare failure. (It's not good enough to use
// and already-existing, but out-of-date, classfile)
if (!compile( javaFilename ) || !classFile.exists()) {
throw new ClassNotFoundException( "Compile failed: "+javaFilename );
}
} catch( IOException ie ) {
// Another place where we might come to if we fail
// to compile
throw new ClassNotFoundException( ie.toString() );
}
}
// Let's try to load up the raw bytes, assuming they were
// properly compiled, or didn't need to be compiled
try {
// read the bytes
byte raw[] = getBytes( classFilename );
// try to turn them into a class
clas = defineClass( name, raw, 0, raw.length );
} catch( IOException ie ) {
// This is not a failure! If we reach here, it might
// mean that we are dealing with a class in a library,
// such as java.lang.Object
}
//System.out.println( "defineClass: "+clas );
// Maybe the class is in a library -- try loading
// the normal way
if (clas==null) {
clas = findSystemClass( name );
}
//System.out.println( "findSystemClass: "+clas );
// Resolve the class, if any, but only if the "resolve"
// flag is set to true
if (resolve && clas != null)
resolveClass( clas );
// If we still don't have a class, it's an error
if (clas == null)
throw new ClassNotFoundException( name );
// Otherwise, return the class
return clas;
}
}
CCRun.java
这是CCRun.java的源代码
// $Id$
import java.lang.reflect.*;
/*
CCLRun executes a Java program by loading it through a
CompilingClassLoader.
*/
public class CCLRun
{
static public void main( String args[] ) throws Exception {
// The first argument is the Java program (class) the user
// wants to run
String progClass = args[0];
// And the arguments to that program are just
// arguments 1..n, so separate those out into
// their own array
String progArgs[] = new String[args.length-1];
System.arraycopy( args, 1, progArgs, 0, progArgs.length );
// Create a CompilingClassLoader
CompilingClassLoader ccl = new CompilingClassLoader();
// Load the main class through our CCL
Class clas = ccl.loadClass( progClass );
// Use reflection to call its main() method, and to
// pass the arguments in.
// Get a class representing the type of the main method's argument
Class mainArgType[] = { (new String[0]).getClass() };
// Find the standard main method in the class
Method main = clas.getMethod( "main", mainArgType );
// Create a list containing the arguments -- in this case,
// an array of strings
Object argsArray[] = { progArgs };
// Call the method
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] );
new Bar( args[0], args[1] );
}
}
Bar.java
这是Bar.java的源代码
// $Id$
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的源代码
// $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!" );
}
}
Wrapping up
Wrapping up
正如您在本简短教程中所看到的那样,了解如何创建自定义ClassLoader确实可以帮助您充分了解JVM。 可以从任何来源加载类文件,甚至可以即时生成它们的能力,可以扩展JVM的范围,并允许您做一些真正有趣且功能强大的事情。
其他ClassLoader想法
正如我在本教程前面提到的那样,自定义ClassLoader对于诸如启用Java的浏览器和appletviewer之类的程序至关重要。 以下是一些有趣的ClassLoader的其他想法:
- 安全。 您的ClassLoader可以在将类移交给JVM之前对其进行检查,以查看它们是否具有正确的数字签名。 您还可以通过检查源代码并拒绝尝试在沙箱之外执行操作的类,来创建一种不允许某些方法调用的“沙箱”。
- 加密。 可以创建一个可以动态解密的ClassLoader,以便具有反编译器的人无法读取磁盘上的类文件。 用户必须提供密码才能运行该程序,并且该密码用于解密代码。
- 存档。 是否要以特殊格式或特殊压缩方式分发代码? 您的ClassLoader可以从任何想要的源中提取原始类文件字节。
- 自解压程序。 可以将整个Java应用程序编译为包含压缩和/或加密的类文件数据以及集成的ClassLoader的单个可执行类文件; 程序运行时,它将自身完全解压缩到内存中 -无需先安装。
- 动态生成。 他们是这里的极限。 您可以生成引用尚未生成的其他类的类-动态创建整个类并将它们带入JVM,而不会错过任何机会。
翻译自: https://www.ibm.com/developerworks/java/tutorials/j-classloader/j-classloader.html