18.2.2 创建并使用自定义的类加载器

18.2.3 创建并使用自定义的类加载器

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过扩展ClassLoader的子类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。查阅API文档中关于ClassLoader的方法不难发现, ClassLoader中包含了大量的protected方法,这些方法都可被子类重写。

ClassLoader类关键方法

ClassLoader类有如下两个关键方法。

方法 描述
loadClass(String name, boolean resolve) 该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的 Class对象。
findClass(String name) 根据指定名称来查找类

重写findClass方法自定义ClassLoader

如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法,而不是重写loadClass()方法

loadClass方法的执行步骤

loadClass()方法的执行步骤如下。

  1. findClass(String)来检查是否已经加载类,如果已经加载则直接返回.
  2. 在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
  3. 调用findClass(String)方法查找类。

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂

defineClass方法

ClassLoader里还有一个核心的defineClass方法

方法 描述
Class defineClass(String name, byte[] b, int off,int len) 将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等.

defineClass方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心该方法是final方法,所以程序员无须重写该方法。

ClassLoader普通方法

除此之外, ClassLoader里还包含如下一些普通方法。

方法 描述
protected Class<?> findSystemClass(String name) 从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass方法将原始字节转换成Class对象,以将该文件转换成类
static ClassLoader getSystemClassLoader() 返回系统类加载器。
ClassLoader getParent() 获取该类加载器的父类加载器。
protected void resolveClass(Class<?> c) 链接指定的类。类加载器可以使用此方法来链接类c。读者无须理会关于此方法的太多细节。
protected Class<?> findLoadedClass(String name) 如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。

程序示例 自定义ClassLoader

下面程序开发了一个自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import java.io.*;
import java.lang.reflect.*;

public class CompileClassLoader extends ClassLoader
{
// 读取一个文件的内容
private byte[] getBytes(String filename)
throws IOException
{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int)len];
try(
FileInputStream fin = new FileInputStream(file))
{
// 一次读取class文件的全部二进制数据
int r = fin.read(raw);
if(r != len)
throw new IOException("无法读取全部文件:"
+ r + " != " + len);
return raw;
}
}
// 定义编译指定Java文件的方法
private boolean compile(String javaFile)
throws IOException
{
System.out.println("CompileClassLoader:正在编译 "
+ javaFile + "...");
// 调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try
{
// 其他线程都等待这个线程完成
p.waitFor();
}
catch(InterruptedException ie)
{
System.out.println(ie);
}
// 获取javac线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
protected Class<?> findClass(String name)
throws ClassNotFoundException
{
Class clazz = null;
// 将包路径中的点(.)替换成斜线(/)。
String fileStub = name.replace("." , "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 当指定Java源文件存在,且class文件不存在、或者Java源文件
// 的修改时间比class文件修改时间更晚,重新编译
if(javaFile.exists() && (!classFile.exists()
|| javaFile.lastModified() > classFile.lastModified()))
{
try
{
// 如果编译失败,或者该Class文件不存在
if(!compile(javaFilename) || !classFile.exists())
{
throw new ClassNotFoundException(
"ClassNotFoundExcetpion:" + javaFilename);
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
// 如果class文件存在,系统负责将该文件转换成Class对象
if (classFile.exists())
{
try
{
// 将class文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = defineClass(name,raw,0,raw.length);
}
catch(IOException ie)
{
ie.printStackTrace();
}
}
// 如果clazz为null,表明加载失败,则抛出异常
if(clazz == null)
{
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
public static void main(String[] args) throws Exception
{
// 如果运行该程序时没有参数,即没有目标类
if (args.length < 1)
{
System.out.println("缺少目标类,请按如下格式运行Java源文件:");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数,
// 将这些参数复制到一个新数组中
String[] progArgs = new String[args.length-1];
System.arraycopy(args , 1 , progArgs
, 0 , progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main" , (new String[0]).getClass());
Object[] argsArray = {progArgs};
main.invoke(null,argsArray);
}
}

上面程序中的重写了findClass()方法,通过重写该方法就可以实现自定义的类加载机制。在本类的findClass()方法中先检查需要加载类的Class文件是否存在,如果不存在则先编译源文件,再调用ClassLoaderdefineClass()方法来加载这个Class文件,并生成相应的Class对象。
接下来可以随意提供一个简单的主类,该主类无须编译就可以使用上面的CompileClassLoader来运行它。

1
2
3
4
5
6
7
8
9
10
public class Hello
{
public static void main(String[] args)
{
for (String arg : args)
{
System.out.println("运行Hello的参数:" + arg);
}
}
}

无须编译该Hello.java,可以直接使用如下命令来运行该Hello.java程序

1
java CompileClassLoader Hello 这是命令行参数

运行结果如下:

1
2
3
G:\Desktop\codes\18\18.2>java CompileClassLoader Hello 这是命令行参数
CompileClassLoader:正在编译 Hello.java...
运行Hello的参数:这是命令行参数

本示例程序提供的类加载器功能比较简单,仅仅提供了在运行之前先编译Java源文件的功能。

自定义类加载器可以实现什么功能

实际上,使用自定义的类加载器,可以实现如下常见功能。

  • 执行代码前自动验证数字签名
  • 根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译*.class文件。
  • 根据用户需求来动态地加载类。
  • 根据应用需求把其他数据以字节码的形式加载到应用中。