零、写在前面
在 Go 语言实现 JVM(一)中,我们已经实现了简单的命令行工具,那这里我们就具体分析一下参数 classpath。
还是以经典的 HelloWorld 为例,我们大致分析一下 HelloWorld 是如何启动的:首先启动 java 虚拟机,然后加载主类,最后调用主类的 main() 方法。
这是代码:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
这个流程中有一个很关键的点必须要指出来:我们都知道一个类是无法独自运行的。而在 HelloWorld 中,加载 HelloWorld 类之前,先要加载他的超类(java.lang.Object)。main() 方法又带有 args 的参数数组,所以虚拟机需要加载 java.lang.String 和 java.lang.String[] 类。又需要把字符串打印到控制台,所以还要加载 java.lang.System 类。
那么虚拟机是从哪里找到这么类的呢?
一、java 类路径分析
实际上,java 虚拟机规范并没有规定虚拟机应该从哪里寻找,所以不同的虚拟机可以使用不同的方式来实现。
Oracle 的 java 虚拟机实现根据类路径(class path)来搜索类,按照搜索的前后顺序,类路径可以分为以下三个部分:
- 启动类路径(bootstrap classpath)
- 扩展类路径(extension classpath)
- 用户类路径(user classpath)
启动类路径默认对应 jre\lib 目录,java 标准库位于这个路径。
扩展类路径默认对应 jre\lib\ext 目录,使用 java 扩展机制的类位于这个路径。
我们自己实现的类,或者使用第三方的类库则属于用户类路径。
我们在命令行中主要设置的是用户类路径,这也是这篇博客主要讨论的。
用户类路径默认值是当前目录,就是 “.”,在配置 java 环境变量时, 可以配置 CLASSPATH 变量来指定用户类路径,不过这样不够“优雅”,博主一般不用。
好一点的办法是在执行 java 命令时,指定选项-classpath/-cp,这个参数的优先级高于 CLASSPATH 环境变量,可以把 CLASSPATH 的值覆盖掉。
最后说一下 -classpath/-cp 选项可以指定的几种形式:
- 可以指定目录,java -cp path\classes
- 可以指定 JAR 文件或 ZIP 文件,java -cp path\classes\a.jar
- 可以指定多个文件或目录,用分割符分开,windows 中的分隔符是“;”,而在类 UNIX 系统中是“:” ,java -cp path\classes\a.zip;b.jar
- 从 java 6 开始,可以使用通配符“*”来指定,java -cp classes\*
二、代码准备
这一部分的代码要以上一个博客的代码为基础的,没有看上一个博客的朋友先去看上一个博客,链接在这里:https://blog.youkuaiyun.com/qq_37362600/article/details/88093712
Go 语言实现 JVM 这个系列的代码都是连续的,强烈不建议有朋友跳着看。
好了,开整。
首先我们在 src\jvmgo 目录下将 ch01 文件夹复制一份,将其命名为 ch02。
我们在 ch02 目录下新建一个 classpath 文件夹,这里存放关于搜索 class 文件的处理代码。 现在的目录结构就是这个样子的:
我们写的 java 虚拟机将使用的 JDK 的启动类路径来寻找和加载 java 标准库中的类,所以需要一种方法来指定 jre 目录的位置。我们选择命令行选项,因此我们要增加一个新的选项 -Xjre。打开 ch02 目录下的 cmd.go,修改 Cmd 结构体,新增加一个字段 XjreOption。下面是代码:
type Cmd struct{
helpFlag bool
versionFlag bool
cpOption string
// 新增字段 XjreOption
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")
// 新增 XjreOption 的解析
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
}
三、实现类路径代码
我们可以这样认为,类路径是一个大的整体,它由启动类路径,扩展类路径和用户类路径三部分组成。三部分路径又由其他更小的部分组成。这样子就很像是组合模式,那好,我们就用组合模式来设计和实现类路径。
a、Entry 接口
先定义一个接口来表示类路径项。在 ch02\classpath 目录下创建 entry.go 文件,在里面定义 Entry 接口,下面是代码:
package classpath
import "os"
import "strings"
// 路径分割符,string() 相当于 Java 中的 toString() 方法
const pathListSeparator = string(os.PathListSeparator)
// Entry 为一个接口
type Entry interface{
// 寻找加载 class 文件
// readClass 方法参数为 string 类型的 className,指的是 class 文件的相对路径,路径间使用 / 分割,文件名后缀为 .class
// 返回值有三个,分别是读取到的字节数据:[]byte,最终定位到 class 文件的 Entry,错误信息 error
readClass(className string) ([]byte,Entry,error)
String() string
}
代码的解释就放在注释里面了,在这里就不进行赘述了。
因为 Go 语言没有构造函数的相关定义,所以我们使用 newEntry() 函数来根据参数创建不同类型的 Entry 实例,下面是代码:
// 生成新的 Entry
func newEntry(path string) Entry {
// 如果路径包含路径分割符,生成 CompositeEntry
if strings.Contains(path,pathListSeparator){
return newCompositeEntry(path)
}
// 如果路径以 * 结尾,生成 WildcardEntry
if strings.HasSuffix(path,"*"){
return newWildcardEntry(path)
}
// 如果路径以 .jar,.JAR,.zip,.ZIP 结尾,生成 ZipEntry
if strings.HasSuffix(path,".jar") || strings.HasSuffix(path,".JAR") ||
strings.HasSuffix(path,".zip") || strings.HasSuffix(path,".ZIP"){
return newZipEntry(path)
}
// 如果不属于上面所有情况,生成 DirEntry
return newDirEntry(path)
}
在 newEntry() 函数中生成了四种类型的 Entry,分别是 DirEntry、ZipEntry、WildcardEntry 和 CompositeEntry。下面我们来实现他们。
b、DirEntry 实现
DirEntry 的实现相对简单一点,它表示目录形式的类路径。在 ch02\classpath\ 目录下创建 entry_dir.go 文件,定义一个 DirEntry 结构体,用来存放目录的绝对路径。下面是代码:
package classpath
import "io/ioutil"
import "path/filepath"
type DirEntry struct{
// 存放目录的绝对路径
absDir string
}
建立 DirEntry 的“构造函数” newDirEntry() 函数,下面是代码:
/*
Go 语言没有专门的构造函数,这里使用 new 函数代替
*/
// 调用参数将 path 转换为绝对路径 absDir,如果转换过程出现错误则调用 panic() 函数终止程序执行,否则建立 DirEntry实例并返回
func newDirEntry(path string) *DirEntry{
absDir, err := filepath.Abs(path)
// nil 零值,初始化时赋的值
if err != nil {
panic(err)
}
return &DirEntry{absDir}
}
然后编写 readClass() 方法:
// readClass 将绝对路径和类名拼成一个完整的路径,然后使用 ioutil 包的 ReadFile 读取,最后返回 data,出现错误就返回 err
func (self *DirEntry) readClass(className string) ([]byte,Entry,error){
fileName := filepath.Join(self.absDir,className)
data, err := ioutil.ReadFile(fileName)
return data, self, err
}
最后是 String() 方法,作用是直接返回绝对目录:
func (self *DirEntry) String() string{
return self.absDir
}
c、ZipEntry 实现
ZipEntry 表示的是后缀名为 .zip 和 .jar 的文件,在 ch02\classpath\ 目录下创建 entry_zip.go 文件,定义一个 ZipEntry 结构体,一样是存放目录的绝对路径。下面是代码:
package classpath
import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"
type ZipEntry struct {
// 存放目录的绝对路径
absPath string
}
“构造函数”和 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 文件,也就是 readClass() 函数的实现,先上代码:
// 打开 zip 文件,读取其中的 .class
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error){
// 打开 zip 文件
r, err := zip.OpenReader(self.absPath)
if err != nil {
return nil, nil, err
}
defer r.Close()
// for range 循环遍历,遍历 zip 中的所有文件,查找与 className 相同的文件
// for _, f := range r.File --> _ 代表返回值不理会,将 r 中的每一个元素的 File 赋值给 f
for _, f := range r.File {
// 如果找到就打开 class 文件,读出内容,并返回回来
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)
}
这个 readClass() 大致可以实现读取 class 文件的功能,只是他在每读取一个 class 文件时,都要调用一次,这无疑是很浪费的一种做法。有兴趣的朋友可以试着改进,然后一定要联系博主哦。
d、CompositeEntry 实现
CompositeEntry 表示对由分隔符组成的类路径的解析。显而易见的,它是由一个个 Entry 组成的。在 ch02\classpath 目录下建立 entry_zip.go 文件,定义 CompositeEntry 结构体,下面是代码:
package classpath
import "errors"
import "strings"
// 正好可以用 Entry 的数组来表示有一个个 Entry 组成的 CompositeEntry
type CompositeEntry []Entry
然后开始建立“构造函数”,
// 将 pathList 按照分隔符分成一个单独的 Entry,存放到 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
}
readClass() 方法的代码相信大家都已经猜到了,依次调用每一次子路径的 readClass() 方法就可以了。
// 依次调用每一个 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)
}
String() 方法同样是调用每一个子路径的 String() 方法,然后把得到的字符串用路径分隔符拼接起来即可,下面是代码:
// 依次调用每一个 Entry 的 String 方法,并将其按照 pathListSeparator 分割符返回一个总的字符串
func (self CompositeEntry) String() string {
strs := make([]string, len(self))
for i, entry := range self {
strs[i] = entry.String()
}
return strings.Join(strs, pathListSeparator)
}
好了,只剩下 WildcardEntry 的实现了。大家在坚持一会儿。
e、WildcardEntry 实现
很明显,WildcardEntry 与 CompositeEntry 是一样的。WildcardEntry 指的是带有通配符的路径,也就是去掉通配符的路径下所有的 class 文件的集合。
在 ch02\classpath 目录下创建 entry_wildcard.go 文件,不需要建立 WildcardEntry 结构体,我们直接使用 CompositeEntry 的结构体。
先定义“构造”函数 newWildcardEntry(),代码如下:
package classpath
import "os"
import "path/filepath"
import "strings"
func newWildcardEntry(path string) CompositeEntry {
baseDir := path[:len(path) - 1] // 删除 * 号,将 java.lang.utils.* 变成 java.lang.utils.
compositeEntry := []Entry{}
walkFn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 如果是一个目录并且 path 与 baseDir 不相等(就是给定的根目录),则用 SkipDir 跳过子目录
if info.IsDir() && path != baseDir {
return filepath.SkipDir
}
// 根据后缀名选出 jar 文件,并添加到 compositeEntry
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR"){
jarEntry := newZipEntry(path)
compositeEntry = append(compositeEntry, jarEntry)
}
return nil
}
// Walk 函数遍历 baseDir,使用 walkFn 创建 ZipEntry。
// walkFn 函数作为第二个参数
filepath.Walk(baseDir, walkFn)
return compositeEntry
}
这里我们使用了函数式编程的特性,因为 filepath 的 Walk() 函数的第二个参数需要的是一个函数,我们就定义了一个 walkFn 变量存储函数。
这个时候我们就将需要的类路径代码全部写出来了,下一次部分就是 Classpath 结构体的实现了,有了原材料还要有菜谱才能做出佳肴的嘛。
四、菜谱的制作(实现 Classpath 结构体)
同样在 ch02\classpath 目录下新建 classpath.go 文件,先录入如下代码:
package classpath
import "os"
import "path/filepath"
type Classpath struct {
// 分别存放的是三种类路径
bootClasspath Entry // 启动类路径
extClasspath Entry // 扩展类路径
userClasspath Entry // 用户类路径
}
我们使用一个 Parse() 函数通过输入的 -Xjre 选项解析启动类路径和扩展类路径,通过 -classpath/-cp 来解析用户类路径,代码如下:
// 使用 -Xjre 选项解析启动类路径和扩展类路径,使用 -classpath/-cp 选项解析用户类路径
func Parse(jreOption, cpOption string) *Classpath {
cp := &Classpath{}
cp.parseBootAndExtClasspath(jreOption)
cp.parseUserClasspath(cpOption)
return cp
}
一步一步来,我们先实现 parseBootAndExtClasspath() 函数:
// 解析 Boot 和 Ext 类路径
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)
}
parseBootAndExtClasspath() 函数首先需要获取到 jre 目录的位置,下面是实现 getJreDir() 的代码:
// 寻找 jre 目录
func getJreDir(jreOption string) string {
// 先使用用户使用的 -Xjre 选项作为 jre 目录
if jreOption != "" && exists(jreOption) {
return jreOption
}
// 没有输入则在当前目录下寻找 jre 目录
if exists("./jre") {
return "./jre"
}
// 在 JAVA_HOME 中寻找 jre 目录
if jh := os.Getenv("JAVA_HOME"); jh != "" {
return filepath.Join(jh, "jre")
}
panic("Can not find jre folder!")
}
getJreDir() 函数中需要实现一个判断目录是否存在 exists() 函数,实现它:
// 判断目录是否存在
func exists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
这几个函数的逻辑都不是很复杂,博主也就不再多解析了。
parseBootAndExtClasspath() 解析启动类路径和扩展类路径的函数以及他需要的函数就已经构建完成了,那么开始构建解析用户类路径的 parseUserClasspath() 函数,代码如下:
// 解析 User 类路径,如果用户没有提供 -classpath/-cp 选项,则使用当前目录作为用户类路径
func (self *Classpath) parseUserClasspath(cpOption string) {
if cpOption == "" {
cpOption = "."
}
self.userClasspath = newEntry(cpOption)
}
这个函数就比上面那个简单一些了。
最后实现 ReadClass() 函数,它依次从启动类路径、扩展类路径和用户类路径中搜索 class 文件,下面是代码:
// ReadClass 方法依次从 bootClasspath,extClasspath,userClasspath 搜索 class 文件
func (self *Classpath) ReadClass(className string) ([]byte, Entry, error) {
// 参数 className 不包含 .class 后缀
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)
}
最最后,实现 String() 函数返回用户类路径的字符串表示,代码贴上:
// String 方法返回用户类路径的字符串
func (self *Classpath) String() string {
return self.userClasspath.String()
}
自此基本上所有的代码就都实现了,那开始测试吧。
五、测试代码构建
在 main.go 文件中导入两个新的包:
package main
import "fmt"
import "strings"
import "jvmgo/ch02/classpath"
main 方法不变,重写 startJVM() 函数:
func startJVM(cmd *Cmd){
// 将 -Xjre 的值与 -cp/-classpath 的参数传入 classpath 的 Parse() 函数,解析三种路径
cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
// 打印路径及 cmd 的 class 和 args 这两个字段的返回
fmt.Printf("classpath: %v class:%v args:%v\n",cp, cmd.class, cmd.args)
// 将 cmd 的 class 字段的 . 转换为 /,-1 代表替换所有
className := strings.Replace(cmd.class, ".", "/", -1)
调用 classpath 的 ReadClass,获得 className 的类的主要数据
classData, _, err := cp.ReadClass(className)
if err != nil {
fmt.Printf("Could not find or load main class %s\n", cmd.class)
return
}
fmt.Printf("class data: %v\n", classData)
}
好了,测试代码构建完毕,开始编译运行吧。
六、编译运行
go install jvm\ch02
走起,没有问题后运行 bin 目录下的 ch02.exe。
尽管只是一些看似杂乱无章的数字,但是这可是实实在在解析了 E:\java\jre 目录下的 java.lang.Object 类。
七、小结
这篇博客实现了 java 虚拟机寻找 class 文件的操作,并且将具体的类转换成了一些数字,下一部分就实现解析 class 文件。
博主在拜读张秀宏大神的《自己动手写Java虚拟机》中,感悟良多,大神用一些巧妙的思想,实现了在博主这样的普通程序员看来极其复杂的功能。张大神为博主打开了一个全新的世界,真真正正的让博主对 JVM 的产生了浓厚的兴趣,而不仅仅是为了在简历上着新墨。