【JVM】手写Java虚拟机-02 搜索class文件

环境

操作系统:Windows 10

IDE:IntelliJ IDEA 2019.1 x64

JDK:Java 11.0.8

项目管理工具:apache-maven-3.5.4 [下载]

可能遇到的问题

可能很多工具函数看不懂,没事,都注释在代码中了。

开始

simpleJVM-2
├── pom.xml
└── src
    └── main
    │    └── java
    │        └── cn.zwy.simplejvm
    │             ├── classpath
    │             │   ├── impl
    │             │   │   ├── CompositeEntry.java
    │             │   │   ├── DirEntry.java 
    │             │   │   ├── WildcardEntry.java 
    │             │   │   └── ZipEntry.java    
    │             │   ├── Classpath.java
    │             │   └── Entry.java    
    │             ├── Cmd.java
    │             └── Main.java
    └── test
         └── java
             └── cn.zwy.test
                 └── HelloWorld.java

在Java中,Object类是所有类的父类。

我们要运行一个class文件,要先把class文件加载到JVM中,并且还要加载其中用到的像Object类这样的父类。执行的流程大概可以概括为:

解析命令行参数 -> 启动JVM -> 把类加载进JVM -> 调用main方法

由于双亲委派模型的存在,我们需要保证启动类加载器,在启动类路径加载java库,因此有参数-Xjre来指定jre目录的位置:

Cmd.java

package cn.zwy.simplejvm;

import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;

import java.util.List;

/**
 * @author weiyu_zeng
 *
 * Parameter函数参数:
 * names 设置命令行参数,如-old
 * required 设置此参数是否必须
 * description 设置参数的描述
 * order 设置帮助文档的顺序
 * help 设置此参数是否为展示帮助文档或者辅助功能
 *
 * -Xjre: jre目录位置指定
 */
public class Cmd {

    @Parameter(names = {"-?", "-help"}, description = "print help message", order = 3, help = true)
    boolean helpFlag = false;

    @Parameter(names = "-version", description = "print version and exit", order = 2)
    boolean versionFlag = false;

    @Parameter(names = {"-cp", "-classpath"}, description = "classpath", order = 1)
    String classpath;

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

    @Parameter(description = "main class and args")
    List<String> mainClassAndArgs;

    boolean ok;

    // 返回mainClassAndArgs的Class(List<String>的第一个参数)
    String getMainClass() {
        return (mainClassAndArgs != null && !mainClassAndArgs.isEmpty())
                ? mainClassAndArgs.get(0) : null;
    }

    List<String> getAppArgs() {
        return (mainClassAndArgs != null && mainClassAndArgs.size() > 1)
                ? mainClassAndArgs.subList(1, mainClassAndArgs.size()) : null;
    }

    // 解析函数:传入String[] argv,返回Cmd类
    static Cmd parse(String[] argv) {
        Cmd args = new Cmd();
        JCommander cmd = JCommander.newBuilder().addObject(args).build();
        cmd.parse(argv);
        args.ok = true;
        return args;
    }
}

JVM 可读取的类路径项,有四种类型:
a.目录形式(DirEntry.java)
b.压缩包形式(jar, zip)(ZipEntry.java)
c.通配符形式 (WildcardEntry.java)
d.混合形式(就是以上三种的混合,以路径分隔符分割)(CompositeEntry.java)

我们创建四个类路径文件,先放着:
在这里插入图片描述

然后我们来定义这四个类路径的接口,Entry.java

package cn.zwy.simplejvm.classpath;

import cn.zwy.simplejvm.classpath.impl.CompositeEntry;
import cn.zwy.simplejvm.classpath.impl.DirEntry;
import cn.zwy.simplejvm.classpath.impl.ZipEntry;
import cn.zwy.simplejvm.classpath.impl.WildcardEntry;

import java.io.IOException;
import java.io.File;

/**
 * @author weiyu_zeng
 * 类路径接口
 *
 * java.io.File: File.pathSeparator字段,代表系统目录中的间隔符/分隔符 "/" "\",能够兼容不同系统(window,Unix等)
 */
public interface Entry {

    byte[] readClass(String className) throws IOException;

    static Entry create(String path) {

        // 包含以分隔符分割的多种组合路径
        if (path.contains(File.pathSeparator)) {
            return new CompositeEntry(path);
        }

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

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

        // 目录
        return new DirEntry(path);
    }
}

然后定义四种类路径class:

首先是最简单的目录形式的类路径,DirEntry.java

package cn.zwy.simplejvm.classpath.impl;

