Java ClassLoader

时间:2020-02-23 14:36:26  来源:igfitidea点击:

Java ClassLoader是项目开发中至关重要但很少使用的组件之一。
我从未在任何项目中扩展ClassLoader。
但是,拥有自己的可以自定义Java类加载的ClassLoader的想法令人兴奋。

本文将概述Java ClassLoader,然后继续使用Java创建自定义类加载器。

什么是Java ClassLoader?

我们知道Java程序在Java虚拟机(JVM)上运行。
当我们编译Java类时,JVM将创建字节码,该字节码与平台和机器无关。
字节码存储在.class文件中。
当我们尝试使用一个类时,ClassLoader将其加载到内存中。

内置的ClassLoader类型

Java内置了三种类型的内置ClassLoader。

  • Bootstrap类加载器–加载JDK内部类。
    它加载rt.jar和其他核心类,例如java.lang。
    *包类。

  • 扩展类加载器–它从JDK扩展目录(通常为$JAVA_HOME/lib/ext目录)中加载类。

  • 系统类加载器–该类加载器从当前类路径加载类。
    我们可以在使用-cp或者-classpath命令行选项调用程序时设置classpath。

ClassLoader层次结构

ClassLoader在将类加载到内存中是分层的。
每当提出加载类的请求时,它都会将其委托给父类加载器。
这就是在运行时环境中保持唯一性的方式。
如果父类加载器找不到该类,则该类加载器本身会尝试加载该类。

通过执行以下Java程序来了解这一点。

package com.theitroad.classloader;

public class ClassLoaderTest {

  public static void main(String[] args) {

      System.out.println("class loader for HashMap: "
              + java.util.HashMap.class.getClassLoader());
      System.out.println("class loader for DNSNameService: "
              + sun.net.spi.nameservice.dns.DNSNameService.class
                      .getClassLoader());
      System.out.println("class loader for this class: "
              + ClassLoaderTest.class.getClassLoader());

      System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());

  }

}

输出:

class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Java ClassLoader如何工作?

让我们从上面的程序输出中了解类加载器的工作方式。

  • java.util.HashMap ClassLoader作为null出现,这反映了Bootstrap ClassLoader。
    DNSNameService类的ClassLoader是ExtClassLoader。
    由于类本身位于CLASSPATH中,因此System ClassLoader会加载它。

  • 当我们尝试加载HashMap时,我们的System ClassLoader将其委托给Extension ClassLoader。
    扩展类加载器将其委托给Bootstrap ClassLoader。
    引导类加载器会找到HashMap类并将其加载到JVM内存中。

  • DNSNameService类遵循相同的过程。
    但是,由于Bootstrap ClassLoader位于$JAVA_HOME/lib/ext/dnsns.jar中,因此无法找到它。
    因此,它由扩展类加载器加载。

  • Blob类包含在MySql JDBC连接器jar(mysql-connector-java-5.0.7-bin.jar)中,该jar存在于项目的构建路径中。
    系统类加载器也正在加载它。

  • 子类加载器加载的类可以查看其父类加载器加载的类。
    因此,由System Classloader加载的类可以查看由Extensions和Bootstrap Classloader加载的类。

  • 如果有同级的类加载器,则它们将无法访问彼此加载的类。

为什么要用Java编写自定义ClassLoader?

Java默认的ClassLoader可以从本地文件系统加载类,这在大多数情况下已经足够了。
但是,如果在加载类时希望在运行时或者从FTP服务器或者通过第三方Web服务获得类,则必须扩展现有的类加载器。
例如,AppletViewers从远程Web服务器加载类。

Java ClassLoader方法

  • 当JVM请求一个类时,它通过传递类的完全分类名称来调用ClassLoader的loadClass()函数。

  • loadClass()函数调用findLoadedClass()方法来检查该类是否已经被加载。
    必须避免多次加载同一程序。

  • 如果尚未加载该类,则它将把请求委派给父ClassLoader以加载该类。

  • 如果父类ClassLoader找不到该类,则它将调用findClass()方法在文件系统中查找这些类。

Java自定义ClassLoader示例

我们将通过扩展ClassLoader类并覆盖loadClass(String name)方法来创建自己的ClassLoader。

如果类名将从com.theitroad开始,那么我们将使用自定义类加载器加载它,否则我们将调用父类ClassLoader的loadClass()方法加载该类。

1. CCLoader.java

这是我们的自定义类加载器,具有以下方法。

  • private byte [] loadClassFileData(String name):该方法将从文件系统读取类文件到字节数组。

  • private Class <?> getClass(String name):此方法将调用loadClassFileData()函数,并通过调用父defineClass()方法,将生成Class并返回它。

  • public Class <?> loadClass(String name):此方法负责加载类。
    如果类名以com.theitroad(我们的示例类)开头,则它将使用getClass()方法加载它,否则它将调用父loadClass()函数来加载它。

  • public CCLoader(ClassLoader parent):这是构造函数,负责设置父ClassLoader。

import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * Our Custom ClassLoader to load the classes. Any class in the com.theitroad
 * package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
 *
 */
public class CCLoader extends ClassLoader {
 
  /**
   * This constructor is used to set the parent ClassLoader
   */
  public CCLoader(ClassLoader parent) {
      super(parent);
  }
 
