欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

go实现java虚拟机02

程序员文章站 2022-06-23 22:26:57
上一篇通过flag包实现了命令行参数的解析,其实就是将输入的参数保存到一个结构体中,上一篇说过的例如java -classpath hello.jar HelloWorld这种命令,那么HelloWorld这个类是怎么找出来的呢?是直接在hello.jar中去找吗? 还记得java的类加载机制吗?有 ......

  上一篇通过flag包实现了命令行参数的解析,其实就是将输入的参数保存到一个结构体中,上一篇说过的例如java -classpath hello.jar helloworld这种命令,那么helloworld这个类是怎么找出来的呢?是直接在hello.jar中去找吗?

  还记得java的类加载机制吗?有个叫做双亲委托机制,就比如我们自己定义一个string类为什么没用呢?虽然说编译时可以通过的,但是在运行的时候却会报错,如下所示,为什么提示string类中没有main方法呢,明明我就写了呀!其实是在类加载的时候,首先会把string类交给启动类加载器加载,也就是在jdk中jre/lib目录下去找;没有的话就使用扩展类加载器去加载,也就是在jdk中jre/lib/ext中去加载,最后才是我们用户的类路径下找,默认是当前路径,也可以通过-classpath命令指定用户类路径;

  而string类很明显在启动类路径下rt.jar包中,所以加载当的是官方的string类,当然没有main方法啦!

go实现java虚拟机02

 

  再回到最开始的问题,例如java -classpath hello.jar helloworld这种命令,helloworld这个类在哪里找,现在就很清楚了,现在jdk下jre/lib中找,找不到就到jre/lib/ext中找,还找不到就在-classpath指定的路径中找,下面就用go代码实现一下,文件目录如下,这次的目录是ch02,基于上一篇的ch01实现,classpath是一个目录,cmd.go和main.go是文件

go实现java虚拟机02

 

 一.命令行添加jre路径

  为了可以更好的指定jre路径,我们命令行中添加一个参数-xjre,例如ch2 -xjre “d:\java\jdk8” java.lang.string,如果命令行中没有指定-xjre参数,那么就去你计算机环境变量中获取java_home了,这就不多说了,所以我们要把cmd.go这里结构体做一个修改,以及对应的解析也添加一个,不多说;

go实现java虚拟机02

go实现java虚拟机02

   

  cmd.go

package main

import (
    "flag"
    "fmt"
    "os"
)

//这个结构体用保存命令行输入的参数,例如:.\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object
type cmd struct {
    helpflag    bool
    versionflag bool
    cpoption    string
    xjreoption  string
    class       string
    args        []string
}

//命令行输入      .\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object
func parsecmd() *cmd {
    cmd := &cmd{}
    //这里的意思就是如果解析失败的话,就调用printusage函数
    flag.usage = printusage
    //下面这些在控制台中表示的是-xxx,要匹配上,没有匹配上就报错
    //匹配上了的话,在-xxx后面的都是【表情】属于参数,其中第一个表示的是类名,后面的都是其他的参数
    flag.boolvar(&cmd.helpflag, "help", false, "print help message")
    flag.boolvar(&cmd.versionflag, "version", false, "print version and exit")
    flag.stringvar(&cmd.cpoption, "classpath", "", "classpath")
    //解析jre类路径
    flag.stringvar(&cmd.xjreoption, "xjre", "", "path to jre")
    flag.parse()
    args := flag.args()
    //解析成功的话,那就继续获取后面的参数
    if len(args) > 0 {
        cmd.class = args[0]
        cmd.args = args[1:]
    }
    return cmd
}

//这里传进去的参数,解析错误的话就显示第一个参数的提示信息
func printusage() {
    fmt.printf("usage:%s [-options] class [args]\n", os.args[0])
}

 

 

 二.定义类路径接口

  在classpath目录下定义entry接口,这个接口是找到制指定class文件的入口,这个接口很重要,根据-classpath后面实际上传进去的路径,可以判断应该获取哪个实例去该路径下读取class字节码文件;其中有四种类型的结构体:compositeentry,wildcardentry,zipentry和direntry,这四种结构体都要实现entry接口,我们先别在意这四种是怎么实现的,假设已经实现好了,我们直接拿来用;

package classpath

import (
    "os"
    "strings"
)

//这里存放类路径的分隔符,这里是分号,因为-classpath命令行中后面可以指定多个目录名称,用分号分隔
const pathlistseparator = string(os.pathlistseparator)

