实现java虚拟机_go实现java虚拟机02

上一篇通过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方法啦!

34408639fe8809b389d43883a726010a.png

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

49cdbc45ca6a1128fc03389e773d8cfd.png

一.命令行添加jre路径

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

9f11c7f1b6dc2d79c1e03f852a0d5ac5.png

475b12a2a6b71cc0cf47308d0aa35ae5.png

cmd.go

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

packagemainimport("flag"

"fmt"

"os")//这个结构体用保存命令行输入的参数,例如:.\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object

type Cmd struct {

helpFlag bool

versionFlag bool

cpOption string

XjreOption stringclassstring

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:]

}returncmd

}//这里传进去的参数,解析错误的话就显示第一个参数的提示信息

func printUsage() {

fmt.Printf("Usage:%s [-options] class [args]\n", os.Args[0])

}

View Code

二.定义类路径接口

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

packageclasspathimport("os"

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

const pathListSeparator =string(os.PathListSeparator)

type Entryinterface{//这个接口用于寻找和加载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 ...这种路径形式

ifstrings.Contains(path, pathListSeparator) {returnnewCompositeEntry(path)

}//传进去的类全路径path字符串是以*号结尾//例如java -classpath lib\*...

if strings.HasSuffix(path, "*") {returnnewWildcardEntry(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") {returnnewZipEntry(path)

}//这种就是该java文件在当前目录下

returnnewDirEntry(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参数,没有提供的话就默认当前目录下所有文件对于这用户类路径

packageclasspathimport("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)returncp

}//拼接启动类和扩展类的的路径,然后实例化对应的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) {returnjreOption

}//传进来的路径不存在,那么判断当前路径下是否有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 {ifos.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 {returndata, entry, err

}if data, entry, err := this.extClasspath.readClass(className); err ==nil {returndata, 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字节码文件到内存中,然后给打印出来;

packagemainimport("firstGoPrj0114/jvmgo/ch02/classpath"

"fmt"

"strings")//命令行输入 .\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object

func main() {

cmd :=parseCmd()ifcmd.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

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

packageclasspathimport("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)returndata, self, err

}//也实现了Entry的String方法

func (self *DirEntry) String() string {returnself.absString

}

5.2 ZipEntry

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

packageclasspathimport("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 {returnnil, nil, err

}//这个关键字后面的方法是在当前readClass方法执行完之后就会执行

defer reader.Close()//遍历zip包中的文件名有没有和命令行中提供的一样

for _, f :=range reader.File {if f.Name ==className {

rc, err :=f.Open()if err !=nil {returnnil, nil, err

}//defer关键字是用于关闭已打开的文件

defer rc.Close()

data, err :=ioutil.ReadAll(rc)if err !=nil {returnnil, nil, err

}returndata, self, nil

}

}return nil, nil, errors.New("class not found:" +className)

}//实现接口的String方法

func (self *ZipEntry) String() string {returnself.absPath

}

5.3 CompositeEntry

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

packageclasspathimport("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)

}returncompositeEntry

}//由于有多个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 {returndata, 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()

}returnstrings.Join(strs, pathListSeparator)

}

5.4 WildcardEntry

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

packageclasspathimport("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 {returnerr

}//跳过子目录,说明带有通配符*下的子目录中的jar包是不能被搜索到的

if info.IsDir() && path !=baseDir {returnfilepath.SkipDir

}//如果是jar包文件,就实例化ZipEntry,然后添加到compositeEntry里面去

if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {

jarEntry :=newZipEntry(path)

compositeEntry=append(compositeEntry, jarEntry)

}returnnil

}//这个函数就是遍历baseDir中所有文件

filepath.Walk(baseDir, walkFn)returncompositeEntry

}

六.测试

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

82186c39011f4b86251ceaf8ca4e0968.png

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

45098734ec1886538c274441da6f812d.png

7448ecb9240e4e92f6b043631c16e641.png

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

d0e59f33b915376fa7f1aebccae8d2db.png

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值