自己动手写Java虚拟机(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-20 01:39:32

点击下载

作者:张秀宏

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

自己动手写Java虚拟机

自己动手写Java虚拟机试读:

前言

为什么编写本书

Java语言于1995年首次公开发布,很快便取得了巨大的成功,成为使用最为广泛的编程语言之一。到现在,Java已经经历了20多个年头。在这期间,无论是Java语言本身还是Java虚拟机技术,都取[1]得了长足的进步。现如今,Java依然长期占据TIOBE网站的编程语[2]言排行榜首。最近更是被TIOBE选为2015年度编程语言,风采可谓不减当年。

众所周知,Java早已不仅仅是一个单纯的语言,而是一个开放的[3]平台。活跃在这个平台之上的编程语言除了Java之外,还有Groovy、[4][5][6][7]Scala、Clojure、Jython和JRuby等。Java虚拟机则是支持这个平台的基石。

市面上教授Java语言的书籍种类繁多,相比之下,介绍Java虚拟机的书籍却是凤毛麟角。这足以说明Java作为一门高级语言是多么成功(让程序员远离底层),但并不代表Java虚拟机技术不重要。恰恰相反,当Java语言掌握到一定程度时,Java虚拟机原理自然就会成为必须越过的一道鸿沟。

近几年,国内涌现出了一些讨论Java虚拟机技术的优秀书籍,这些书籍主要以分析OpenJDK或Oracle JDK为主。本书另辟蹊径,带领读者自己动手从零开始用Go语言编写Java虚拟机。这样做好处颇多,弥补了OpenJDK等虚拟机的不足。

首先,OpenJDK等虚拟机实现非常复杂。对于初学者而言,很容易陷入代码的海洋和不必要的细节之中。其次,OpenJDK等虚拟机大多用C++语言编写。C++语言非常复杂,理解起来难度很大。最后,单纯阅读代码比较乏味,缺少乐趣,而脱离代码又很难透彻讨论技术。通过自己动手编写代码,很好地避免了上述问题。看着自己实现的Java虚拟机功能逐渐增强,看到可以运行的Java程序越来越复杂,成就感非常强。总之,通过实践的方式,相信读者可以更深刻地领悟Java虚拟机的工作原理。

Go是Google公司于2012年推出的系统编程语言。从到硬件的距离来看,Go语言介于C和Java之间。Go的语法和C类似,但更加简洁,因此很容易学习。Go语言内置了丰富的基本数据类型,并且支持结构体,所以很适合用来实现Java虚拟机。Go支持指针,但并不支持指针运算,因此用Go编写的代码要比C代码更加安全。此外,Go还支持垃圾回收和接口等Java类语言中才有的功能,大大降低了实现Java虚拟机的难度。

以上是本书采用Go语言编写Java虚拟机的原因,希望读者在学习本书的过程中,可以喜欢上Go这门还很年轻的语言。

本书主要内容

全书一共分为11章,各章内容安排如下:

第1章:安装开发环境,讨论java命令,并编写一个类似Java的命令行程序。

第2章:讨论Java虚拟机如何搜索class文件,实现类路径。

第3章:讨论class文件结构,实现class文件解析。

第4章:讨论运行时数据区,实现线程私有的运行时数据区,包括线程、Java虚拟机栈、栈帧、操作数栈和局部变量表等。

第5章:讨论Java虚拟机指令集和解释器,实现解释器和150余条指令。

第6章:讨论类、对象以及线程共享的运行时数据区,实现类加载器、方法区以及部分引用类指令。

第7章:讨论方法调用和返回,实现方法调用和返回指令。

第8章:讨论数组和字符串,实现数组相关指令和字符串池。

第9章:讨论本地方法调用,实现Object.hashCode()等本地方法。

第10章:讨论异常处理机制,实现athrow指令。

第11章:讨论System类的初始化过程和System.out.println()的工作原理等,并对全书进行总结。

本书面向读者

本书主要面向有一定经验的Java程序员,但任何对Java虚拟机工作原理感兴趣的读者都可以从本书获益。如前所述,本书将使用Go语言实现Java虚拟机。书中会简要介绍Go语言的部分语法以及与Java语言的区别,但不会深入讨论。由于Go语言相对比较简单,相信任何有C系列语言(如C、C++、C#、Objective-C、Java等)经验的读者都可以轻松读懂书中的源代码。

如何阅读本书

本书代码经过精心调整,每一章(第1章除外)都建立在前一章的基础上,但每一章又都可以单独编译和运行。本书内容主要围绕代码对Java虚拟机展开讨论。读者可以从第1章开始,按顺序阅读本书并运行每一章的源代码,也可以直接跳到感兴趣的章节阅读,必要时再阅读其他章节。

参考资料

本书主要参考了下面这些资料:

·《Java虚拟机规范》第8版

·《Java语言规范》第8版[8]

·《深入Java虚拟机》(原书第2版)

其中《Java虚拟机规范》主要参考了第8版,但同时也参考了第7版和更老的版本。《Java语言规范》则主要参考了第8版。读者可以从http://docs.oracle.com/javase/specs/index.html获取各个版本的《Java虚拟机规范》和《Java语言规范》。

笔者早在十年前还在上学时就读过由Bill Venners著,曹晓钢等翻译的《深入Java虚拟机(原书第2版)》。但是由于当时水平有限,理解得并不是很深入。时隔十年,重读此书还是颇有收获。较之《Java虚拟机规范》的严谨和刻板,该书更加通俗易懂。原书作者已经将部分章节放于网上,网址是http://www.artima.com/insidejvm/ed2/,读者可以免费阅读。

以上是Java方面的资料。Go语言方面主要参考了Go官网上的各[9][10]种资料,包括《如何编写Go程序》《Effective Go》《Go语言规

[11][12]范》以及Go标准库文档等。另外,在本书的写作过程中,笔者[13]还通过搜索引擎查阅了遍布于网络上(特别是StackOverflow和[14]Wikipedia)的各种资料,这里就不一一罗列了。

下载本书源代码

本书源代码可以从https://github.com/zxh0/jvmgo-book获取。代码分为Go和Java两部分,目录结构如下:https://github.com/zxh0/jvmgo-book/v1/code/ |-go |-src |-jvmgo |-java |-example

Go语言部分是Java虚拟机代码,每章为一个子目录,可以独立编译和运行。Java语言部分是Java示例代码,每章为一个包。Java代[15]码按照Gradle工程标准目录结构组织,可以用Gradle编译整个工程,也可以用javac分别编译每个文件。

勘误和支持

《Java虚拟机规范》对Java虚拟机的工作机制有十分严谨的描述。但是由于笔者水平和表达能力有限,本书一定存在表述不精确、不准确,甚至不正确的地方。另外,由于时间有限,书中也难免会有一些疏漏之处,还请读者谅解。

本书的勘误将通过https://github.com/zxh0/jvmgo-book/blob/master/v1/errata.md发布和更新。如果读者发现书中的错误、有改进意见,或者有任何问题需要讨论,都可以在本书的Github项目上创建Issue。此外也可以加入QQ群(470333113)与读者交流。

致谢

首先要感谢我的家人和朋友,没有你们的鼓励、支持和帮助,本书不可能面世。这里特别感谢我的妻子,在我陷入低谷的时候,叮嘱我继续努力不要放弃。还有我的朋友范森,每章开头的可爱鼹鼠就是出自他手,希望这些鼹鼠能给枯燥的文字增添一些色彩。[16]

其次感谢我所在的公司乐元素,它为我提供了舒适和愉悦的工作环境,使我在工作之余可以全心投入本书的写作之中。

代码被我放到了Github上,地址是https://github.com/zxh0/jvm.go。不过由于能力和时间有限,这个虚拟机离完整实现《Java虚拟机规范》还相距甚远。2015年4月份,我停止了jvm.go的编写,同时开始改造代码,酝酿本书。感谢所有关注过jvm.go项目的人,没有你们的帮助就没有jvm.go,也就没有本书。

最后,感谢机械工业出版社华章分社的编辑,本书能够顺利出版离不开他们的敬业精神和一丝不苟的工作态度。

[1] http://www.tiobe.com/。

[2] Java曾被TIOBE选为2005年度编程语言。

[3] http://www.groovy-lang.org/。

[4] http://www.scala-lang.org/。

[5] http://clojure.org/。

[6] http://www.jython.org/。

[7] http://jruby.org/。

[8] 原书名为《Inside the Java Virtual Machine,Second Edition》。

[9] https://golang.org/doc/code.html。

[10] https://golang.org/doc/effective_go.html。

[11] https://golang.org/ref/spec。

[12] https://golang.org/pkg/。

[13] http://stackoverflow.com/。

[14] https://en.wikipedia.org/。

[15] http://gradle.org/。

[16] http://www.happyelements.cn/。第1章 命令行工具

Java虚拟机非常复杂,要想真正理解它的工作原理,最好的方式就是自己动手写一个。本书的目的就是带领读者按照Java虚拟机规范[1],从零开始,一步一步用Go语言实现一个功能逐步增强的Java虚[2]拟机。第1章将编写一个类似java的命令行工具,用它来启动我们自己的虚拟机。在开始编写代码之前,需要先准备好开发环境。

本书假定读者使用的是Windows操作系统,因此书中出现的命令和路径等都是Windows形式的。如果读者使用的是其他操作系统(如Mac OS X、Linux等),需要根据自己的情况做出相应调整。由于Go和Java都是跨平台语言,所以本书代码在常见的操作系统中都可以正常编译和运行。

[1] 如无特殊说明,本书中出现的“Java虚拟机规范”均指《Java虚拟机规范第8版》,网址为http://docs.oracle.com/javase/specs/jvms/se8/html/index.html。

[2] 后文中,首字母小写的java特指java命令行工具。1.1 准备工作1.1.1 安装JDK

我们都知道,要想运行Java程序,只有Java虚拟机是不够的,还需要有Java类库。Java虚拟机和Java类库一起,构成了Java运行时环境。本书编写的Java虚拟机依赖于JDK类库,另外,编译本书中的[1]Java示例代码也需要JDK。从Oracle网站上下载最新版本(写作本章时是8u66)的JDK安装文件,双击运行即可。安装完毕之后,打开命令行窗口执行java-version命令,如果看到类似图1-1所示的输出,就证明安装成功了。图1-1 java-version命令输出

[1] http://www.oracle.com/technetwork/java/javase/downloads/index.html。1.1.2 安装Go[1]

从Go语言官网下载最新版本(写作本章时是1.5.1)的Go安装文件,双击运行即可。安装完毕之后,打开命令行窗口执行go version命令,如果看到类似图1-2所示的输出,就证明安装成功了。图1-2 go version命令输出[2]

go命令是Go语言提供的命令行工具,用来管理Go源代码。go命令就像瑞士军刀,里面包含了各种小工具。用Go语言编写程序,基本上只需要go命令就可以了。go命令里的小工具是各种子命令,version是其中之一。其他常用的子命令包括help、fmt、install和test等。

go命令行工具希望所有的Go源代码被都放在一个工作空间中。所谓工作空间,实际上就是一个目录结构,这个目录结构包含三个子目录。

·src目录中是Go语言源代码。

·pkg目录中是编译好的包对象文件。

·bin目录中是链接好的可执行文件。

实际上只有src目录是必须要有的,go会自动创建pkg和bin目录。工作空间可以位于任何地方,本书使用D:\go\workspace作为工作空间。那么go如何知道工作空间在哪里呢?答案是通过GOPATH环境变量。在桌面上右键单击“我的电脑”图标,在弹出的菜单中单击“属性”,然后单击“高级系统设置”;在“系统属性”对话框中单击“环境变量”按钮,然后添加GOPATH变量即可,如图1-3所示。图1-3 设置GOPATH环境变量

打开命令行窗口,执行go env命令,如果看到类似图1-4所示的输出,GOPATH环境变量就设置成功了。图1-4 使用go env命令查看GOPATH环境变量

[1] https://golang.org/dl/(如果Go官网无法访问,可以从http://golangtc.com/download)下载。

[2] 后文中,首字母小写的go特指go命令行工具。1.1.3 创建目录结构

Go语言以包为单位组织源代码,包可以嵌套,形成层次关系。本书编写的Go源文件全部放在jvmgo包中,其中每一章的源文件又分别放在自己的子包中。包层次和目录结构有一个简单的对应关系,比如,第1章的代码在jvmgo\ch01目录下。除第1章以外,每一章都是先复制前一章代码,然后进行修改和完善。每一章的代码都是独立的,可以单独编译为一个可执行文件。下面创建第1章的目录结构。

在D:\go\workspace\src(也就是%GOPATH%\src)目录下创建jvmgo目录,在jvmgo目录下创建ch01目录。现在,工作空间的目录结构如下:D:\go\workspace\src |-jvmgo |-ch011.2 java命令

Java虚拟机的工作是运行Java应用程序。和其他类型的应用程序一样,Java应用程序也需要一个入口点,这个入口点就是我们熟知的main()方法。如果一个类包含main()方法,这个类就可以用来启动Java应用程序,我们把这个类叫作主类。最简单的Java程序是只有一个main()方法的类,如著名的HelloWorld程序。public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, world!"); }}

那么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命令传递三组参数:选项、主类名(或者JAR文件名)和main()方法参数。选项由减号(–)开头。通常,第一个非选项参数给出主类的完全限定名(fully qualified class name)。但是如果用户提供了–jar选项,则第一个非选项参数表示JAR文件名,java命令必须从这个JAR文件中寻找主类。javaw命令和java命令几乎一样,唯一的差别在于,javaw命令不显示命令行窗口,因此特别适合用于启动GUI(图形用户界面)应用程序。

选项可以分为两类:标准选项和非标准选项。标准选项比较稳定,不会轻易变动。非标准选项以-X开头,很有可能会在未来的版本中变化。非标准选项中有一部分是高级选项,以-XX开头。表1-1列出了[1]java命令常用的选项及其用途。表1-1 java命令常用选项及其用途

[1] 完整的java命令用法请参考http://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html。1.3 编写命令行工具

开发环境已经准备就绪,java命令也已经介绍完毕,相信读者已经迫不及待想开始写代码了吧!下面根据java命令的第一种用法,自己动手编写一个类似的命令行工具。

先定义一个结构体来表示命令行选项和参数。在ch01目录下创[1][2]建cmd.go文件,用你喜欢的文本编辑器打开它,然后在其中定义Cmd结构体,代码如下:package mainimport "flag"import "fmt"import "os"type Cmd struct { helpFlag bool versionFlag bool cpOption string class string args []string}

在Java语言中,API一般以类库的形式提供。在Go语言中,API则是以包(package)的形式提供。包可以向用户提供常量、变量、结构体以及函数等。Java内置了丰富的类库,Go也同样内置了功能强大的包。本章将用到fmt、os和flag包。

os包定义了一个Args变量,其中存放传递给命令行的全部参数。如果直接处理os.Args变量,需要写很多代码。还好Go语言内置了flag包,这个包可以帮助我们处理命令行选项。有了flag包,我们的工作就简单了很多。继续编辑cmd.go文件,在其中定义parseCmd()函[3]数,代码如下:func parseCmd() *Cmd { cmd := &Cmd{} flag.Usage = printUsage 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() args := flag.Args() if len(args) > 0 { cmd.class = args[0] cmd.args = args[1:] } return cmd}

首先设置flag.Usage变量,把printUsage()函数赋值给它;然后调用flag包提供的各种Var()函数设置需要解析的选项;接着调用Parse()函数解析选项。如果Parse()函数解析失败,它就调用printUsage()函数把命令的用法打印到控制台。printUsage()函数的代码如下:func printUsage() { fmt.Printf("Usage: %s [-options] class [args...]\n", os.Args[0])}

如果解析成功,调用flag.Args()函数可以捕获其他没有被解析的参数。其中第一个参数就是主类名,剩下的是要传递给主类的参数。这样,用了不到40行代码,我们的命令行工具就编写完了。下面来测试它。

[1] Go源文件一般以.go作为后缀,文件名全部小写,多个单词之间用下划线分隔。Go语言规范要求Go源文件必须使用UTF-8编码,详见https://golang.org/ref/spec。

[2] 笔者推荐Sublime2,主页为http://www.sublimetext.com/。

[3] Go语言有函数(Function)和方法(Method)之分,方法调用需要receiver,函数调用则不需要。1.4 测试本章代码

在ch01目录下创建main.go文件,然后输入下面的代码。package mainimport "fmt"func main() { cmd := parseCmd() if cmd.versionFlag { fmt.Println("version 0.0.1") } else if cmd.helpFlag || cmd.class == "" { printUsage() } else { startJVM(cmd) }}

注意,与cmd.go文件一样,main.go文件的包名也是main。在Go语言中,main是一个特殊的包,这个包所在的目录(可以叫作任何名字)会被编译为可执行文件。Go程序的入口也是main()函数,但是不接收任何参数,也不能有返回值。

main()函数先调用ParseCommand()函数解析命令行参数,如果一切正常,则调用startJVM()函数启动Java虚拟机。如果解析出现错误,或者用户输入了-help选项,则调用PrintUsage()函数打印出帮助信息。如果用户输入了-version选项,则输出(一个滥竽充数的)版本信息。因为我们还没有真正开始编写Java虚拟机,所以startJVM()函数暂时只是打印一些信息而已,代码如下:func startJVM(cmd *Cmd) { fmt.Printf("classpath:%s class:%s args:%v\n", cmd.cpOption, cmd.class, cmd.args)}

打开命令行窗口,执行下面的命令编译本章代码。go install jvmgo\ch01

命令执行完毕后,如果没有看到任何输出就证明编译成功了,此时在D:\go\workspace\bin目录下会出现ch01.exe文件。现在,可以用各种参数进行测试。笔者的测试结果如图1-5所示。图1-5 ch01.exe测试结果1.5 本章小结

本章准备好了开发环境,学习了java命令的基本用法,并且编写了一个简化版的命令行工具。虽然还没有正式开始编写Java虚拟机,但是已经打好了坚实的基础。下一章将深入了解-classpath选项,探讨Java虚拟机从哪里寻找class文件,并实现class文件加载功能。第2章 搜索class文件

第1章介绍了java命令的用法以及它如何启动Java应用程序:首先启动Java虚拟机,然后加载主类,最后调用主类的main()方法。但是我们知道,即使是最简单的“Hello,World”程序,也是无法独自运行的,该程序的代码如下:public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, world!"); }}

加载HelloWorld类之前,首先要加载它的超类,也就是java.lang.Object。在调用main()方法之前,因为虚拟机需要准备好参数数组,所以需要加载java.lang.String和java.lang.String[]类。把字符串打印到控制台还需要加载java.lang.System类,等等。那么,Java虚拟机从哪里寻找这些类呢?本章将详细讨论这个问题。2.1 类路径

Java虚拟机规范并没有规定虚拟机应该从哪里寻找类,因此不同的虚拟机实现可以采用不同的方法。Oracle的Java虚拟机实现根据类路径(class path)来搜索类。按照搜索的先后顺序,类路径可以分为以下3个部分:

·启动类路径(bootstrap classpath)

·扩展类路径(extension classpath)

·用户类路径(user classpath)

启动类路径默认对应jre\lib目录,Java标准库(大部分在rt.jar里)位于该路径。扩展类路径默认对应jre\lib\ext目录,使用Java扩展机制的类位于这个路径。我们自己实现的类,以及第三方类库则位于用户类路径。可以通过-Xbootclasspath选项修改启动类路径,不过通常并不需要这样做,所以这里就不详细介绍了。

用户类路径的默认值是当前目录,也就是“.”。可以设置CLASSPATH环境变量来修改用户类路径,但是这样做不够灵活,所以不推荐使用。更好的办法是给java命令传递-classpath(或简写为-cp)选项。-classpath/-cp选项的优先级更高,可以覆盖CLASSPATH环境变量设置。第1章简单介绍过这个选项,这里再详细解释一下。

-classpath/-cp选项既可以指定目录,也可以指定JAR文件或者ZIP文件,如下:java -cp path\to\classes ...java -cp path\to\lib1.jar ...java -cp path\to\lib2.zip ...

还可以同时指定多个目录或文件,用分隔符分开即可。分隔符因操作系统而异。在Windows系统下是分号,在类UNIX(包括Linux、Mac OS X等)系统下是冒号。例如在Windows下:java -cp path\to\classes;lib\a.jar;lib\b.jar;lib\c.zip ...

从Java 6开始,还可以使用通配符(*)指定某个目录下的所有JAR文件,格式如下:java -cp classes;lib\* ...2.2 准备工作

从第2章开始,每章的代码都是建立在前一章的基础之上。把ch01目录复制一份,然后改名为ch02。因为本章要创建的源文件都在classpath包中,所以在ch02目录中创建一个classpath子目录。现在目录结构看起来应该是这样:D:\go\workspace\src |-jvmgo |-ch01 |-ch02 |-classpath |-cmd.go |-main.go

我们的Java虚拟机将使用JDK的启动类路径来寻找和加载Java标准库中的类,因此需要某种方式指定jre目录的位置。命令行选项是个不错的选择,所以增加一个非标准选项-Xjre。打开ch02\cmd.go,修改Cmd结构体,添加XjreOption字段,代码如下:type Cmd struct { helpFlag bool versionFlag bool cpOption string XjreOption string class string args []string}

parseCmd()函数也要相应修改,代码如下:func parseCmd() *Cmd { cmd := &Cmd{} flag.Usage = printUsage 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.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre") flag.Parse() ... //其他代码不变}2.3 实现类路径

可以把类路径想象成一个大的整体,它由启动类路径、扩展类路径和用户类路径三个小路径构成。三个小路径又分别由更小的路径构成。是不是很像组合模式(composite pattern)?没错,本节就套用组合模式来设计和实现类路径。2.3.1 Entry接口

先定义一个接口来表示类路径项。在ch02\classpath目录下创建entry.go文件,在其中定义Entry接口,代码如下:package classpathimport "os"import "strings"const pathListSeparator = string(os.PathListSeparator)type Entry interface { readClass(className string) ([]byte, Entry, error) String() string}func newEntry(path string) Entry {...}

常量pathListSeparator是string类型,存放路径分隔符,后面会用到。Entry接口中有两个方法。readClass()方法负责寻找和加载class文件;String()方法的作用相当于Java中的toString(),用于返回变量的字符串表示。

readClass()方法的参数是class文件的相对路径,路径之间用斜线(/)分隔,文件名有.class后缀。比如要读取java.lang.Object类,传入的参数应该是java/lang/Object.class。返回值是读取到的字节数据、最终定位到class文件的Entry,以及错误信息。Go的函数或方法允许返回多个值,按照惯例,可以使用最后一个返回值作为错误信息。

newEntry()函数根据参数创建不同类型的Entry实例,代码如下:func newEntry(path string) Entry { if strings.Contains(path, pathListSeparator) { return newCompositeEntry(path) } if strings.HasSuffix(path, "*") { return newWildcardEntry(path) } if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") || strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") { return newZipEntry(path) } return newDirEntry(path)}

Entry接口有4个实现,分别是DirEntry、ZipEntry、CompositeEntry和WildcardEntry。下面分别介绍每一种实现。2.3.2 DirEntry

在4种实现中,DirEntry相对简单一些,表示目录形式的类路径。在ch02\classpath目录下创建entry_dir.go文件,在其中定义DirEntry结构体,代码如下:package classpathimport "io/ioutil"import "path/filepath"type DirEntry struct { absDir string}func newDirEntry(path string) *DirEntry {...}func (self *DirEntry) readClass(className string) ([]byte , Entry, error) {...}func (self *DirEntry) String() string {...}

DirEntry只有一个字段,用于存放目录的绝对路径。和Java语言不同,Go结构体不需要显示实现接口,只要方法匹配即可。Go没有专门的构造函数,本书统一使用new开头的函数来创建结构体实例,并把这类函数称为构造函数。newDirEntry()函数的代码如下:func newDirEntry(path string) *DirEntry { absDir, err := filepath.Abs(path) if err != nil { panic(err) } return &DirEntry{absDir}}

newDirEntry()先把参数转换成绝对路径,如果转换过程出现错误,则调用panic()函数终止程序执行,否则创建DirEntry实例并返回。

下面介绍readClass()方法:func (self *DirEntry) readClass(className string) ([]byte, Entry, error) { fileName := filepath.Join(self.absDir, className) data, err := ioutil.ReadFile(fileName) return data, self, err}

readClass()先把目录和class文件名拼成一个完整的路径,然后调用ioutil包提供的ReadFile()函数读取class文件内容,最后返回。String()方法很简单,直接返回目录,代码如下:func (self *DirEntry) String() string { return self.absDir}2.3.3 ZipEntry

ZipEntry表示ZIP或JAR文件形式的类路径。在ch02\classpath目录下创建entry_zip.go文件,在其中定义ZipEntry结构体,代码如下:package classpathimport "archive/zip"import "errors"import "io/ioutil"import "path/filepath"type ZipEntry struct { absPath string}func newZipEntry(path string) *ZipEntry {...}func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {...}func (self *ZipEntry) String() string {...}

absPath字段存放ZIP或JAR文件的绝对路径。构造函数和String()与DirEntry大同小异,就不多解释了,代码如下:func newZipEntry(path string) *ZipEntry { absPath, err := filepath.Abs(path) if err != nil { panic(err) } return &ZipEntry{absPath}}func (self *ZipEntry) String() string { return self.absPath}

下面重点介绍如何从ZIP文件中提取class文件,代码如下:func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) { r, err := zip.OpenReader(self.absPath) if err != nil { return nil, nil, err } defer r.Close() for _, f := range r.File { if f.Name == className { rc, err := f.Open() if err != nil { return nil, nil, err } 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)}

首先打开ZIP文件,如果这一步出错的话,直接返回。然后遍历ZIP压缩包里的文件,看能否找到class文件。如果能找到,则打开class文件,把内容读取出来,并返回。如果找不到,或者出现其他错误,则返回错误信息。有两处使用了defer语句来确保打开的文件得以关闭。readClass()方法每次都要打开和关闭ZIP文件,因此效率不是很高。笔者进行了优化,但鉴于篇幅有限,就不展示具体代码了。感兴趣的读者可以阅读ch02\classpath\entry_zip2.go文件。2.3.4 CompositeEntry

在ch02\classpath目录下创建entry_composite.go文件,在其中定义CompositeEntry结构体,代码如下:package classpathimport "errors"import "strings"type CompositeEntry []Entryfunc newCompositeEntry(pathList string) CompositeEntry {...}func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) {...}func (self CompositeEntry) String() string {...}

如前所述,CompositeEntry由更小的Entry组成,正好可以表示成[]Entry。在Go语言中,数组属于比较低层的数据结构,很少直接使用。大部分情况下,使用更便利的slice类型。构造函数把参数(路径列表)按分隔符分成小路径,然后把每个小路径都转换成具体的Entry实例,代码如下:func newCompositeEntry(pathList string) CompositeEntry { compositeEntry := []Entry{} for _, path := range strings.Split(pathList, pathListSeparator) { entry := newEntry(path) compositeEntry = append(compositeEntry, entry) } return compositeEntry}

相信读者已经想到readClass()方法的代码了:依次调用每一个子路径的readClass()方法,如果成功读取到class数据,返回数据即可;如果收到错误信息,则继续;如果遍历完所有的子路径还没有找到class文件,则返回错误。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)}