import cn.zwy.simplejvm.classpath.Entry;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * @author weiyu_zeng
 * 目录形式的类路径
 *
 * java.nio.file.Paths: get()方法,用来获取 Path 对象,可以将多个字符串串连成路径
 * java.nio.file.Path: toAbsolutePath()方法,作为绝对路径返回调用 Path 对象
 *                     resolve(Path p)方法,将相对路径解析为绝对路径
 *
 * java.nio.file.Files: readAllBytes(Path path)方法,读取文件中的所有字节。
 */
public class DirEntry implements Entry {

    private final Path absolutePath;

    public DirEntry(String path) {
        // 获取绝对路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }

    @Override
    public byte[] readClass(String className) throws IOException {
        return Files.readAllBytes(absolutePath.resolve(className));
    }

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

压缩包形式类路径,ZipEntry.java

package cn.zwy.simplejvm.classpath.impl;

import cn.zwy.simplejvm.classpath.Entry;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.Files;


/**
 * @author weiyu_zeng
 * zip/jar包形式类路径
 *
 * java.nio.file.FileSystems: newFileSystem(Path path, ClassLoader loader)方法,构建一个文件系统
 *                            去访问path指定的文件的内容,该专门用来创建虚拟文件系统的providers。
 *                            这里获取FileSystem实例。
 * java.nio.file.FileSystem: getPath(String first,String... more)方法, 将字符串拼接成一个路径,
 *                           并根据路径生成Path的实例。
 */
public class ZipEntry implements Entry {

    private Path absolutePath;

    public ZipEntry(String path) {
        // 得到绝对路径
        this.absolutePath = Paths.get(path).toAbsolutePath();
    }

    @Override
    public byte[] readClass(String className) throws IOException {
        try (FileSystem zipFs = FileSystems.newFileSystem(absolutePath, null)) {
            return Files.readAllBytes(zipFs.getPath(className));
        }
    }
}

包含以分隔符分割的多种组合路径,CompositeEntry:

package cn.zwy.simplejvm.classpath.impl;

import cn.zwy.simplejvm.classpath.Entry;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


/**
 * @author weiyu_zeng
 * 包含以分隔符分割的多种组合路径
 *
 * 构造函数CompositeEntry:获取包含以分隔符分割的多种组合路径的绝对路径
 * readClass方法:赋予Entry中readClass方法的实现细节,读取CompositeEntry的class文件
 */
public class CompositeEntry implements Entry {

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

    public CompositeEntry(String pathList) {
        String[] paths = pathList.split(File.pathSeparator);  // 以分隔符分隔开path,一个元素是单个的path
        for (String path: paths) {
            entryList.add(Entry.create(path));  // 套娃:再把单个的path传入Entry.create,最后就会当做目录形式的类路径
                                                // DirEntry来调用。所以其实CompositeEntry底层也还是DirEntry。
        }
    }

    @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);  // 把strs中的元素用File.pathSeparator连接起来
    }
}

包含通配符的路径,WildcardEntry.java:

package cn.zwy.simplejvm.classpath.impl;


import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;


/**
 * @author weiyu_zeng
 * 包含通配符的路径
 *
 * Files.walk:传入Path类型,返回Stream<Path>类型。Files.walk(Paths.get(...))可以看做固定搭配。
 *             列出Path中所有的文件。加filter之后可以列出特定格式的文件。
 * "::" :A :: B,等于调用A的B方法。
 * Stream流中的常用方法filter:用于对Stream流中的数据进行过滤,Stream<T> filter(Predicate<? super T> predicate);
 *                            filter方法参数Predicate是一个函数式接口,可传递Lambda表达式,对数据进行过滤。
 * Stream流中的常用方法map:将对象转换为其他对象,常和正则式一起用。
 * Stream流中的常用方法collect:在这里将Stream对象转为String对象。Stream对象.collect(Collectors.joining("某字符"))可以看做
 *                             固定搭配。实际就是把返回的String对象用"某字符"连起来。
 *
 * 构造函数WildcardEntry:获取包含以分隔符分割的多种组合路径的绝对路径
 * readClass方法:赋予Entry中readClass方法的实现细节,读取CompositeEntry的class文件
 */
public class WildcardEntry extends CompositeEntry {

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

    private static String toPathList(String wildcardPath) {
        String baseDir = wildcardPath.replace("*", ""); // 去掉"*"符号
        try {
            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 "";
        }
    }
}

因为有"*"的存在,所以这里要Files.walk来搜索路径下所有的文件,加载到jvm中。

ClassPath.java

package cn.zwy.simplejvm.classpath;

import cn.zwy.simplejvm.classpath.impl.WildcardEntry;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;


