02_查找class文件

查找class文件

加载允许一个类,必须把它相关的依赖类也加载进来,比如父类、成员类等。

Java虚拟机规范并没有规定去哪里寻找类,所以不同虚拟机可以采用不同的方法。

一、类加载路径

Oracle的Java虚拟机是根据类路径(classpath)来搜索类,按照搜索顺序可分为3类:

  1. 启动类路径(bootstrasp classpath):默认目录是 jre\lib,即Java标准库(大部分在rt.jar里)所在位置
  2. 扩展类路径(extension classpath):默认目录是 jre\lib\ext,即Java扩展机制的类所在位置
  3. 用户类路径(user classpath):默认当前目录,即自己实现的类、以及第三方类库所在位置

可以通过参数 -Xbootclasspath 来修改启动类路径。

可以设置环境变量 CLASSPATH 来修改用户类路径,也可以使用参数 -classpath/-cp 来设置用户类路径。

1
2
3
java -cp path\classes ...
java -cp path\lib1.jar ...
java -cp path\lib2.zip ...

-classpath/-cp 既可以使用目录,也可以指向 jar 文件或者 zip 文件,可以同时指定多个目录或文件。

指定多个路径,需要分隔符分开,不同操作系统的分隔符不一样,在 windows 下是分号 ;,在类 Unix 下是冒号 :

1
java -cp path\classes;path\lib1.jar;path\lib2.zip ...

从 Java 6 开始,还可以使用通配符(*)指定某个目录下的所有 jar 文件(注意,不会递归子目录的 jar 文件):

1
java -cp path\classes;path\lib\* ...

二、类文件查找

2.1 添加jre参数

首先在命令行 Cmd 类中增加一个非标准参数 Xjre,表示 jre 所在的目录路径。

1
2
3
4
options.addOption(Option.builder("Xjre")
.hasArg().desc("jre directory")
.type(String.class)
.build());
1
private String jreOption;
1
2
3
4
// jre路径
if (line.hasOption("Xjre")) {
jreOption = line.getOptionValue("Xjre");
}

2.2 查找入口类

其次,要实现不同类加载路径的入口类,接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface Entry {

/**
* 读取class文件
* @param className class的完整名称(java/lang/Object)
* @return 文件字节
*/
byte[] readClass(String className) throws IOException;

/**
* 路径
* @return 路径
*/
String string();
}

然后有4种入口实现,分别对应上面几种类加载路径的写法:

  • DirEntry:查找 class 的目录
  • ZipEntry:查找 class 的 zip 或 jar 文件
  • CompositeEntry:多种查找路径的组合,比如目录,或 zip,或 jar
  • WildcardEntry:通配符路径,指定某个目录下的所有子 zip 或 jar 文件

