获取三种类路径

获取jre路径的原理为:

  1. 先判断是否有java -xjre xxx(java没有此命令)来指定启动类路径用于寻找和加载java标准库中的类。
  2. 若没有执行启动类路径,那么在当前目录下寻找 jre目录。
  3. 若当前目录下没有jre目录,取JAVA_HOME下的 jre。

Java虚拟机将使用JDK的启动类路径来寻找和加载Java标准库中的类,因此需要某种方式指定jre目录的位置(假设没有JAVA_HOME)。使用 -Xjre 命令来指定,注意这个选项的效果类似于-classpath 。
-cp是指定用户类路径,用于根据其执行的路径和后面所跟的类名,找到对应的class文件。

首先考虑一下在命令行使用时的可能的场景:

  1. 直接指定路径,后面跟类名:java -cp aaa/bbb/ccc ddd arg1 arg2
  2. 指定类所在的jar文件的路径,后面跟类名:java -cp aaa/bbb/ccc.jar ddd arg1 arg2
  3. 指定一个模糊路径,后面跟类名:java -cp aaa/bbb/* ddd arg1 arg2
  4. 指定若干个路径,后面跟类名(指定的类存在指定的某一条路径中,如果都存在那么以第一条为准):java -cp aaa1/bbb/ccc;aaa2/bbb/ccc;aaa3/bbb/ccc; ddd arg1 arg2

抽象类 Entry

  1. public abstract class Entry {
  2. //路径分隔符,在window下,使用 ; 分割开的 在Unix/Linux下使用: 分割开的
  3. public static final String pathListSeparator = System.getProperty("os.name").contains("Windows") ? ";" : ":";
  4. /**
  5. * 负责寻找和加载class文件
  6. *
  7. * @param className class文件的相对路径,路径之间用斜线 / 分隔,文件名有.class后缀
  8. */
  9. abstract byte[] readClass(String className) throws IOException;
  10. /**
  11. * @return 返回className的字符串表示形式;
  12. */
  13. abstract String printClassName();
  14. /**
  15. * 工厂方法,根据传入的path的形式不同,
  16. * @param path 命令行得到的路径字符串
  17. * @return 创建具体的Entry
  18. */
  19. static Entry createEntry(String path) {
  20. if (path != null) {
  21. if (path.contains(pathListSeparator)) {
  22. return new CompositeEntry(path, pathListSeparator);
  23. } else if (path.contains("*")) {
  24. return new WildcardEntry("");
  25. } else if (path.contains(".jar") || path.contains(".JAR") || path.contains(".zip") || path.contains("" +
  26. ".ZIP")) {
  27. return new ZipJarEntry(path);
  28. }
  29. return new DirEntry(path);
  30. }
  31. return null;
  32. }
  33. }

具体实现类

  • DirEntry
  • ZipJarEntry
  • WildcardEntry
  • CompositeEntry

这四个类是Entry 的具体实现类,分别对应使用场景中的四种情况,下面会分析四种情况所要解决的问题,请查看本项目源码。

DirEntry

这应该是最简单的一种使用场景了,构造方法中传入的字符串表示目录形式的类路径,这里拿到的直接就是指定的路径,那么先判断该路径是否存在,如果存在,那么和className拼接起来,使用IO流读取其中的字节码,并返回。

ZipJarEntry

这种使用场景是指定了一个zip文件或者jar包的情况,那么需要解决的问题是如何拿到压缩文件中的文件,这里主要使用了java.util.zip包下的类来拿到文件,使用zipFile来读取文件和读取普通的文件夹类似,这里只管把zip文件当成文件夹就好。
这里有一点要注意的是:
如果是zip文件,在获取ZipEntry的时候ZipEntry ze = zipFile.getEntry(zipName + “/“ + className)
如果是jar包,在获取ZipEntry的时候,ZipEntry ze = zipFile.getEntry(className)

WildcardEntry