type entry interface {
    //这个接口用于寻找和加载class文件
    //classname是类的相对路径,斜线分隔,以.class文件结尾,比如要读取java.lang.object,应该传入java/lang/object.class
    //返回的数据有该class文件的字节数组
    readclass(classname string) ([]byte, entry, error)

    //相当于java中的tostring方法
    string() string
}

//根据参数不同创建不同类型的entry
func newentry(path string) entry {
    //如果有多个类以分号的形式传进来,就实例化compositeentry这个entry
    //例如java -classpath path\to\classes;lib\a.jar;lib\b.jar;lib\c.zip ...这种路径形式
    if strings.contains(path, pathlistseparator) {
        return newcompositeentry(path)
    }

    //传进去的类全路径path字符串是以*号结尾
    //例如java -classpath lib\*...
    if strings.hassuffix(path, "*") {
        return newwildcardentry(path)
    }

    //传进去的类的全路径名是以jar,jar,zip,zip结尾的字符串
    //例如java -classpath hello.jar ...  或者   java -classpath hello.zip ...
    if strings.hassuffix(path, ".jar") || strings.hassuffix(path, ".jar") ||
        strings.hassuffix(path, ".zip") || strings.hassuffix(path, ".zip") {
        return newzipentry(path)
    }

    //这种就是该java文件在当前目录下
    return newdirentry(path)
}

 