String()方法也不复杂。调用每一个子路径的String()方法,然后把得到的字符串用路径分隔符拼接起来即可,代码如下:func (self CompositeEntry) String() string { strs := make([]string, len(self)) for i, entry := range self { strs[i] = entry.String() } return strings.Join(strs, pathListSeparator)}2.3.5 WildcardEntry

WildcardEntry实际上也是CompositeEntry,所以就不再定义新的类型了。在ch02\classpath目录下创建entry_wildcard.go文件,在其中定义newWildcardEntry()函数,代码如下:package classpathimport "os"import "path/filepath"import "strings"func newWildcardEntry(path string) CompositeEntry { baseDir := path[:len(path)-1] // remove * compositeEntry := []Entry{} walkFn := func(path string, info os.FileInfo, err error) error {...} filepath.Walk(baseDir, walkFn) return compositeEntry}

首先把路径末尾的星号去掉,得到baseDir,然后调用filepath包的Walk()函数遍历baseDir创建ZipEntry。Walk()函数的第二个参数也是一个函数,了解函数式编程的读者应该一眼就可以认出这种用法(即函数可作为参数)。walkFn变量的定义如下:walkFn := func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() && path != baseDir { return filepath.SkipDir } if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") { jarEntry := newZipEntry(path) compositeEntry = append(compositeEntry, jarEntry) } return nil}

在walkFn中,根据后缀名选出JAR文件,并且返回SkipDir跳过子目录(通配符类路径不能递归匹配子目录下的JAR文件)。2.3.6 Classpath

Entry接口和4个实现介绍完了,接下来实现Classpath结构体。还是在ch02\classpath目录下创建classpath.go文件,把下面的代码输入进去。package classpathimport "os"import "path/filepath"type Classpath struct { bootClasspath Entry extClasspath Entry userClasspath Entry}func Parse(jreOption, cpOption string) *Classpath {...}func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) {...}func (self *Classpath) String() string {...}

