11.4 应用类加载知识

若想应用类加载知识,一定要完全理解 java.lang.ClassLoader 类。

这是个抽象类,功能完善,没有抽象方法。之所以使用 abstract 修饰符是为了强调,若想使用,必须创建子类。

除了前面提到的 defineClass() 方法,还可以使用公开的 loadClass() 方法加载类。子类 URLClassLoader 一般会使用这个方法,从 URL 或文件路径中加载类。

我们可以使用 URLClassLoader 对象从本地硬盘中加载类,如下所示:

  1. String current = new File( "." ).getCanonicalPath();
  2. try (URLClassLoader ulr =
  3. new URLClassLoader(new URL[] {new URL("file://"+ current + "/")})) {
  4. Class<?> clz = ulr.loadClass("com.example.DFACaller");
  5. System.out.println(clz.getName());
  6. }

loadClass() 方法的参数是类文件的二进制名。注意,类文件必须存放在文件系统中的预定位置,URLClassLoader 对象才能找到指定的类。例如,要在相对于工作目录的 com/example/DFACaller.class 文件中才能找到 com.example.DFACaller 类。

Class 类还提供了 Class.forName() 方法,这是个静态方法,能从类路径中加载还未被引用的类。

这个方法的参数是类的完全限定名称。例如:

  1. Class<?> jdbcClz = Class.forName("oracle.jdbc.driver.OracleDriver");

如果找不到指定的类,这个方法会抛出 ClassNotFoundException 异常。如这个示例所示,forName() 方法在旧版 JDBC 中经常使用,目的是确保加载了正确的驱动器。如果使用 import,会把依赖导入使用驱动器的类,forName() 方法则能避免这个问题。

JDBC 4.0 之后,不再需要这个初始化步骤了。

Class.forName() 方法还有一种形式,接受三个参数,有时会和另一个类加载程序一起使用:

  1. Class.forName(String name, boolean inited, Classloader classloader);

ClassLoader 类有很多子类,分别处理各种特殊的类加载过程。这些子类组成了类加载程序层次结构。

类加载程序层次结构

JVM 有多个类加载程序,而且形成一个层次结构,每个类加载程序(除了第一层“原始”类加载程序)都可以把工作交给父级类加载程序完成。

按照约定,类加载程序会要求父级类加载程序解析并加载类,只有父级类加载程序无法完成时才会自己动手。一些常用的类加载程序如图 11-2 所示。

{%}

图 11-2:类加载程序的层次结构

1. 原始类加载程序

这是所有 JVM 进程中出现的第一个类加载程序,只用来加载核心系统类(在 rt.jar 中)。这个类加载程序不做验证,安全性靠引导类路径(boot classpath)保障。

引导类路径可以使用 -Xbootclasspath 选项调整,详情参见第 13 章。

2. 扩展类加载程序

这个类加载程序只用于加载 JDK 扩展——扩展一般保存在 JVM 安装目录中的 lib/ext 目录里。

扩展类加载程序的父级类加载程序是原始类加载程序。这个类加载程序使用不广泛,不过有时会用来实现调试器及相关的开发工具。

Nashorn JavaScript 环境(参见第 12 章)也使用这个类加载程序加载。

3. 应用类加载程序

这个类加载程序以前叫系统类加载程序,这个名称可不好,因为它并不加载系统(这是原始类加载程序的工作)。应用类加载程序的作用是从类路径中加载应用代码。这个类加载程序最常见,其父级类加载程序是扩展类加载程序。

应用类加载程序使用非常广泛,但很多高级 Java 框架需要的功能,这些主要的类加载程序没有提供,因此要扩展标准的类加载程序。“自定义类加载”的基础是,实现 ClassLoader 的新子类。

4. 自定义类加载程序

加载类时,迟早要把数据变成代码。前面说过,defineClass() 方法(其实是一组相关的方法)的作用是把 byte[] 数组转换成类对象。

这个方法通常在子类中调用。例如,下面这个简单的自定义类加载程序从硬盘中读取文件,创建类对象:

  1. public static class DiskLoader extends ClassLoader {
  2. public DiskLoader() {
  3. super(DiskLoader.class.getClassLoader());
  4. }
  5. public Class<?> loadFromDisk(String clzName) throws IOException {
  6. byte[] b = Files.readAllBytes(Paths.get(clzName));
  7. return defineClass(null, b, 0, b.length);
  8. }
  9. }

注意,上述示例和 URLClassLoader 类的示例不同,不用把类文件存放在硬盘中“正确的”位置。

每个自定义类加载程序都要有父级类加载程序。在这个示例中,我们把加载 DiskLoader 类的类加载程序(通常都是应用类加载程序)指定为它的父级类加载程序。

自定义类加载这个技术在 Java EE 和高级的 SE 环境中十分常见,目的是为 Java 平台提供非常复杂的功能。本章后面会举个自定义类加载的例子。

动态类加载有个缺点:使用动态加载的类对象时,往往对这个类知之甚少或一无所知。为了有效使用这个类,我们通常要使用一套动态编程技术——反射。