三.实现双亲委托机制

  上面是定义好了具体的针对不同路径进行解析的结构体,下面我们就实现双亲委托机制就行了,其实比较容易,大概的逻辑就是:例如命令行输入的是.\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object,那么首先会判断我们提供的jre目录"d:\java\jdk8\jre"是否存在,不存在的话就获取环境变量的jre,反正就是获取jre路径;

  然后就是获取jre下的lib/*和lib/ext/*,将这两个目录分别实例化两个entry实例(其实每一种entry实例就是对每一种不同路径下的文件进行io流读取),分别对应着启动类路径和扩展类路径;最后就是判断有没有提供-classpath参数,没有提供的话就默认当前目录下所有文件对于这用户类路径

package classpath

import (
    "os"
    "path/filepath"
)

//三种类路径对应的entry
type classpath struct {
    //启动类路径
    bootclasspath entry
    //扩展类路径
    extclasspath entry
    //用户自定义的类路径
    userclasspath entry
}

//jreoption这个参数用于读取启动类和扩展类
//cpoption这个参数用于解析用户类
//命令行输入      .\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object
func parse(jreoption string, cpoption string) *classpath {
    cp := &classpath{}
    //解析启动类路径和扩展类路径
    cp.parsebootandclasspath(jreoption)
    cp.parseuserclasspath(cpoption)
    return cp
}

//拼接启动类和扩展类的的路径,然后实例化对应的entry
func (this *classpath) parsebootandclasspath(jreoption string) {
    //这里就是判断这个jre路径是否存在,不存在就在环境变量中获取java_homr变量+jre
    //总之就是想尽办法获取jdk下的jre文件夹全路径
    jredir := getjredir(jreoption)

    //由于找到了jdk下的jre文件夹,那么下一步就是找到启动类和扩展类所在的目录
    //拼接路径:jre/lib/*
    jrelibpath := filepath.join(jredir, "lib", "*")
    this.bootclasspath = newwildcardentry(jrelibpath)

    //拼接路径:jre/lib/ext/*
    jreextpath := filepath.join(jredir, "lib", "ext", "*")
    this.extclasspath = newwildcardentry(jreextpath)
}

//这个函数就是获取正确的jre文件夹,注意jreoption是绝对路径哦
func getjredir(jreoption string) string {
    //传进来的文件路径存在的话,那就返回
    if jreoption != "" && exists(jreoption) {
        return jreoption
    }
    //传进来的路径不存在,那么判断当前路径下是否有jre文件夹
    if exists("./jre") {
        return "./jre"
    }
    //当前路径不存在,当前路径下也没有jre文件夹,那么就直接获取jdk下的jre全路径
    if jh := os.getenv("java_home"); jh != "" {
        return filepath.join(jh, "jre")
    }
    //都没有的话就抛出没有这个文件夹
    panic("can not find jre folder ")

}

//判断一个目录是否存在,存在的话就返回true,不存在就返回false
func exists(jreoption string) bool {
    if _, err := os.stat(jreoption); err != nil {
        if os.isnotexist(err) {
            return false
        }
    }
    return true
}

//加载用户类,如果-classpath的参数为空,那么就默认当前路径为用户类所在的路径
func (this *classpath) parseuserclasspath(cpoption string) {
    if cpoption == "" {
        cpoption = "."
    }
    this.userclasspath = newentry(cpoption)
}

//此方法可以看到实现了双亲委托机制
//在jdk中遍历启动类,扩展类和用户定义的类,这个readclass是个公开方法,在其他包中可以调用
func (this *classpath) readclass(classname string) ([]byte, entry, error) {
    classname = classname + ".class"
    if data, entry, err := this.bootclasspath.readclass(classname); err == nil {
        return data, entry, err
    }
    if data, entry, err := this.extclasspath.readclass(classname); err == nil {
        return data, entry, err
    }
    return this.userclasspath.readclass(classname)
}

func (this *classpath) string() string {
    return this.userclasspath.string()
}

 

四.修改main.go文件

  之前这里startjvm函数就是随便打印了一行数据,现在我们就可以调用上面的parse方法,根据命令行传入的jre和类,根据双亲委托机制在jre(注意,这里指定的jre路径不存在的话就会获取环境变量中的jre)中找指定的类,加载该类的class字节码文件到内存中,然后给打印出来;

package main

import (
    "firstgoprj0114/jvmgo/ch02/classpath"
    "fmt"
    "strings"
)

//命令行输入      .\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object

func main() {
    cmd := parsecmd()
    if cmd.versionflag {
        fmt.println("version 1.0.0")
    } else if cmd.helpflag || cmd.class == "" {
        printusage()
    } else {
        startjvm(cmd)
    }

}

//主要是修改这个函数
func startjvm(cmd *cmd) {
    //传入jdk中的jre全路径和类名,就会去里面lib中去找或者lib/ext中去找对应的类
    //命令行输入      .\ch02.exe -xjre "d:\java\jdk8\jre" java.lang.object
    cp := classpath.parse(cmd.xjreoption, cmd.cpoption)
    fmt.printf("classpath:%v class:%v args:%v\n", cp, cmd.class, cmd.args)
    //将全类名中的.转为/,以目录的形式去读取class文件,例如上面的java.lang.object就变成了java/lang/object
    classname := strings.replace(cmd.class, ".", "/", -1)
    //去读取指定类的时候,会有一个顺序,首先去启动需要的类中尝试去加载,然后再到扩展类目录下去加载,最后就是到用户定义的目录加载
    //其中用户定义的目录,可以有很多中方式,可以指定是.zip方式,也可以是.jar方式
    classdata, _, err := cp.readclass(classname)
    if err != nil {
        fmt.printf("could not find or load mian class %s\n", cmd.class)
        return
    }
    fmt.printf("class data:%v\n", classdata)

}

 

 

5.entry接口的实现类

  为什么这个放到最后再说呢?因为这个我感觉不是最核心的吧,把前面基本的逻辑弄清楚了,然后就是对几种不同路径的文件进行查找然后读取;

  前面说过,我们传进去的-classpath后面的参数可以有很多种,例如:

//对应direntry
java -classpath path\to\service helloworld
//对应wildcardentry
java -classpath path\to\* helloworld
//对应zipentry
java -classpath path\to\lib2.zip helloworld
java -classpath path\to\hello.jar helloworld //由于可以有多个路径,对应compositeentry java -classpath path\to\classes\*;lib\a.jar;lib\b.jar;lib\c.zip helloworld

 

 

  5.1 direntry

  这个是最容易的,该结构体中只是存了一个绝对路径

package classpath

import (
    "io/ioutil"
    "path/filepath"
)

//结构体相当于类,newdirentry相当于构造方法,下面的readclass和string就是实现接口的方法
type direntry struct {
    //这里用于存放绝对路径
    absstring string
}

//返回一个direntry实例
func newdirentry(path string) *direntry {
    //将参数转为绝对路径,如果是在命令行中使用,那么就会非常精确到当前文件父文件+当前文件
    //如果是在编辑器中使用,那么在这里就是当前只会到当前项目路径+文件路径
    absdir, err := filepath.abs(path)
    if err != nil {
        panic(err) //终止程序运行
    }
    return &direntry{absstring: absdir}

}

//direntry实现了entry的readclass方法,拼接class字节码文件的绝对路径,然后用ioutil包中提供的readfile函数去读取
func (self *direntry) readclass(classname string) ([]byte, entry, error) {
    filename := filepath.join(self.absstring, classname)
    data, err := ioutil.readfile(filename)
    return data, self, err
}

//也实现了entry的string方法
func (self *direntry) string() string {
    return self.absstring
}

 

 

  5.2 zipentry

  这个比较容易,因为zip压缩包中可以有多个文件,所以只是遍历,比较文件名就行了

package classpath

import (
    "archive/zip"
    "errors"
    "io/ioutil"
    "path/filepath"
)

//里面也是存了一个绝对路径
type zipentry struct {
    abspath string
}

//构造函数
func newzipentry(path string) *zipentry {
    abs, err := filepath.abs(path)
    if err != nil {
        panic(err)
    }
    return &zipentry{abspath: abs}
}

//从zip包中解析class文件,这里比较关键
func (self *zipentry) readclass(classname string) ([]byte, entry, error) {
    //go中专门有个zip包读取zip类型的文件
    reader, err := zip.openreader(self.abspath)
    if err != nil {
        return nil, nil, err
    }
    //这个关键字后面的方法是在当前readclass方法执行完之后就会执行
    defer reader.close()
    //遍历zip包中的文件名有没有和命令行中提供的一样
    for _, f := range reader.file {
        if f.name == classname {
            rc, err := f.open()
            if err != nil {
                return nil, nil, err
            }
            //defer关键字是用于关闭已打开的文件
            defer rc.close()
            data, err := ioutil.readall(rc)
            if err != nil {
                return nil, nil, err
            }
            return data, self, nil
        }
    }
    return nil, nil, errors.new("class not found:" + classname)

}

//实现接口的string方法
func (self *zipentry) string() string {
    return self.abspath
}

 

 

  5.3 compositeentry

  注意,这是对应多个路径的情况啊!

package classpath

import (
    "errors"
    "strings"
)

//注意,这是一个[]entry类型哦,因为这种entry可以对应的命令行中是多个路径的
//多个路径是用分号分隔的,于是我们就用分号分割成多个路径,每一个都可以实例化一个entry
//我们把实例化的entry都放到这个切片中存着
type compositeentry []entry

func newcompositeentry(pathlist string) compositeentry {
    compositeentry := []entry{}
    for _, path := range strings.split(pathlist, pathlistseparator) {
        entry := newentry(path)
        compositeentry = append(compositeentry, entry)
    }
    return compositeentry

}
//由于有多个entry,我们就遍历一下,调用每一个entry的readclass方法
func (self compositeentry) readclass(classname string) ([]byte, entry, error) {
    for _, entry := range self {
        data, from, err := entry.readclass(classname)
        if err == nil {
            return data, from, nil
        }
    }
    return nil, nil, errors.new("class not found: " + classname)

}
func (self compositeentry) string() string {
    strs := make([]string, len(self))
    for i, entry := range self {
        strs[i] = entry.string()
    }
    return strings.join(strs, pathlistseparator)
}

 

 

  5.4 wildcardentry

  这种对应的是带有通配符*的路径,其实这种也是一种compositeentry;

package classpath

import (
    "os"
    "path/filepath"
    "strings"
)

func newwildcardentry(path string) compositeentry {
    //移除路径最后的*
    basedir := path[:len(path)-1] // remove *
    //其实这种entry就是compositeentry
    compositeentry := compositeentry{}
    //一个函数,下面就是把函数作为参数传递,这种用法还不是很熟悉,不过我感觉就是跟jdk8中传lambda
    //可以作为参数是一样的吧
    walkfn := func(path string, info os.fileinfo, err error) error {
        if err != nil {
            return err
        }
        //跳过子目录,说明带有通配符*下的子目录中的jar包是不能被搜索到的
        if info.isdir() && path != basedir {
            return filepath.skipdir
        }
        //如果是jar包文件,就实例化zipentry,然后添加到compositeentry里面去
        if strings.hassuffix(path, ".jar") || strings.hassuffix(path, ".jar") {
            jarentry := newzipentry(path)
            compositeentry = append(compositeentry, jarentry)
        }
        return nil
    }
    //这个函数就是遍历basedir中所有文件
    filepath.walk(basedir, walkfn)
    return compositeentry
}

 

 

六.测试

  其实这样就差不多了,根据命令行中输入的命令,利用双亲委托机制,在指定的jre(或者环境变量的jre)中找指定的类,没有的话就在用户当前目录中找,找到字节码文件之后就读取该文件,最终目录如下:

go实现java虚拟机02

 

 

  就比如我们要输出jdk8中的object类的字节码文件,我们首先要根据上一篇我们说的方式进行go install一下,就会在workspace下的bin目录下有个ch02.exe可执行文件,也可以不指定-xjre参数,都可以得到相同的结果;

go实现java虚拟机02

 

 

go实现java虚拟机02

 

 

  也可以测试前面自己压缩成的jar包,注意,指定jar包的全路径啊!

go实现java虚拟机02

 

 

  至于上面那些数字什么,这就是字节码文件,每一个字节码文件的格式都是几乎一样的,就是魔数,次版本号,主版本号,线程池大小,线程池等等组成,很容易的!下一篇再说怎么解析这个字节码文件。。。