Go语言实现 JVM(一)命令行工具

零、写在前面

JVM 的学习是每一个致力于 JAVA 语言的程序员一段最特殊的经历,至少说对于博主来说是这样的,有时候总是前脚看了,后脚就忘了。要是自己写一个 JVM,大概就很难忘了吧。带着这样的想法,博主找到一本张秀宏大神编写的《自己动手写 Java 虚拟机》,好了,话不多说,开整。
ps:博主已经把代码托管到了 GitHub 上,下面是地址
https://github.com/Mor1aty/golangJVM

一、准备工作

1、JDK

jdk 安装还是必须的,具体方法这里就不赘述了,博主用的版本是 1.8。
在这里插入图片描述

2、Go

在 Go 语言官网下载 Go 的安装文件,博主使用的版本是 1.5.1。需要注意的是国外的 Go 官网缘分到了才能访问,所以去 Go 语言中文网下载吧。
Go语言实现 JVM(一)命令行工具_第1张图片
下载好 Go 语言之后,在环境变量中配置 GOPATH 变量,设置一下 GO 语言的工作目录。
Go语言实现 JVM(一)命令行工具_第2张图片
最后在 cmd 中输入 go env 命令,如果显示下面类似的输出,就代表成功了。
Go语言实现 JVM(一)命令行工具_第3张图片

3、创建目录结构

先规划好项目的目录结构,下面是博主的目录结构。
Go语言实现 JVM(一)命令行工具_第4张图片
bin:存放生成的 exe 文件。
pkg:go 语言自动生成,不必理会。
resultPic:存放项目结果截图。
src:代码的主要目录结构。
Go语言实现 JVM(一)命令行工具_第5张图片
每一个章节的工程代码,以 ch + 数字的风格命名。好了前期准备就差不多了。

二、java 命令行工具分析

一开始学习 java,很多人都被告诫不要直接使用像 Eclipse、IDEA 等的 IDE 工具,多用一用命令行编译运行对初学者更好。
为什么大家都这么说呢?
因为一段 java 代码的编译运行都是离不开那些命令的。
用最经典的 Hello World 程序为例。

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

如果一个类包含 main 方法,那么我们就把这个类叫做主类。一般这个类就可以用来启动 java 应用程序。
那么 java 虚拟机是怎么知道我们要从哪个类启动应用程序呢?这一点,java 虚拟机规范并没有明确规定。也就是说,是由虚拟机实现自行决定的。比如 Oracle 的 java 虚拟机实现是通过 java 命令来启动的,主类名由命令行参数指定。

java 命令有如下 4 种形式:

java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]

java 命令的具体含义这里就不赘述了,感兴趣的朋友可以去看张大神的书。

三、命令行工具代码构建

好了,经过上面简单的分析,我们大致已经明白要做什么样的东西了。那就开始吧。
下面的代码是根据 java 命令的第一种形式,编写的一个类似的命令行工具。
先在 ch01 目录下创建 cmd.go 文件,用你喜欢的文本编辑器打开,博主使用的是 VSCode,然后在其中定义一个 Cmd 结构体,用来表示命令行选项和参数。下面是代码:

package main

import "flag"
import "fmt"
import "os"

type Cmd struct{
	helpFlag bool	// 参数 help 的标记
	versionFlag bool // 参数 version 的标记
	cpOption string	// 参数 cp 的标记
	class string	// class 类
	args []string	// 参数 string 数组
}

在 Go 语言中和 java 一样内置了许多功能强大的包。我们将用到 fmt,os 和 flag 包。
在 os 包中定义了一个 Args 变量,在其中存放的是传递给命令行的全部参数。我们可以编写相应的处理代码来处理这些参数。不过,Go 语言为我们内置了 flag 的包,这个包可以帮我们处理命令行选项。
继续编写 cmd.go 文件,代码如下:

// 定义一个 parseCmd() 函数,使用 flag 帮我们解析
func parseCmd() *Cmd{
	cmd := &Cmd{}
	
	// 先将 flag.Usage 赋值一个 printUsage 的函数。这个函数由我们手动编写。
	// printUsage() 函数主要是在我们解析选项失败之后返回一些信息
	flag.Usage = printUsage
	// 使用 flag 提供的各种 Var() 函数设置需要解析的选项
	flag.BoolVar(&cmd.helpFlag,"help",false,"print help message")
	flag.BoolVar(&cmd.helpFlag,"?",false,"print help message")
	flag.BoolVar(&cmd.versionFlag,"version",false,"print version and exit")
	flag.StringVar(&cmd.cpOption,"classpath","","classpath")
	flag.StringVar(&cmd.cpOption,"cp","","classpath")
	// 开始解析
	flag.Parse()
	
	// 使用 flag 的 Args() 函数捕获其他的参数
	args := flag.Args()
	if len(args) > 0 {
		// 第一个参数作为类,剩下的参数存到 args 中
		cmd.class = args[0]
		cmd.args = args[1:]
	}
	return cmd
}

接下来编写 printUsage() 函数:

func printUsage(){
	fmt.Printf("Usage: %s [-option] class [args...]\n",os.Args[0])
}

用了简单的几十行代码,我们的命令行工具就编写好了。

四、测试代码构建

在 ch01 目录下创建 main.go 文件,然后输入下面代码:

package main

import "fmt"

func main(){
	// 调用 parseCmd() 函数解析命令行参数
	cmd := parseCmd()
	if cmd.versionFlag{
		// 是 version 
		fmt.Println("version 0.0.1")
	}else if cmd.helpFlag || cmd.class == "" {
		// 是 help/?/""
		printUsage()
	}else{
		// 其他的就调用 startJVM()
		startJVM(cmd)
	}
}

注意,cmd.go 和 main.go 的包名都是 main,因为在 Go 语言中,main 是一个特殊的包,这个包所在的目录会被编译成可执行文件。Go 语言中的代码入口同样是 main() 函数,但不能有任何参数,也不能有返回值。
main() 函数先调用 parseCmd() 函数,得到 cmd 变量,如果用户输入 -version,则输出 “version 0.0.1”,如果输入的是 -help,或者解析出现错误,就调用 printUsage() 函数输出帮助信息,其他情况就调用 startJVM() 函数,好,接下来就编写 startJVM() 函数。

func startJVM(cmd *Cmd){
	fmt.Printf("classpath: %s class:%s args:%v\n",cmd.cpOption,cmd.class,cmd.args)
}

这样我们的测试代码也就结束了,那开始编译执行吧。

五、编译运行

打开 cmd 窗口,cd 到工程的 src 目录下,执行下面的命令。

go install jvmgo\ch01

在这里插入图片描述
出现这样的情况就代表成功了。这个时候就会在与 src 同级的 bin 目录下生成 ch01.exe 文件。
好了,开始测试吧。
Go语言实现 JVM(一)命令行工具_第6张图片

六、小结

这里尽管我们还没有正式开始编写 java 虚拟机,但是打好了一个不错的基础,编写了一个简化版的命令行工具,是一个不错的开始。

你可能感兴趣的:(JVM解析)