Classpath结构体有三个字段,分别存放三种类路径。Parse()函数使用-Xjre选项解析启动类路径和扩展类路径,使用-classpath/-cp选项解析用户类路径,代码如下:func Parse(jreOption, cpOption string) *Classpath { cp := &Classpath{} cp.parseBootAndExtClasspath(jreOption) cp.parseUserClasspath(cpOption) return cp}

parseBootAndExtClasspath()方法的代码如下:func (self *Classpath) parseBootAndExtClasspath(jreOption string) { jreDir := getJreDir(jreOption) // jre/lib/* jreLibPath := filepath.Join(jreDir, "lib", "*") self.bootClasspath = newWildcardEntry(jreLibPath) // jre/lib/ext/* jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") self.extClasspath = newWildcardEntry(jreExtPath)}

优先使用用户输入的-Xjre选项作为jre目录。如果没有输入该选项,则在当前目录下寻找jre目录。如果找不到,尝试使用JAVA_HOME环境变量。getJreDir()函数的代码如下:func getJreDir(jreOption string) string { if jreOption != "" && exists(jreOption) { return jreOption } if exists("./jre") { return "./jre" } if jh := os.Getenv("JAVA_HOME"); jh != "" { return filepath.Join(jh, "jre") } panic("Can not find jre folder!")}

exists()函数用于判断目录是否存在,代码如下:func exists(path string) bool { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return false } } return true}

