Java 类加载器
Java ClassLoader是项目开发中至关重要但很少使用的组件之一。我从来没有在我的任何项目中扩展过ClassLoader。但是,拥有可以自定义Java类加载的自己的ClassLoader的想法令人兴奋。本文将概述Java ClassLoader,然后继续在Java中创建一个自定义类加载器。
Java ClassLoader是什么?
我们知道,Java程序运行在Java虚拟机(JVM)上。当我们编译一个Java类时,JVM会创建字节码,这是与平台和机器无关的。字节码存储在一个.class文件中。当我们尝试使用一个类时,类加载器将其加载到内存中。
内置的ClassLoader类型
Java 中有三种类型的内置 ClassLoader。
-
- 启动类加载器 – 它加载JDK内部的类。它加载rt.jar和其他核心类,例如java.lang.*包中的类。
-
- 扩展类加载器 – 它从JDK扩展目录加载类,通常是$JAVA_HOME/lib/ext目录。
- 系统类加载器 – 此类加载器从当前类路径加载类。我们可以在调用程序时使用-cp或-classpath命令行选项来设置类路径。
类加载器层次结构 qì
类加载器在将类加载到内存中时是具有层次性的。每当有一个加载类的请求时,类加载器会将其委托给父类加载器。这就是运行环境中如何保持唯一性的方式。如果父类加载器找不到该类,那么类加载器本身会尝试加载该类。通过执行下面的Java程序来理解这个过程。
package com.Olivia.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类加载器的工作原理是怎样的?
让我们从上面的程序输出中理解类加载器的工作原理。
- The java.util.HashMap ClassLoader is coming as null, which reflects Bootstrap ClassLoader. The DNSNameService class ClassLoader is ExtClassLoader. Since the class itself is in CLASSPATH, System ClassLoader loads it.
- When we are trying to load HashMap, our System ClassLoader delegates it to the Extension ClassLoader. The extension class loader delegates it to the Bootstrap ClassLoader. The bootstrap class loader finds the HashMap class and loads it into the JVM memory.
- The same process is followed for the DNSNameService class. But, the Bootstrap ClassLoader is not able to locate it since it’s in $JAVA_HOME/lib/ext/dnsns.jar. Hence, it gets loaded by Extensions Classloader.
- The Blob class is included in the MySql JDBC Connector jar (mysql-connector-java-5.0.7-bin.jar), which is present in the build path of the project. It’s also getting loaded by the System Classloader.
- The classes loaded by a child class loader have visibility into classes loaded by its parent class loaders. So classes loaded by System Classloader have visibility into classes loaded by Extensions and Bootstrap Classloader.
- If there are sibling class loaders then they can’t access classes loaded by each other.
为什么要在Java中编写自定义的类加载器?
Java默认的类加载器可以从本地文件系统加载类,这在大多数情况下已经足够。但是,如果你希望在运行时或者从FTP服务器或者通过第三方Web服务在加载类的时候获取到类,那么你就必须扩展现有的类加载器。例如,AppletViewers会从远程Web服务器加载类。
Java类加载器的方法。
- When JVM requests for a class, it invokes loadClass() function of the ClassLoader by passing the fully classified name of the Class.
- The loadClass() function calls the findLoadedClass() method to check that the class has been already loaded or not. It’s required to avoid loading the same class multiple times.
- If the Class is not already loaded, then it will delegate the request to parent ClassLoader to load the class.
- If the parent ClassLoader doesn’t find the class then it will invoke findClass() method to look for the classes in the file system.
Java自定义类加载器示例
CCLoader.java 的中文同义句如下:
1. CCLoader.java
这是我们的自定义类加载器,具有以下方法。
-
- 私有字节数组的加载ClassFileData(String name)方法将从文件系统中读取类文件,并转换为字节数组。
-
- 获取Class(String name)方法将调用loadClassFileData()函数,并通过调用父定义defineClass()方法来生成并返回Class。
-
- 公共Class(String name)方法负责加载类。如果类名以com.Olivia(我们的示例类)开头,则使用getClass()方法加载它;否则,将调用父加载loadClass()函数来加载它。
- 公共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.Olivia
* 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.Olivia.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.Olivia 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.Olivia")) {
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 – CCRun.java文件
这是我们的测试类,其中包含主函数。我们正在创建我们的ClassLoader的一个实例,并使用其loadClass()方法加载样本类。在加载类之后,我们使用Java的反射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.Olivia.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.Olivia.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自定义类加载器执行步骤
首先,我们将通过命令行编译所有的类。然后,我们将通过传递三个参数来运行CCRun类。第一个参数是完全分类的Foo类名称,将被我们的类加载器加载。其他两个参数将传递给Foo类的main函数和Bar构造函数。执行步骤和输出如下。
$ javac -cp . com/scdev/cl/Foo.java
$ javac -cp . com/scdev/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.Olivia.cl.Foo 1212 1313
Loading Class 'com.Olivia.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.Olivia.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$
如果您查看输出,它正在尝试加载com.Olivia.cl.Foo类。由于它继承了java.lang.Object类,所以它首先尝试加载Object类。因此,请求到达了CCLoader loadClass方法,该方法将其委派给父类。因此,父类加载器正在加载Object、String和其他Java类。我们的ClassLoader仅从文件系统加载Foo和Bar类。从printCL()函数的输出中可以清楚地看出。我们可以改变loadClassFileData()功能,从FTP服务器读取字节数组,或者通过调用任何第三方服务来动态获取类字节数组。我希望本文对理解Java类加载器的工作原理以及我们如何扩展它以完成更多任务有所帮助。
将自定义的ClassLoader作为默认的ClassLoader。
我们可以使用Java选项让我们的自定义类加载器成为JVM启动时的默认类加载器。例如,在提供Java类加载器选项后,我将再次运行ClassLoaderTest程序。
$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/scdev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.Olivia.classloader.ClassLoaderTest
Loading Class 'com.Olivia.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.Olivia包中。
你可以从我们的GitHub存储库中下载ClassLoader示例代码。