用Java手写JVM第二章——搜索class文件


第1章介绍了java命令如何启动Java应用程序:首先解析命令行参数,启动JVM,将主类加载进JVM,最后调用主类的 main()方法。例如一个HelloWorld程序:

public class HelloWorld {
    
    public static void main(String[] args) {
        System.out.println("hello world");
    }
    
}

加载HelloWorld类之前,首先要加载它的父类,也就是java.lang.object。在调用main()方法之前,因为虚拟机需要准备好参数数组,所以需要加载java.lang.String和java.lang.String[]类。把字符串打印到控制台还需要加载java.lang.System类,等等。若要加载这些类,就必须提到双亲委派机制。

代码目录

ZYX-demo-jvm-02
├── pom.xml
└── src
    └── main
    │    └── java
    │        └── org.ZYX.demo.jvm
    │             ├── classpath
    │             │   ├── impl
    │             │   │   ├── CompositeEntry.java
    │             │   │   ├── DirEntry.java 
    │             │   │   ├── WildcardEntry.java 
    │             │   │   └── ZipEntry.java    
    │             │   ├── Classpath.java
    │             │   └── Entry.java    
    │             ├── Cmd.java
    │             └── Main.java
    └── test
         └── java
             └── org.ZYX.demo.test
                 └── HelloWorld.java

一、类路径

Oracle的Java虚拟机实现根据类路径(classpath)来搜索类。按照搜索的先后顺序,类路径可以分为以下3个部分。当 JVM 需要一个类时,就会按照上述顺序在类路径中查找,如果在启动类路径中找不到,就会去扩展类路径寻找,最后才去用户类路径寻找:
·启动类路径(bootstrap classpath):默认对应·jre\lib目录,Java标准库(大部分在rt.jar里)位于该路径。
·扩展类路径(extension classpath):默认对应·jre\lib\ext目录,使用Java扩展机制的类位于这个路径。
·用户类路径(user classpath)我们自己实现的类,以及第三方类库则位于用户类路径,位于当前目录,也可以通过-cp和-classpath指定。

首先我们创建一个classPath子目录。

我们的Java虚拟机将使用JDK的启动类路径来寻找和加载Java标准库中的类,因此需要某种方式指定jre目录的位置。命令行选项是个不错的选择,所以增加一个非标准选项-Xjre。修改Cmd类,增加一个属性:

    @Parameter(names = "-Xjre", description = "path to jre", order = 4)
    String jre;

JVM 可读取的类路径项,分四种类型:目录形式、压缩包形式(jar 和 zip)、通配符形式和混合形式(就是以上三种的混合体,以路径分隔符分割)。

二、实现类路径

下面我们来实现类路径。

1、Entry接口及其实现类

先定义一个接口Entry来表示类路径项。代码如下:

public interface Entry {
    
    /*此方法留给具体类来实现,负责寻找和加载class文件;
    注意这个方法的参数是 class 的相对路径,例如读取 java.lang.Object 类,应传入 java/lang/Object;
    */
    byte[] readClass(String className) throws IOException;
    
    //静态方法 create() 根据传入的路径字符串,来判断具体创建哪种实现类;
    static Entry create(String path) {

        //File.pathSeparator;路径分隔符(win\linux)
        if (path.contains(File.pathSeparator)) {
            return new CompositeEntry(path);
        }

        if (path.endsWith("*")) {
            return new WildcardEntry(path);
        }

        if (path.endsWith(".jar") || path.endsWith(".JAR") ||
                path.endsWith(".zip") || path.endsWith(".ZIP")) {
            return new ZipEntry(path);
        }

        return new DirEntry(path);
    }

}

①DirEntry:

DirEntry相对简单一些,表示目录形式的类路径。DirEntry只有一个字段,用于存放目录的绝对路径。代码如下:

public class DirEntry implements Entry {

    private Path absolutePath;

    public DirEntry(String path){
        //获取绝对路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }
    
    /*利用 java.nio.file.Path 类,可以直接在路径下找到对应文件,
    再使用 Files.readAllBytes() 直接读取到字节数组;
    */
    @Override
    public byte[] readClass(String className) throws IOException {
        return Files.readAllBytes(absolutePath.resolve(className));
    }

    @Override
    public String toString() {
        return this.absolutePath.toString();
    }
}

②ZipEntry:

ZipEntry表示ZIP或JAR文件形式的类路径。这种情况下,class 的相对路径就是压缩内部的目录的路径。代码如下:

public class ZipEntry implements Entry {

    private Path absolutePath;

    public ZipEntry(String path) {
        //获取绝对路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }
    
    //java.nio 包提供了一个 FileSystem 类,可以快速读取压缩包。这个实现就变得非常简单了;
    @Override
    public byte[] readClass(String className) throws IOException {
        try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
            return Files.readAllBytes(zipFs.getPath(className));
        }
    }