parseUserClasspath()方法的代码相对简单一些,如下:func (self *Classpath) parseUserClasspath(cpOption string) { if cpOption == "" { cpOption = "." } self.userClasspath = newEntry(cpOption)}

如果用户没有提供-classpath/-cp选项,则使用当前目录作为用户类路径。ReadClass()方法依次从启动类路径、扩展类路径和用户类路径中搜索class文件,代码如下:func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) { className = className + ".class" if data, entry, err := self.bootClasspath.readClass(className); err == nil { return data, entry, err } if data, entry, err := self.extClasspath.readClass(className); err == nil { return data, entry, err } return self.userClasspath.readClass(className)}

注意,传递给ReadClass()方法的类名不包含“.class”后缀。最后,String()方法返回用户类路径的字符串表示,代码如下:func (self *Classpath) String() string { return self.userClasspath.String()}

至此,整个类路径都实现了,下面我们来测试一下。2.4 测试本章代码

打开ch02/main.go文件,添加两条import语句,代码如下:package mainimport "fmt"import "strings"import "jvmgo/ch02/classpath"func main() {...}func startJVM(cmd *Cmd) {...}

main()函数不用变,重写startJVM()函数,代码如下:func startJVM(cmd *Cmd) { cp := classpath.Parse(cmd.XjreOption, cmd.cpOption) fmt.Printf("classpath:%v class:%v args:%v\n", cp, cmd.class, cmd.args) className := strings.Replace(cmd.class, ".", "/", -1) classData, _, err := cp.ReadClass(className) if err != nil { fmt.Printf("Could not find or load main class %s\n", cmd.class) return }

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载