/**
 * @author weiyu_zeng
 * 类路径
 *
 * System.getenv():方法是获取指定的环境变量的值。接收参数为任意字符串,当存在指定环境变量时即返回环境变量的值,否则返回null。
 * Files.exits():方法用来检查给定的Path在文件系统中是否存在
 */
public class Classpath {
    private Entry bootstrapClasspath;  // 启动类路径
    private Entry extensionClasspath;  // 拓展类路径
    private Entry userClasspath;  // 用户类路径

    public Classpath(String jreOption, String cpOption) {
        //启动类&扩展类 "C:\Program Files\Java\...\jre"
        parseBootAndExtensionClasspath(jreOption);
        //用户类 E:\..\test\java\cn\zwy\test\HelloWorld
        parseUserClasspath(cpOption);
    }

    // 解析启动类路径和拓展类路径
    private void parseBootAndExtensionClasspath(String jreOption) {

        String jreDir = getJreDir(jreOption);

        // 启动类加载器负责 jre/lib 目录下类库。有通配符用WildcardEntry
        String jreLibPath = Paths.get(jreDir, "lib") + File.separator + "*";
        bootstrapClasspath = new WildcardEntry(jreLibPath);

        // 拓展类加载器负责 jre/lib/ext 目录下的类库。有通配符用WildcardEntry
        String jreExtPath = Paths.get(jreDir, "lib", "ext") + File.separator + "*";
        extensionClasspath = new WildcardEntry(jreExtPath);
    }

    // 获取jre路径
    private static String getJreDir(String jreOption) {
        /*
         * 寻找 jre 路径顺序
         * 1. 用户指定路径
         * 2. 当前文件夹下的 jre 文件夹
         * 3. 系统环境变量 JAVA_HOME 指定的文件夹
         * 作者:何人听我楚狂声
         */
        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("Can not find JRE folder!!");
    }

    // 解析用户类路径
    private void parseUserClasspath(String cpOption) {
        if (cpOption == null) {
            cpOption = ".";
        }
        userClasspath = Entry.create(cpOption);
    }

    // 双亲委派机制,先从启动类加载器加载,加载不出不报错。
    public byte[] readClass(String className) throws Exception {
        className = className + ".class";

        // 启动类路径
        try {
            return bootstrapClasspath.readClass(className);
        } catch (Exception ignored) {
            // ignored
        }

        // 拓展类路径
        try {
            return extensionClasspath.readClass(className);
        } catch (Exception ignored) {
            // ignored
        }

        // 用户类路径
        return userClasspath.readClass(className);
    }
}

我们把之前的HelloWorld,用javac解析成class文件
在这里插入图片描述

找到文件位置,右键,open in terminal
在这里插入图片描述
然后命令行里输入javac HelloWorld.java
在这里插入图片描述
得到的HelloWorld.class文件复制到src\main\resources下

在这里插入图片描述

重写一下Main.java

package cn.zwy.simplejvm;

import cn.zwy.simplejvm.classpath.Classpath;
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        Cmd cmd = Cmd.parse(args);
        if (!cmd.ok || cmd.helpFlag) {
            System.out.println("Usage: <main class> [-options] class [args...]");
            return;
        }
        if (cmd.versionFlag) {
            System.out.println("java version \"1.8.0\"");
            return;
        }
        startJVM(cmd);
    }

    // %s链接后面的变量
    // 分别打印classpath, getMainClass()返回值,和getAppArgs()返回值
    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());
        //获取className
        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();
        }
    }
}

执行

打开

在这里插入图片描述

在这里插入图片描述
在这里我们填入,-Xjre "你的jre的路径" 你的HelloWorld.class的路径

-Xjre "D:\Other Program\JAVA\jre" D:\Project\JAVA_simpleJVM\simpleJVM-2\src\main\resources\HelloWorld

如果你跟我一样是java11,那么java11是不自带jre的,你还需要把jre弄出来,可见这里

有jre之后,运行Main.java:

在这里插入图片描述
完成。

参考

https://bugstack.cn/itstack-demo-jvm/itstack-demo-jvm.html

https://www.nowcoder.com/discuss/604388?source_id=profile_create_nctrack&channel=-1

《自己动手写Java虚拟机》

https://github.com/Xtarling/JVM-Demo

https://www.cnblogs.com/fengli9998/p/9002377.html

https://geek-docs.com/java/java-tutorial/fileswalk.html#Fileswalk

https://blog.csdn.net/zebe1989/article/details/83054037

https://www.cnblogs.com/liangblog/p/8920579.html

https://blog.csdn.net/qq_22695001/article/details/101074284

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锥栗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值