各自的实现代码如下:

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
/**
* 目录入口
*/
public class DirEntry implements Entry {

/**
* 目录绝对路径
*/
private String absPath;

public DirEntry(String path) {
this.absPath = Paths.get(path).toAbsolutePath().toString();
}

@Override
public byte[] readClass(String className) throws IOException {
Path path = Paths.get(absPath, className);
if (Files.exists(path)) {
System.out.println(className + " found in " + string());
return Files.readAllBytes(path);
}
return null;
}

@Override
public String string() {
return absPath;
}
}
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
/**
* zip或jar入口
*/
public class ZipEntry implements Entry {

/**
* 文件绝对路径
*/
private String absPath;

public ZipEntry(String path) {
this.absPath = Paths.get(path).toAbsolutePath().toString();
}

@Override
public byte[] readClass(String className) throws IOException {
try (FileInputStream fis = new FileInputStream(absPath);
BufferedInputStream buf = new BufferedInputStream(fis);
ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream(buf)) {
ArchiveEntry entry;
while ((entry = in.getNextEntry()) != null) {
if (!in.canReadEntryData(entry)) {
continue;
}
// 转成路径格式一样的才能对比得上完整名称
String entryPath = Paths.get(entry.getName()).toString();
if (entryPath.equals(className)) {
System.out.println(className + " found in " + string());
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(in, out);
return out.toByteArray();
}
}
} catch (ArchiveException e) {
e.printStackTrace();
}
return null;
}

@Override
public String string() {
return absPath;
}
}
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
/**
* 组合入口
*/
public class CompositeEntry implements Entry {

/**
* 路径选项
*/
private String pathOptions;
/**
* 子入口列表
*/
private List<Entry> entries;

public CompositeEntry(String pathOptions) {
this.pathOptions = pathOptions;
this.initEntries();
}

@Override
public byte[] readClass(String className) throws IOException {
if (entries == null) {
return null;
}
for (Entry e : entries) {
byte[] bytes = e.readClass(className);
if (bytes != null) {
return bytes;
}
}
return null;
}

@Override
public String string() {
if (entries == null) {
return pathOptions;
}
StringBuilder sb = new StringBuilder();
sb.append("CompositeEntry [");
for (Entry e : entries) {
sb.append(e.string()).append(", ");
}
sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();
}

/**
* 初始化Entries列表
*/
private void initEntries() {
String[] paths = pathOptions.split(File.pathSeparator);
entries = new ArrayList<>(paths.length);
for (String path : paths) {
entries.add(EntryFactory.newEntry(path));
}
System.out.println(string());
}
}
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
/**
* 通配符入口
*/
public class WildcardEntry implements Entry {

/**
* 绝对路径
*/
private String absPath;
/**
* 子入口列表
*/
private List<Entry> entries;

public WildcardEntry(String path) {
// 去掉后面的通配符
String p = path.substring(0, path.length() - 1);
this.absPath = Paths.get(p).toAbsolutePath().toString();
initEntries();
}

@Override
public byte[] readClass(String className) throws IOException {
if (entries == null) {
return null;
}
for (Entry e : entries) {
byte[] bytes = e.readClass(className);
if (bytes != null) {
return bytes;
}
}
return null;
}

@Override
public String string() {
if (entries == null) {
return absPath;
}
StringBuilder sb = new StringBuilder();
sb.append("WildcardEntry [");
for (Entry e : entries) {
sb.append(e.string()).append(", ");
}
sb.delete(sb.length() - 2, sb.length());
sb.append("]");
return sb.toString();
}

/**
* 初始化Entries列表
*/
private void initEntries() {
File dir = new File(absPath);
if (!dir.isDirectory()) {
return;
}

// 获取目录下所有的zip或jar文件
File[] files = dir.listFiles((dir1, name) -> {
String lowerName = name.toLowerCase();
return lowerName.endsWith(".zip") || lowerName.endsWith(".jar");
});
if (files == null) {
return;
}

// 生成所有子入口实例
entries = new ArrayList<>(files.length);
for (File file : files) {
String filePath = file.getAbsolutePath();
entries.add(EntryFactory.newEntry(filePath));
}
System.out.println(string());
}
}

2.3 类查找实现

查找入口搞定后,接下来就是真正的类查找实现了。

首先,对命令行参数进行解析,包括设置 jre 目录、启动类路径、用户类路径等,并创建对应的入口。

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
/**
* 类路径
*/
public class Classpath {

/**
* 启动类路径
*/
private Entry bootEntry;
/**
* 扩展类路径
*/
private Entry extEntry;
/**
* 用户类路径
*/
private Entry userEntry;

/**
* 启动路径选项
*/
private String jreOption;
/**
* 用户类路径选项
*/
private String cpOptions;

public Classpath(String jreOption, String cpOptions) {
this.jreOption = jreOption;
this.cpOptions = cpOptions;
initOptions();
}

/**
* classpath
*/
public String string() {
return cpOptions;
}

/**
* 初始化选项
*/
private void initOptions() {
parseBootAndExtClasspath();
parseUserClasspath();
}

/**
* 解析启动类路径和扩展类路径
*/
private void parseBootAndExtClasspath() {
String jrePath = getJrePath();

// 启动类路径(jre/lib/*)
Path bootPath = Paths.get(jrePath, "lib");
String bootDir = bootPath.toAbsolutePath() + File.separator + "*";
bootEntry = EntryFactory.newEntry(bootDir);

// 扩展类路径(jre/lib/ext/*)
Path extPath = Paths.get(jrePath, "lib", "ext");
String extDir = extPath.toAbsolutePath() + File.separator + "*";
extEntry = EntryFactory.newEntry(extDir);
}

/**
* 解析用户类路径
*/
private void parseUserClasspath() {
if (cpOptions == null || "".equals(cpOptions)) {
return;
}
// 用户类路径(-classpath/-cp)
userEntry = EntryFactory.newEntry(cpOptions);
}

/**
* 获取jre文件夹路径
*
* @return jre文件夹路径
*/
private String getJrePath() {
Path path;

// 用户自定义路径
if (jreOption != null) {
path = Paths.get(jreOption);
if (Files.exists(path)) {
return path.toString();
}
}

// 系统环境变量
String jdkPath = System.getenv("JAVA_HOME");
if (jdkPath != null) {
path = Paths.get(jdkPath, "jre");
if (Files.exists(path)) {
return path.toString();
}
}

// 最后尝试在当前路径下寻找jre目录
path = Paths.get(".", "jre");
if (Files.exists(path)) {
return path.toString();
}

// 找不到jre的路径
throw new IllegalStateException("Can not found jre folder!");
}

}