    @Override
    public String toString() {
        return this.absolutePath.toString();
    }

}

③CompositeEntry:

CompositeEntry 表示以文件分隔符(如 ;)分割的多个路径。它实际上可以分割成更小的 Entry 去处理,Entry 接口中的静态方法正好可以直接判断各个路径该如何创建 Entry。代码如下:

public class CompositeEntry implements Entry {

    private final List<Entry> entryList = new ArrayList<>();

    public CompositeEntry(String pathList) {
        String[] paths = pathList.split(File.pathSeparator);
        for (String path : paths) {
            entryList.add(Entry.create(path));
        }
    }
    
    //遍历 entryList 逐个读取;
    @Override
    public byte[] readClass(String className) throws IOException {
        for (Entry entry : entryList) {
            try {
                return entry.readClass(className);
            } catch (Exception ignored) {
                //ignored
            }
        }
        throw new IOException("class not found " + className);
    }


    @Override
    public String toString() {
        String[] strs = new String[entryList.size()];
        for (int i = 0; i < entryList.size(); i++) {
            strs[i] = entryList.get(i).toString();
        }
        return String.join(File.pathSeparator, strs);
    }
    
}

④WildcardEntry:

WildcardEntry 是结尾通配符的类路径,它实际上也就是 CompositeEntry,所以可以直接继承CompositeEntry。

public class WildcardEntry extends CompositeEntry {

    public WildcardEntry(String path) {
        super(toPathList(path));
    }

    private static String toPathList(String wildcardPath) {
        String baseDir = wildcardPath.replace("*", ""); // remove *
        try {
            /*将遍历 baseDir 下所有文件,挑选出 jar 包,拼接成字符串之后返回;
            在构造中这个字符串传递给父类构造,实际上这个过程就是将通配符转换为多个有效路径。
            */
            return Files.walk(Paths.get(baseDir))
                    .filter(Files::isRegularFile)
                    .map(Path::toString)
                    .filter(p -> p.endsWith(".jar") || p.endsWith(".JAR"))
                    .collect(Collectors.joining(File.pathSeparator));
        } catch (IOException e) {
            return "";
        }
    }

}

2、ClassPath

Entry接口和4个实现介绍完了,接下来实现Classpath类。Classpath 就用来读取上面提到的三种类路径了,在构造方法中就解析路径并构造 Entry。代买如下:

public class Classpath {

    // JVM 启动时必须要加载的三类类路径
    private Entry bootstrapClasspath;       // 启动类路径
    private Entry extensionClasspath;       // 扩展类路径
    private Entry userClasspath;            // 用户类路径

    public Classpath(String jreOption, String cpOption) {
        // 启动类和扩展类由 jre 提供
        parseBootAndExtensionClasspath(jreOption);
        // 解析用户自定义的类的路径
        parseUserClasspath(cpOption);
    }
}

两个 parse 方法就是根据传入的参数路径,构造 Entry 用的,很简单,就是个路径的拼接:

    private void parseBootAndExtensionClasspath(String jreOption) {
        String jreDir = getJreDir(jreOption);

        // 启动类在 jre/lib/*
        String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
        bootstrapClasspath = new WildcardEntry(jreLibPath);

        // 扩展类在 jre/lib/ext/*
        String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
        extensionClasspath = new WildcardEntry(jreExtPath);

    }

    private void parseUserClasspath(String cpOption) {
        // 如果用户没有提供-classpath/-cp选项,则使用当前目录作为用户类路径
        if(cpOption == null) {
            cpOption = ".";
        }
        userClasspath = Entry.create(cpOption);
    }

parseBootAndExtensionClasspath中的 getJreDir 方法用于获取 jre 的路径:

        /*
         * 寻找 jre 路径顺序
         * 1. 用户指定路径
         * 2. 当前文件夹下的 jre 文件夹
         * 3. 系统环境变量 JAVA_HOME 指定的文件夹
         */
    private static String getJreDir(String jreOption) {

        if(jreOption != null && Files.exists(Paths.get(jreOption))) {
            return jreOption;
        }
        if(Files.exists(Paths.get("./jre"))) {
            return "./jre";
        }
        String jh = System.getenv("JAVA_HOME");
        if(jh != null) {
            return Paths.get(jh, "jre").toString();
        }
        throw new RuntimeException("找不到 jre 路径!");
    }

最后是ReadClass()方法,它依次从启动类路径、扩展类路径和用户类路径中搜索class文件,代码如下:

    public byte[] readClass(String className) throws Exception {
        // 根据类名获取一个类的字节码
        // 根据双亲委派机制,按顺序读取,并且在前两个读取不到时不会报错
        className = className + ".class";

        try {
            return bootstrapClasspath.readClass(className);
        } catch (Exception e) {
            // ignored
        }

        try {
            return extensionClasspath.readClass(className);
        } catch (Exception e) {
            // ignored
        }

        return userClasspath.readClass(className);
    }

三、测试

main() 函数可以不用改动,我们只需要重写 startJVM() 函数即可:

