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

零、写在前面 JVM 的学习是每一个致力于 JAVA 语言的程序员一段最特殊的经历,至少说对于博主来说是这样的,有时候总是前脚看了,后脚就忘了。要是自己写一个 JVM,大概就很难忘了吧。带着这样的想法,博主找到一本张秀宏大神编写的《自己动手写 Java 虚拟机》,好了,话不多说,开整。
ps:博主已经把代码托管到了 GitHub 上,下面是地址
https://github.com/Mor1aty/golangJVM
一、准备工作 1、JDK jdk 安装还是必须的,具体方法这里就不赘述了,博主用的版本是 1.8。
Go语言实现 JVM(一)命令行工具
文章图片

2、Go 在 Go 语言官网下载 Go 的安装文件,博主使用的版本是 1.5.1。需要注意的是国外的 Go 官网缘分到了才能访问,所以去 Go 语言中文网下载吧。
Go语言实现 JVM(一)命令行工具
文章图片

下载好 Go 语言之后,在环境变量中配置 GOPATH 变量,设置一下 GO 语言的工作目录。
Go语言实现 JVM(一)命令行工具
文章图片

最后在 cmd 中输入 go env 命令,如果显示下面类似的输出,就代表成功了。
Go语言实现 JVM(一)命令行工具
文章图片

3、创建目录结构 先规划好项目的目录结构,下面是博主的目录结构。
Go语言实现 JVM(一)命令行工具
文章图片

bin:存放生成的 exe 文件。
pkg:go 语言自动生成,不必理会。
resultPic:存放项目结果截图。
src:代码的主要目录结构。
Go语言实现 JVM(一)命令行工具
文章图片

每一个章节的工程代码,以 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 mainimport "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 mainimport "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

Go语言实现 JVM(一)命令行工具
文章图片

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

六、小结 【Go语言实现 JVM(一)命令行工具】这里尽管我们还没有正式开始编写 java 虚拟机,但是打好了一个不错的基础,编写了一个简化版的命令行工具,是一个不错的开始。

    推荐阅读