这种使用场景是指定了一个形如aa/bb/*的路径,这种路径表明我们的class文件在aa/bb/路径下的jar包中,所以我们只要遍历该路径下的所有以.jar结尾的文件,然后调用ZipJarEntry的实现方法,即可以获得字节码.

CompositeEntry

这种使用场景是包含多个路径的情况,(eg:a1/b1/c1;a2/b2/c2;a3/b3/c3),那么遇到这种情况,需要将字符串分割成不同的子串,注意分割符在不同的系统下是不同的,这里仅仅实现windows,其它情况下视为unix

  1. public static final String pathListSeparator = System.getProperty("os.name").contains("Windows") ? ";" : ":";

当然分成的子串分别对应一个DirEntry,再调用DirEntry的方法来获取字节码.

classpath对外的统一使用接口

上面我们已经实现了对于 classpath 路径的解析,这里在进行一步封装,提供一个对外的统一接口.

  1. public class ClassPath {
  2. //分别存放三种类路径
  3. Entry bootClasspath;
  4. Entry extClasspath;
  5. Entry userClasspath;
  6. //parse()函数使用 -Xjre 选项解析启动类路径和扩展类路径
  7. // 使用-classpath/-cp选项解析用户类路径
  8. public ClassPath(String jreOption, String cpOption) {
  9. parseBootAndExtClasspath(jreOption);
  10. parseUserClasspath(cpOption);
  11. }
  12. //这里参数传进来的是: C:\Program Files\Java\jdk1.8.0_20\jre
  13. void parseBootAndExtClasspath(String jreOption) {
  14. String jreDir = getJreDir(jreOption);
  15. //可能出现的情况是: jre/lib/*
  16. String jreLibPath = jreDir + File.separator + "lib" + File.separator + "*";
  17. bootClasspath = new WildcardEntry(jreLibPath);
  18. //可能出现的情况是: jre/lib/ext/*
  19. String jreExtPath = jreDir + File.separator + "lib" + File.separator + "ext" + File.separator + "*";
  20. extClasspath = new WildcardEntry(jreExtPath);
  21. }
  22. String getJreDir(String jreOption) {
  23. File jreFile;
  24. if (jreOption != null && jreOption != "") {
  25. jreFile = new File(jreOption);
  26. if (jreFile.exists()) {
  27. return jreOption;
  28. }
  29. }
  30. jreFile = new File("jre");
  31. if (jreFile.exists()) {
  32. return jreFile.getAbsolutePath();
  33. }
  34. String java_home = System.getenv("JAVA_HOME");
  35. if (java_home != null) {
  36. return java_home + File.separator + "jre";
  37. }
  38. throw new RuntimeException("Can not find jre folder!");
  39. }
  40. void parseUserClasspath(String cpOption) {
  41. userClasspath = Entry.createEntry(cpOption);
  42. }
  43. public byte[] readClass(String className) {
  44. className = className + ".class";
  45. byte[] data;
  46. try {
  47. data = bootClasspath.readClass(className);
  48. if (data != null) {
  49. return data;
  50. }
  51. data = extClasspath.readClass(className);
  52. if (data != null) {
  53. return data;
  54. }
  55. data = userClasspath.readClass(className);
  56. if (data != null) {
  57. return data;
  58. }
  59. } catch (IOException e) {
  60. e.printStackTrace();
  61. }
  62. throw new RuntimeException("can't find class!");
  63. }
  64. @Override
  65. public String toString() {
  66. return userClasspath.printClassName();
  67. }
  68. }

Jre目录的寻找:优先使用用户输入的 -Xjre 选项作为 jre 目录。如果没有输入该选项,则在当前目录下寻找 jre 目录。如果找不到,尝试使用 JAVA_HOME 环境变量。
class 文件寻找的优先级,依次是:bootClasspath-> extClasspath -> userClasspath 。 分别对应

  • jre/lib/*
  • jre/lib/ext/*
  • -cp的值

然后再根据这三个Entry的顺序来加载类文件,一旦加载到,直接返回。最终,使用readClass()方法就可以返回我们要加载的类文件字节数组。