 private static void startJVM(Cmd cmd) {
        Classpath cp = new Classpath(cmd.jre, cmd.classpath);
        System.out.printf("classpath:%s class:%s args:%s\n", cp, cmd.getMainClass(), cmd.getAppArgs());
        //获取主类目录名
        String className = cmd.getMainClass().replace(".", "/");
        try {
            byte[] classData = cp.readClass(className);
            
            System.out.println(Arrays.toString(classData));

            System.out.println("classData:");
//            for (byte b : classData) {
//                //16进制输出
//                System.out.print(String.format("%02x", b & 0xff) + " ");
//            }
        } catch (Exception e) {
            System.out.println("Could not find or load main class " + cmd.getMainClass());
            e.printStackTrace();
        }
    }

startJVM() 会先打印出命令行参数,然后读取主类数据,并打印到控制台。

我们还需要一个主类,本节最开头提到的 HelloWorld 类就可以。将它通过 javac 命令编译成 class 文件,再把class文件放置到resources下。

启动参数也需要对应修改为,-Xjre “本机jre路径” “HelloWorld.class的文件路径”

启动之后输出如下:

classpath:org.ZYX.demo.jvm.classpath.Classpath@7591083d classD:\JavaProject\Idea-project\ZYX-demo-jvm\ZYX-demo-jvm-02\src\main\resources\HelloWorld args:null
[-54, -2, -70, -66, 0, 0, 0, 52, 0, 29, 10, 0, 6, 0, 15, 9, 0, 16, 0, 17, 8, 0, 18, 10, 0, 19, 0, 20, 7, 0, 21, 7, 0, 22, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 4, 109, 97, 105, 110, 1, 0, 22, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 15, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 7, 0, 8, 7, 0, 23, 12, 0, 24, 0, 25, 1, 0, 13, 72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, 7, 0, 26, 12, 0, 27, 0, 28, 1, 0, 28, 111, 114, 103, 47, 90, 89, 88, 47, 100, 101, 109, 111, 47, 116, 101, 115, 116, 47, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 9, 0, 11, 0, 12, 0, 1, 0, 9, 0, 0, 0, 37, 0, 2, 0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 6, 0, 8, 0, 7, 0, 1, 0, 13, 0, 0, 0, 2, 0, 14]
classData:

以十六进制的形式输出了 HelloWorld 类的内容,虽然这些数据我们还看不懂,但是数据最前面的 8 个十六进制数,是 CAFEBABE,“咖啡宝贝”。了解过字节码的同学应该知道,这正是字节码最开头的 magic number,读取成功。

查看javaclass文件,在网络上搜索了一下资料,这样就很方便的去查看别人的代码 在myeclipse中查看class文件,就像查看普通的java文件一样,通过以下步骤可以做到: 安装设置步骤:1. 下载jadnt158.zip 。 2. 下载net.sf.jadclipse_3.3.jar(对应eclipse3.3) ,二者都可以去CSDN上下载。 3. 将jadnt158.zip 解压,拷贝jad.exe 到%JAVA_HOME%\bin\ 目录下。 4. 安装插件net.sf.jadclipse_3.3.jar (将此文件拷到eclipse 的plugins目录(%eclipse_home%\plugins)下,例如C:\MyEclipse Enterprise Workbench 6.5GA\eclipse\plugins下)。 5. 设定路径:进入Window -> Preferences -> Java -> JadClipse,Path to decomiler设置为jad.exe的绝对路径,例如C:\j2sdk1.4.2\bin\jad.exe,Directory for temporary files设置为存放临时文件的决对路径这里我把它设置为C:\Documents and Settins\桌面\.net.sf.jadclipse 。 6. 在Eclipse 的Windows—> Perferences—>General->Editors->File Associations 中修改“*.class”默认关联的编辑器为“JadClipse Class File Viewer”。如果没有*.class,则点击“add"增加,如果没有jadclipse Class File Viewer,可以点击“Add-Internal Editors”,查找是否有jadclipse,若还是没有,则说明net.sf.jadclipse没有复制到正确的位置. 安装设置完毕,可以双击class文件,测试一下,正确的情况是:反编译成功,可以显示class文件(无需增加src源文件)为普通的java源代码样式,另外,在Myeclipse菜单栏上多出了一个JadClipse菜单. 现在可以通过myeclipse直接查看class文件,阅读代码效率高了许多~~~~~~ 安装时应注意:jadclipse的版本一定要和eclipse的版本一致!比如,我用的Myeclipse版本为6.5,其包含的eclipse版本为3.3。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值