Java虚拟机之类加载实战

本人在前篇《Java虚拟机之Class文件》中已经分析了Class文件结构,末尾小结提到了“自己动手实现远程执行功能”的实战,本篇就来进行该实战。

自定义ClassLoader

自定义ClassLoader都需要继承 Classloader类,这是一个抽象类。诸如findClass()在ClassLoader中是这样一个方法:

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

权限修饰符为protected,还默认只在方法体中抛出个异常,看来等待着实现呢。

再看看loadClass()的源码:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

这个类如果重写,会破坏双亲委派机制,通常只要重写findClass()就能满足类加载方面的需求。

defineClass()具体实现很复杂(一部分涉及native底层实现,需要单独开一篇来分析),而且用final修饰(不能重写),在自定义ClassLoader的时候直接调用即可。调用之前需要先获得要读取的Class文件字节数据,而这个在《Java虚拟机之Class文件》关联的项目中有实现,要说明的是这里只是通过实现了解原理,所以不考虑读取jar包等压缩文件中的Class,在那个项目中Dir_Entry类的readClass()具有这样的功能。稍作修改代码如下:

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
public byte[] readClass(String absDir, String className) {
byte[] buf = new byte[1024];
byte[] b = null;
File f;
if (absDir != null) {
f = new File(absDir, className);
if (!f.exists()) {
return null;
}
} else {
f = new File(className);
}
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(f));
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int bufread = 0;
while ((bufread = in.read(buf)) != -1) {
out.write(buf, 0, bufread);
}
b = out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return b;
}

这里用到的defineClass四参数方法:

1
2
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {}

只重写findClass()方法仍然保证了基本的双亲委派机制,这里放上完整代码:

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
package classloaderdemo;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
public class MyClassLoader extends ClassLoader {
String absDir;
MyClassLoader() {
super(MyClassLoader.class.getClassLoader());
}
MyClassLoader(String absDir) {
this.absDir = absDir;
}
public Class loadByte(byte[] classByte) {
return defineClass(null, classByte, 0, classByte.length);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = readClass(absDir, name);
return defineClass(name, data, 0, data.length);
}
public byte[] readClass(String absDir, String className) {
byte[] buf = new byte[1024];
byte[] b = null;
File f;
if (absDir != null) {
f = new File(absDir, className);
if (!f.exists()) {
return null;
}
} else {
f = new File(className);
}
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(f));
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int bufread = 0;
while ((bufread = in.read(buf)) != -1) {
out.write(buf, 0, bufread);
}
b = out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return b;
}
}

因为没有重写loadClass(),如果显示调用loadClass(),仍然能触发双亲委派机制;如果直接用loadByte()载入字节数据,则应该在构造函数中添加super(MyClassLoader.class.getClassLoader())

ClassModifier

ClassModifier负责更改Class文件字节数据。有了上一篇文章的基础,这里实现起来容易很多。首先通过遍历找到Class常量池中出现的CONSTANT_Utf8常量类型,判断这个常量类型转化成的字符串是否与给定的需要被替换的字符串相等,相等就将表示这段CONSTANT_Utf8的字节码转换为要替换成的字符串相应的字节码,不相等就遍历到下一个,直到跳出循环。

这里我沿用了上一篇项目中的类ClassReader,它的特点在于每次读取字节数据的时候,内部的游标变量cursor自动更新(为了方便调用此字段设为public)。一开始读取数据需要跳过表示咖啡北鼻和版本号的8个字节。所以先直接执行cr.readUint64();,cr是ClassReader实例。接下来的流程大致如下:

  1. 读取字节,获取常量池中的常量个数cpc;
  2. 遍历常量池常量,每次遍历并通过ClassReader计算出tag,通过tag获知是什么常量类型,判断是否为CONSTANT_Utf8_info,如果不是,跳过当前判断;
  3. 如果是,计算出CONSTANT_Utf8_info字符,判断该字符串是否等于要更改的字符串部分;
  4. 等于,则进行目标替换,创建一个新的数组表示替换成功的数组并返回;反之跳过当前判断;

字符串替换的源码如下:

1
2
3
4
5
6
7
8
public byte[] bytesReplace(byte[] bytes01, int offset, int len, byte[]bytes02) {
byte[] newBytes = new byte[bytes01.length + bytes02.length - len];
System.arraycopy(bytes01, 0, newBytes, 0, offset);
System.arraycopy(bytes02, 0, newBytes, offset, bytes02.length);
System.arraycopy(bytes01, offset + len, newBytes, offset
+ bytes02.length, bytes01.length - offset - len);
return newBytes;
}

当然,本实战还包括一个被替换类、一个替换类以及一个执行类,实现起来并不复杂就不敷述了,最核心的部分已经在上文进行了分析,详细代码请见GitHub

小结

本实战的难点在于对类加载器的理解,以及一些字符串数组边界的处理。在处理Class文件时,直接将byte[]转换成String亦没有出错,并没有在中间转换一道MUTF-8。

参考文献