9.3 创建类加载器

大部分Java程序在运行时并不需要使用自己的类加载器,依靠Java平台提供的3个类加载器就足够了。在绝大多数时候,也只有系统类加载器发挥作用。如果程序对加载类的方式有特殊的要求,就要创建自己的类加载器。这样的实际应用场景有很多,主要可以分成两类:一类是对Java类的字节代码进行特殊的查找和处理,如Java类的字节代码存放在磁盘上特定的位置或远程服务器上,或者字节代码的数据经过了加密处理;另一类是利用类加载器产生的隔离特性来满足特殊的需求。

创建自定义类加载器只需继承ClassLoader类即可,可以选择覆写其中的某些方法来实现自定义的类加载逻辑。ClassLoader类中的defineClass方法用来从字节代码中定义出表示Java类的Class类的对象。由于定义Java类涉及虚拟机的核心功能,从安全的角度出发,defineClass方法被声明为final,不能由ClassLoader类的子类来覆写。一般来说,defineClass方法是由原生代码实现的。在ClassLoader类中包含了不少声明为protected的方法,这些方法是创建自定义类加载器的基础。自定义类加载器通过覆写这些方法来实现特殊的功能。

第一个方法是loadClass。这个方法与ClassLoader类公开的loadClass方法同名,但是多一个表示是否对加载的类进行链接操作的参数。在这个声明为protected的loadClass方法中封装了默认的双亲类加载器优先的代理模式的实现。默认的实现流程是进行下面的查找过程:先通过findLoadedClass方法来检查该Java类是否已经被加载过,如果已经被加载过了,就直接返回之前加载过的Class类的对象;接着通过getParent方法得到双亲类加载器对象,再调用双亲类加载器对象的loadClass方法,这一步是代理模式生效的地方,如果getParent方法返回为null,则使用启动类加载器来进行加载;最后调用findClass方法由当前类加载器对象进行查找。这3步依次进行,如果在其中某一步的查找过程中找到了Java类的定义,就返回定义的Class类的对象作为loadClass方法的返回值。如果尝试所有的步骤后仍然找不到Java类的定义,loadClass方法就会抛出java.lang.ClassNotFoundException异常。如果调用loadClass方法的第二个参数值为true,即需要对找到的类进行链接操作,则loadClass方法会调用resolveClass方法进行链接。

第二个方法是findLoadedClass。在类加载的过程中,虚拟机会记录下已经加载的Java类的初始类加载器。在findLoadedClass方法的实现中,会查找已经加载的Java类,比较这些Java类的初始类加载器对象和要加载的Java类的名称。如果某个已经加载的Java类的初始类加载器是当前类加载器对象,同时类的名称也与要加载的类的名称相同,就把该Java类对应的Class类的对象作为结果返回。

第三个方法是findClass。默认情况下,当通过代理模式无法使用双亲类加载器对象成功加载Java类时,findClass方法被调用。这个方法主要用来封装当前类加载器对象自己的类加载逻辑。在一般的自定义类加载器中只需要覆写此方法即可。只有在需要改变默认的双亲优先代理模式的类加载器中才需要覆写loadClass方法。ClassLoader类中的findClass方法只是简单抛出ClassNotFoundException异常。

最后一个方法是resolveClass。这个方法的作用是链接一个定义好的Class类的对象。链接的具体过程将在第10章进行介绍。

下面通过几个具体的示例来说明如何创建自定义类加载器。第一个示例是从磁盘上的特定目录加载Java类的字节代码的类加载器。代码清单9-5中给出了完整的实现代码。在创建FileSystemClassLoader类的对象时,需要指定一个路径作为Java类字节代码所在的目录。在findClass方法的实现中,先把要加载的类名转换成对应的class文件的路径,再读取class文件以得到字节代码的内容,最后通过defineClass方法来定义Class类的对象。

代码清单9-5 从文件系统加载字节代码的类加载器


public class FileSystemClassLoader extends ClassLoader{

private Path path;

public FileSystemClassLoader(Path path){

this.path=path;

}

protected Class<?>findClass(String name)throws ClassNotFoundException{

try{

byte[]classData=getClassData(name);

return defineClass(name, classData,0,classData.length);

}catch(IOException e){

throw new ClassNotFoundException();

}

}

private byte[]getClassData(String className)throws IOException{

Path classFilePath=classNameToPath(className);

return Files.readAllBytes(classFilePath);

}

private Path classNameToPath(String className){

return path.resolve(className.replace('.',File.separatorChar)+".class");

}

}


FileSystemClassLoader类只是简单地读取了字节代码的内容,实际上可以在读取字节代码之后,调用defineClass方法之前进行很多操作。例如,如果字节代码的内容是经过加密的,就需要在调用defineClass方法之前进行解密操作。另外,可以对字节代码应用第8章介绍的字节代码增强技术来处理。

除了通过磁盘文件或网络方式加载已有的字节代码之外,还可以在类加载器中即时生成所需的字节代码。使用第8章介绍的ASM工具可以很容易地实现这个功能。代码清单9-6中给出了一个动态生成字节代码的类加载器的实现。在GreetingClassLoader类的findClass方法中,使用ASM工具生成了类的字节代码。在生成的Java类的构造方法中,添加了对System.out方法的调用来输出提示信息。

代码清单9-6 动态生成字节代码的类加载器


public class GreetingClassLoader extends ClassLoader implements Opcodes{

private String message;

public GreetingClassLoader(String message){

this.message=message;

}

protected Class<?>findClass(String name)throws ClassNotFoundException{

byte[]classData=generateClassData(name);

return defineClass(name, classData,0,classData.length);

}

private byte[]generateClassData(String className){

className=className.replaceAll("\.","/");

ClassWriter writer=new ClassWriter(0);

writer.visit(V1_7,ACC_PUBLIC+ACC_SUPER, className, null,"java/lang/Object",null);

MethodVisitor mv=writer.visitMethod(Opcodes.ACC_PUBLIC,"<init>","()V",null, null);

mv.visitCode();

mv.visitVarInsn(ALOAD,0);

mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V");

mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");

mv.visitLdcInsn(message);

mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V");

mv.visitInsn(RETURN);

mv.visitMaxs(2,1);

mv.visitEnd();

writer.visitEnd();

return writer.toByteArray();

}

}


前面介绍的两个示例并没有覆写ClassLoader类中的loadClass方法中的逻辑,而是覆写了findClass方法。在这两个示例中,ClassLoader类默认使用的双亲优先的代理模式是有效的。如果希望改变这种默认的代理模式,那么可以覆写loadClass方法。在某些情况下,可能需要优先使用当前类加载器对象进行查找,再考虑使用双亲类加载器对象进行查找。代码清单9-7给出了这种当前类加载器对象优先的代理模式的实现。在ParentLastClassLoader类的loadClass方法中仍然先通过findLoadedClass方法来查找已经加载的Java类。这一步通常是必需的。接着调用findClass方法优先由当前类加载器对象进行查找。最后再代理给双亲类加载器对象来进行查找。

代码清单9-7 当前类加载器对象优先的类加载器


public class ParentLastClassLoader extends ClassLoader{

protected Class<?>loadClass(String name, boolean resolve)throws ClassNotFoundException{

Class<?>clazz=findLoadedClass(name);

if(clazz!=null){

return clazz;

}

clazz=findClass(name);

if(clazz!=null){

return clazz;

}

ClassLoader parent=getParent();

if(parent!=null){

return parent.loadClass(name);

}

else{

return super.loadClass(name, resolve);

}

}

}