  /**
   * Loads the class from the file system. The class file should be located in
   * the file system. The name should be relative to get the file location
   *
   * @param name
   *            Fully Classified name of the class, for example, com.theitroad.Foo
   */
  private Class getClass(String name) throws ClassNotFoundException {
      String file = name.replace('.', File.separatorChar) + ".class";
      byte[] b = null;
      try {
          //This loads the byte code data from the file
          b = loadClassFileData(file);
          //defineClass is inherited from the ClassLoader class
          //that converts byte array into a Class. defineClass is Final
          //so we cannot override it
          Class c = defineClass(name, b, 0, b.length);
          resolveClass(c);
          return c;
      } catch (IOException e) {
          e.printStackTrace();
          return null;
      }
  }
 
  /**
   * Every request for a class passes through this method. If the class is in
   * com.theitroad package, we will use this classloader or else delegate the
   * request to parent classloader.
   *
   *
   * @param name
   *            Full class name
   */
  @Override
  public Class loadClass(String name) throws ClassNotFoundException {
      System.out.println("Loading Class '" + name + "'");
      if (name.startsWith("com.theitroad")) {
          System.out.println("Loading Class using CCLoader");
          return getClass(name);
      }
      return super.loadClass(name);
  }
 
  /**
   * Reads the file (.class) into a byte array. The file should be
   * accessible as a resource and make sure that it's not in Classpath to avoid
   * any confusion.
   *
   * @param name
   *            Filename
   * @return Byte array read from the file
   * @throws IOException
   *             if an exception comes in reading the file
   */
  private byte[] loadClassFileData(String name) throws IOException {
      InputStream stream = getClass().getClassLoader().getResourceAsStream(
              name);
      int size = stream.available();
      byte buff[] = new byte[size];
      DataInputStream in = new DataInputStream(stream);
      in.readFully(buff);
      in.close();
      return buff;
  }
}

2. CCRun.java

这是带有主要功能的测试类。
我们正在创建ClassLoader的实例,并使用其loadClass()方法加载示例类。

加载该类之后,我们将使用Java Reflection API来调用其方法。

import java.lang.reflect.Method;
 
public class CCRun {
 
  public static 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);

      CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
      Class clas = ccl.loadClass(progClass);
      Class mainArgType[] = { (new String[0]).getClass() };
      Method main = clas.getMethod("main", mainArgType);
      Object argsArray[] = { progArgs };
      main.invoke(null, argsArray);

      //Below method is used to check that the Foo is getting loaded
      //by our custom class loader i.e CCLoader
      Method printCL = clas.getMethod("printCL", null);
      printCL.invoke(null, new Object[0]);
  }
 
}

3. Foo.java和Bar.java

这些是由我们的自定义类加载器加载的测试类。
它们具有一个printCL()方法,该方法将被调用以打印ClassLoader信息。

Foo类将由我们的自定义类加载器加载。
Foo使用Bar类,因此Bar类也将由我们的自定义类加载器加载。

package com.theitroad.cl;
 
public class Foo {
  static public void main(String args[]) throws Exception {
      System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
      Bar bar = new Bar(args[0], args[1]);
      bar.printCL();
  }
 
  public static void printCL() {
      System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
  }
}
package com.theitroad.cl;
 
public class Bar {
 
  public Bar(String a, String b) {
      System.out.println("Bar Constructor >>> " + a + " " + b);
  }
 
  public void printCL() {
      System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
  }
}

4. Java自定义ClassLoader执行步骤

首先,我们将通过命令行编译所有类。
之后,我们将通过传递三个参数来运行CCRun类。
第一个参数是Foo类的完全分类名称,它将由我们的类加载器加载。
其他两个参数将传递给Foo类的主函数和Bar构造函数。
执行步骤和输出将如下所示。

$javac -cp . com/theitroad/cl/Foo.java
$javac -cp . com/theitroad/cl/Bar.java
$javac CCLoader.java
$javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$java CCRun com.theitroad.cl.Foo 1212 1313
Loading Class 'com.theitroad.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.theitroad.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

如果您查看输出,则尝试加载com.theitroad.cl.Foo类。
由于它扩展了java.lang.Object类,因此它尝试首先加载Object类。

因此,该请求即将到达CCLoader loadClass方法,该方法将其委派给父类。
因此,父类加载器正在加载Object,String和其他Java类。

我们的ClassLoader仅从文件系统加载Foo和Bar类。
从printCL()函数的输出中可以清楚地看到。

我们可以更改loadClassFileData()功能以从FTP服务器读取字节数组,或者通过调用任何第三方服务来即时获取类字节数组。

我希望这篇文章对理解Java ClassLoader的工作以及如何扩展它的作用,不仅仅是从文件系统中获取更多信息,将很有用。

将自定义ClassLoader设置为默认ClassLoader

通过使用Java选项,可以在JVM启动时将自定义类装入器作为默认装入器。

例如,在提供java classloader选项之后,我将再次运行ClassLoaderTest程序。

$javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/theitroad/classloader/ClassLoaderTest.java
$java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.theitroad.classloader.ClassLoaderTest
Loading Class 'com.theitroad.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$

CCLoader正在加载ClassLoaderTest类,因为它位于com.theitroad包中。