然后,就是实际的类查找流程实现了,它是按照 启动类路径 -> 扩展类路径 -> 用户类路径 去查找的:

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
/**
* 类路径
*/
public class Classpath {

/**
* 读取class文件
*
* @param className class的完整名称(java/lang/Object)
* @return 文件字节
*/
public byte[] readClass(String className) throws IOException {
byte[] bytes = null;

// 转换成文件路径
String classPath = Paths.get(className).toString();
String classFileName = classPath + ".class";

// 启动类加载
if (bootEntry != null) {
bytes = bootEntry.readClass(classFileName);
}

// 扩展类加载
if (bytes == null && extEntry != null) {
bytes = extEntry.readClass(classFileName);
}

// 用户类加载
if (bytes == null && userEntry != null) {
bytes = userEntry.readClass(classFileName);
}

if (bytes == null) {
System.out.println("not found class " + className);
}

return bytes;
}

}

另外,创建入口实例的方法统一放到了一个工厂类里面,为了方便查看和修改:

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
public class EntryFactory {

/**
* 根据参数生成指定的Entry类
*
* @param entryOption 选项参数
* @return 具体的Entry实例
*/
public static Entry newEntry(String entryOption) {
// 多选项路径
if (entryOption.contains(File.pathSeparator)) {
return new CompositeEntry(entryOption);
}

// 通配符路径
if (entryOption.endsWith("*")) {
return new WildcardEntry(entryOption);
}

// zip或者jar
String lowName = entryOption.toLowerCase();
if (lowName.endsWith(".zip") || lowName.endsWith(".jar")) {
return new ZipEntry(entryOption);
}

// 文件夹路径
return new DirEntry(entryOption);
}

}

完整的类查找代码差不多就是这样了,后面就是要测试实际的效果了。

2.4 单元测试

下面就是单元测试的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClasspathTest {

@Test
public void readClass() throws IOException {
String jreOption = null;
String cpOption = "C:\\IdeaProjects\\self-jvm\\target;C:\\IdeaProjects\\self-jvm\\target\\classes";
String bootClassName = "java\\lang\\Object";
String userClassName = "com\\wjd\\cmd\\Cmd";
Classpath classpath = new Classpath(jreOption, cpOption);
Assert.assertNotNull("Object is null", classpath.readClass(bootClassName));
Assert.assertNotNull("Cmd is null", classpath.readClass(userClassName));
}
}

它的输出结果如下:

1
2
3
4
5
6
7
8
9
WildcardEntry [D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\charsets.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\deploy.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\javaws.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\jce.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\jfr.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\jfxswt.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\jsse.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\management-agent.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\plugin.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\resources.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\rt.jar]

WildcardEntry [D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\access-bridge-64.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\cldrdata.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\dnsns.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\jaccess.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\jfxrt.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\localedata.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\nashorn.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\sunec.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\sunjce_provider.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\sunmscapi.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\sunpkcs11.jar, D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\ext\zipfs.jar]

CompositeEntry [C:\IdeaProjects\self-jvm\target, C:\IdeaProjects\self-jvm\target\classes]

java\lang\Object.class found in D:\Program Files\JDK\jdk1.8.0_25_x64\jre\lib\rt.jar

com\wjd\cmd\Cmd.class found in C:\IdeaProjects\self-jvm\target\classes

看起来,实际加载的包和最后查找到的类路径都是对的。

总结

  • Oracle的Java虚拟机是根据类路径(classpath)来搜索类,按照搜索顺序可分为3类:

    1. 启动类路径(bootstrasp classpath):默认目录是 jre\lib,即Java标准库(大部分在rt.jar里)所在位置
    2. 扩展类路径(extension classpath):默认目录是 jre\lib\ext,即Java扩展机制的类所在位置
    3. 用户类路径(user classpath):默认当前目录,即自己实现的类、以及第三方类库所在位置
  • -classpath/-cp 既可以使用目录,也可以指向 jar 文件或者 zip 文件,可以同时指定多个目录或文件。

  • 指定多个路径,需要分隔符分开,不同操作系统的分隔符不一样,在 windows 下是分号 ;,在类 Unix 下是冒号 :

  • 从 Java 6 开始,还可以使用通配符(*)指定某个目录下的所有 jar 文件(注意,不会递归子目录的 jar 文件)

  • 具体代码实现,有4种入口类,分别对应几种类加载路径的写法:

    • DirEntry:查找 class 的目录
    • ZipEntry:查找 class 的 zip 或 jar 文件
    • CompositeEntry:多种查找路径的组合,比如目录,或 zip,或 jar
    • WildcardEntry:通配符路径,指定某个目录下的所有子 zip 或 jar 文件

ps:上面的代码实现,为了简单,有些地方没做参数校验,默认传入参数的格式都是正确的(好吧。。实际是我懒得写了,先把功能实现了再说~~)

作者

jiaduo

发布于

2021-09-17

更新于

2023-04-03

许可协议