查找class文件
加载允许一个类,必须把它相关的依赖类也加载进来,比如父类、成员类等。
Java虚拟机规范并没有规定去哪里寻找类,所以不同虚拟机可以采用不同的方法。
一、类加载路径
Oracle的Java虚拟机是根据类路径(classpath)来搜索类,按照搜索顺序可分为3类:
- 启动类路径(bootstrasp classpath):默认目录是
jre\lib
,即Java标准库(大部分在rt.jar里)所在位置
- 扩展类路径(extension classpath):默认目录是
jre\lib\ext
,即Java扩展机制的类所在位置
- 用户类路径(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
| 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 {
byte[] readClass(String className) throws IOException;
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
|
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(); }
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(); }
private void initEntries() { File dir = new File(absPath); if (!dir.isDirectory()) { return; }
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(); }
public String string() { return cpOptions; }
private void initOptions() { parseBootAndExtClasspath(); parseUserClasspath(); }
private void parseBootAndExtClasspath() { String jrePath = getJrePath();
Path bootPath = Paths.get(jrePath, "lib"); String bootDir = bootPath.toAbsolutePath() + File.separator + "*"; bootEntry = EntryFactory.newEntry(bootDir);
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; } userEntry = EntryFactory.newEntry(cpOptions); }
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(); } }
path = Paths.get(".", "jre"); if (Files.exists(path)) { return path.toString(); }
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 {
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 {
public static Entry newEntry(String entryOption) { if (entryOption.contains(File.pathSeparator)) { return new CompositeEntry(entryOption); }
if (entryOption.endsWith("*")) { return new WildcardEntry(entryOption); }
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类:
- 启动类路径(bootstrasp classpath):默认目录是
jre\lib
,即Java标准库(大部分在rt.jar里)所在位置
- 扩展类路径(extension classpath):默认目录是
jre\lib\ext
,即Java扩展机制的类所在位置
- 用户类路径(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:上面的代码实现,为了简单,有些地方没做参数校验,默认传入参数的格式都是正确的(好吧。。实际是我懒得写了,先把功能实现了再说~~)