自己动手写Java虚拟机(第2章 搜索class文件)

学习Java虚拟机以及go语法,记录下自己的学习过程

  • 2.1 类路径
  • 2.2 准备工作
  • 2.3 实现类路径
    • 2.3.1 Entry接口
    • 2.3.2 DirEntry
    • 2.3.3 ZipEntry
    • 2.3.4 CompositeEntry
    • 2.3.5 WildcardEntry
    • 2.3.6 Classpath
  • 2.4 测试本章代码
  • 2.5 本章小结

2.1 类路径

前面章节有提过,Java虚拟机规范并没有明确规定虚拟机应该从哪里寻找类,因此不同的虚拟机实现可以采用不同的方法。
问题:Java虚拟机如何搜索class文件?
Oracle的Java虚拟机实现根据类路径(class path)来搜索类。按照搜索的先后顺序:

*启动类路径(默认对应jre\lib目录,Java标准库rt.jar里位于该路径。)
*扩展类路径(默认对应jre\lib\ext目录)
*用户类路径(默认是当前目录,也就是“.”)

使用-classpath/-cp选项指定目录,也可以指定ZIP文件或JAR文件,如下:

java -cp path\to\classes …
java -cp path\to\lib1.zip …
java -cp path\to\lib2.jar

2.2 准备工作

复制ch01文件夹重命名为ch02,修改cmd.go文件,代码如下:

package main

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

type Cmd struct {
	helpFlag 	bool
	versionFlag bool
	cpOption 	string
	XjreOption	string
	class 		string
	args		[]string
}

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()
	
	args := flag.Args()
	if len(args) > 0 {
		cmd.class = args[0]
		cmd.args = args[1:]
	}
 	
	return cmd
}

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

注:新增代码 flag.StringVar(&cmd.XjreOption, “Xjre” ,"" ,“path to jre”),其他代码不变

2.3 实现类路径

刚开始已经有了解类路径是由启动类路径、扩展类路径和用户类路径三个小路径构成。

2.3.1 Entry接口

定义类路径项接口,文件名为entry.go,代码如下:

package classpath

import "os"
import "strings"

//os.PathSeparator 路径分隔符(分隔路径元素)
//os.PathListSeparator 路径列表分隔符(分隔多个路径)
const pathListSeparator = string(os.PathListSeparator)

type Entry interface {
	readClass(className string) ([]byte, Entry, error)
	String() string
}

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)
}
  • pathListSeparator常量,存放路径分隔符;os包os.PathListSeparator 路径列表分隔符(分隔多个路径)(os模块遍历文件目录)
  • Entry接口,定义了两个方法,readClass()方法负责寻找和加载class文件;String()方法的作用相当于Java的toString(),用于返回变量的字符串表示;
  • newEntry() 函数,根据参数创建不同类型的Entry实例,Entry接口有4个实现,分别为DirEntry、ZipEntry、CompositeEntry和WildcardEntry。
    Contains 判断字符串 s 中是否包含子串 substr,例如:
func main() {
	s := "Hello,世界!!!!!"
	b := strings.Contains(s, "!!")
	fmt.Println(b) // true
	b = strings.Contains(s, "!?")
	fmt.Println(b) // false
	b = strings.Contains(s, "")
	fmt.Println(b) // true
}

HasSuffix 判断字符串 s 是否以 suffix 结尾,例如:

func main() {
	s := "Hello 世界!"
	b := strings.HasSuffix(s, "世界")
	fmt.Println(b) // false
	b = strings.HasSuffix(s, "世界!")
	fmt.Println(b) // true
}
2.3.2 DirEntry

DirEntry相对简单一些,表示目录形式的类路径。创建文件entry_dir.go,定义DirEntry结构体,代码如下:

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
	absDir string
}

func newDirEntry(path string) *DirEntry {
	absDir, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &DirEntry{absDir}
}

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

func (self *DirEntry) String() string {
	 return self.absDir
}

上述代码作个解释

  • package和import不用再说了
  • 新建结构体DirEntry ,定义absDir字符串,存放dir包的绝对路径
  • 获取绝对路径absDir, err := filepath.Abs(path),测试代码如下:
package main

import "path/filepath"
import "fmt"

func main() {
    //返回所给路径的绝对路径
    path, _ := filepath.Abs("./1.txt");
    fmt.Println(path);
}

测试结果如下:

C:\Go\bin>go install jvmgo\ch02-test

C:\Go\bin>ch02-test.exe
C:\Go\bin\1.txt

打印出来的路径为C:\Go\bin\1.txt

  • 获取完成的路径fileName := filepath.Join(self.absDir, className),将目录和class文件名合成一个完成的路径,代码如下:
package main

import "path/filepath"
import "fmt"

func main() {
    //返回所给路径的绝对路径
    path, _ := filepath.Abs("./1.txt");
	fileName := filepath.Join(path, "Main.class")
    fmt.Println(fileName);
}

测试结果如下:

C:\Go\bin>go install jvmgo\ch02-test

C:\Go\bin>ch02-test.exe
C:\Go\bin\1.txt\Main.class

打印出来的路径为C:\Go\bin\1.txt\Main.class

  • 读取class文件内容data, err := ioutil.ReadFile(fileName),api说明如下:
    // ReadFile 读取文件中的所有数据,返回读取的数据和遇到的错误。
    // 如果读取成功,则 err 返回 nil,而不是 EOF
    func ReadFile(filename string) ([]byte, error)
  • String()方法就很简单了,直接返回目录。
2.3.3 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
}

func newZipEntry(path string) *ZipEntry {
	absPath, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &ZipEntry(absPath)
}

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, err
		}
	}
	
	return nil, nil, errors.New("class not found:" + className)
}

func (self *ZipEntry) String() string {
	return self.absPath
}

  • 上述代码,package,import,newZipEntry()方法和String()在这里不解释了
  • 重点介绍readClass方法,返回有三个值,分别为[]byte,Entry,error。
  • 打开Zip文件,r, err := zip.OpenReader(self.absPath),如果这一步报错,直接返回return nil, nil, err。
  • 关闭Zip文件,defer r.Close()使用defer关键字。
  • 遍历Zip压缩包(for _, f := range r.File)里的文件,看能否找到class文件( if f.Name == className),如果找到了,则打开class文件,并返回。如果找不到或者出现错误,则返回错误信息。
2.3.4 CompositeEntry

在ch02\classpath目录下创建entry_composite.go文件,定义CompositeEntry结构体,代码如下:

package classpath
import "errors"
import "strings"

type 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
}

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)
}

func (self CompositeEntry) String() string {
	strs := make([]string, len(self))
	for i, entry := range self {
		strs[i] = entry.String()
	}
	return strings.Join(strs, pathListSeparator)
}
  • CompositeEntry是由更小的Entry组成,正好可以表示为[]Entry数组。
  • newCompositeEntry()方法就是按分隔符分成小路径,然后把每个小路径转换成具体的Entry实例,关键代码:range strings.Split(pathList, pathListSeparator)
  • readClass()依次调用每一个子路径,如果成功读取到了class数据,返回数据即可;如果返回错误信息,则继续调用;如果遍历完所有的子路径还没有找到class文件,则返回错误信息。
  • String()返回一个字符串使用路径分隔符拼接起来。
2.3.5 WildcardEntry

WildcardEntry(通配符*)实际上也是CompositeEntry,所以就不用定义新的类型。在ch02\classpath目录下创建entry_wildcard.go文件,定义newWildcardEntry(),代码如下:

package classpath

import "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 {
		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
	}
	filepath.Walk(baseDir, walkFn)
	
	return compositeEntry
}
  • 去掉路径末尾的星号(*),获取baseDir,baseDir := path[:len(path) - 1]
  • 调用filepath包的Walk()函数遍历baseDir创建ZipEntry,Walk()函数第二个参数也是一个函数。
  • info.IsDir()检查是否是目录
  • filepath.SkipDirWalk跳过当前目录
  • filepath.Walk(baseDir, walkFn)遍历指定目录
2.3.6 Classpath

Entry接口和对应的4个实现已经介绍了,下面介绍一下Classpath结构体。在ch02\classpath目录下创建classpath.go文件,代码如下:

package classpath

import "os"
import "path/filepath"

type Classpath struct {
	bootClasspath Entry
	extClasspath  Entry
	userClasspath Entry
}

func Parse(jreOption, cpOption string) *Classpath {
	cp := &Classpath{}
	cp.parseBootAndExtClasspath(jreOption)
	cp.parseUserClasspath(cpOption)
	return cp
}

func parseBootAndExtClasspath(jreOption string) {
	jreDir := getJreDir(jreOption)
	
	//jre/lib/*
	jreLibPath := filepath.Join(jreDir, "lib", "*")
	self.bootClasspath = new WildcardEntry(jreLibPath)
	
	//jre/lib/ext/*
	jreLibExtPath := filepath.Join(jreDir, "lib", "ext","*")
	self.extClasspath = new WildcardEntry(jreLibExtPath)
}

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!")
}

func exists(path string) bool {
	if _,err := os.Stat(path); err != nil {
		if os.IsNotExists(err) {
			return false
		}
	}
	return true;
}

func (self *Classpath) parseUserClasspath(cpOption string) {
	if cpOption == "" {
		cpOption = "."
	}
	self.userClasspath = newEntry(cpOption)
}

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)
}

func (self *Classpath) String() string {
	return self.userClasspath.String()
}

  • Classpath结构体有三个字段,分别存放三种类路径。
  • Parse()函数使用-Xjre选项解析启动类路径和扩展类路径,使用-classpath/-cp选项解析用户类路径。
  • parseBootAndExtClasspath()函数解析启动类路径和扩展类路径
  • getJreDir()方法优先使用用户输入的-Xjre选项作为jre目录。如果没有输入该选项,则在当前目录下寻找jre目录。如果找不到,使用JAVA_HOME环境变量。
  • exists()函数用于判断目录是否存在。
  • parseUserClasspath()函数表示用户没有提供选项-classpath/-cp,则使用当前目录作为用户类路径。
  • 来看看readClass()方法,注意类名不包含.class后缀。
  • String()方法返回用户类路径的字符串表示。

2.4 测试本章代码

暂时先看测试代码,来推导一下代码逻辑:

  • 先输入启动类和扩展类Xjre,以及用户类-classpath/-cp;
  • 然后打印出启动类或扩展类classpath以及用户类class;
  • 最后打印出主类信息。

测试类代码如下:

package main

import "fmt"
import "strings"
import "jvmgo/ch02/classpath"

func main()	{
	cmd := parseCmd()
	if (cmd.helpFlag || cmd.class == "") {
		printUsage()
	} else if cmd.versionFlag {
		println("version 1.0")
	} else {
		startJVM(cmd)
	}
}

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)
	println("className==" + 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)
}
  • 首先打印命令行参数,然后读取主类数据,并打印到控制台。目前还是没有启动Java虚拟机,可以将class文件的内容打印出来。代码如下:

C:\Go\bin>ch02.exe -Xjre "C:\Program Files\Java\jdk1.7.0_65\jre" java.lang.Objec
t
classpath:C:\Go\bin class:java.lang.Object args:[]
class data:[202 254 186 190 0 0 0 51 0 79 7 0 50 10 0 1 0 51 10 0 18 0 52 10 0 5
3 0 54 10 0 1 0 55 8 0 56 10 0 18 0 57 10 0 58 0 59 10 0 1 0 60 7 0 61 8 0 62 10
 0 10 0 63 3 0 15 66 63 8 0 64 3 0 7 161 32 10 0 18 0 65 10 0 18 0 66 7 0 67 1 0
 6 60 105 110 105 116 62 1 0 3 40 41 86 1 0 4 67 111 100 101 1 0 15 76 105 110 1
01 78 117 109 98 101 114 84 97 98 108 101 1 0 15 114 101 103 105 115 116 101 114
 78 97 116 105 118 101 115 1 0 8 103 101 116 67 108 97 115 115 1 0 19 40 41 76 1
06 97 118 97 47 108 97 110 103 47 67 108 97 115 115 59 1 0 9 83 105 103 110 97 1
16 117 114 101 1 0 22 40 41 76 106 97 118 97 47 108 97 110 103 47 67 108 97 115
115 60 42 62 59 1 0 8 104 97 115 104 67 111 100 101 1 0 3 40 41 73 1 0 6 101 113
 117 97 108 115 1 0 21 40 76 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99
 116 59 41 90 1 0 13 83 116 97 99 107 77 97 112 84 97 98 108 101 1 0 5 99 108 11
1 110 101 1 0 20 40 41 76 106 97 118 97 47 108 97 110 103 47 79 98 106 101 99 11
6 59 1 0 10 69 120 99 101 112 116 105 111 110 115 7 0 68 1 0 8 116 111 83 116 11
4 105 110 103 1 0 20 40 41 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105
110 103 59 1 0 6 110 111 116 105 102 121 1 0 9 110 111 116 105 102 121 65 108 10
8 1 0 4 119 97 105 116 1 0 4 40 74 41 86 7 0 69 1 0 5 40 74 73 41 86 1 0 8 102 1
05 110 97 108 105 122 101 7 0 70 1 0 8 60 99 108 105 110 105 116 62 1 0 10 83 11
1 117 114 99 101 70 105 108 101 1 0 11 79 98 106 101 99 116 46 106 97 118 97 1 0
 23 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 66 117 105 108 100
 101 114 12 0 19 0 20 12 0 24 0 25 7 0 71 12 0 72 0 38 12 0 73 0 74 1 0 1 64 12
0 28 0 29 7 0 75 12 0 76 0 77 12 0 37 0 38 1 0 34 106 97 118 97 47 108 97 110 10
3 47 73 108 108 101 103 97 108 65 114 103 117 109 101 110 116 69 120 99 101 112
116 105 111 110 1 0 25 116 105 109 101 111 117 116 32 118 97 108 117 101 32 105
115 32 110 101 103 97 116 105 118 101 12 0 19 0 78 1 0 37 110 97 110 111 115 101
 99 111 110 100 32 116 105 109 101 111 117 116 32 118 97 108 117 101 32 111 117
116 32 111 102 32 114 97 110 103 101 12 0 41 0 42 12 0 23 0 20 1 0 16 106 97 118
 97 47 108 97 110 103 47 79 98 106 101 99 116 1 0 36 106 97 118 97 47 108 97 110
 103 47 67 108 111 110 101 78 111 116 83 117 112 112 111 114 116 101 100 69 120
99 101 112 116 105 111 110 1 0 30 106 97 118 97 47 108 97 110 103 47 73 110 116
101 114 114 117 112 116 101 100 69 120 99 101 112 116 105 111 110 1 0 19 106 97
118 97 47 108 97 110 103 47 84 104 114 111 119 97 98 108 101 1 0 15 106 97 118 9
7 47 108 97 110 103 47 67 108 97 115 115 1 0 7 103 101 116 78 97 109 101 1 0 6 9
7 112 112 101 110 100 1 0 45 40 76 106 97 118 97 47 108 97 110 103 47 83 116 114
 105 110 103 59 41 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103
66 117 105 108 100 101 114 59 1 0 17 106 97 118 97 47 108 97 110 103 47 73 110 1
16 101 103 101 114 1 0 11 116 111 72 101 120 83 116 114 105 110 103 1 0 21 40 73
 41 76 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 1 0 21 40 76
 106 97 118 97 47 108 97 110 103 47 83 116 114 105 110 103 59 41 86 0 33 0 18 0
0 0 0 0 0 0 14 0 1 0 19 0 20 0 1 0 21 0 0 0 25 0 0 0 1 0 0 0 1 177 0 0 0 1 0 22
0 0 0 6 0 1 0 0 0 37 1 10 0 23 0 20 0 0 1 17 0 24 0 25 0 1 0 26 0 0 0 2 0 27 1 1
 0 28 0 29 0 0 0 1 0 30 0 31 0 1 0 21 0 0 0 46 0 2 0 2 0 0 0 11 42 43 166 0 7 4
167 0 4 3 172 0 0 0 2 0 22 0 0 0 6 0 1 0 0 0 150 0 32 0 0 0 5 0 2 9 64 1 1 4 0 3
3 0 34 0 1 0 35 0 0 0 4 0 1 0 36 0 1 0 37 0 38 0 1 0 21 0 0 0 60 0 2 0 1 0 0 0 3
6 187 0 1 89 183 0 2 42 182 0 3 182 0 4 182 0 5 18 6 182 0 5 42 182 0 7 184 0 8
182 0 5 182 0 9 176 0 0 0 1 0 22 0 0 0 6 0 1 0 0 0 237 1 17 0 39 0 20 0 0 1 17 0
 40 0 20 0 0 1 17 0 41 0 42 0 1 0 35 0 0 0 4 0 1 0 43 0 17 0 41 0 44 0 2 0 21 0
0 0 127 0 4 0 4 0 0 0 62 31 9 148 156 0 13 187 0 10 89 18 11 183 0 12 191 29 155
 0 9 29 18 13 164 0 13 187 0 10 89 18 14 183 0 12 191 29 18 15 162 0 13 29 153 0
 13 31 9 148 154 0 7 31 10 97 64 42 31 182 0 16 177 0 0 0 2 0 22 0 0 0 34 0 8 0
0 1 192 0 6 1 193 0 16 1 196 0 26 1 197 0 36 1 201 0 52 1 202 0 56 1 205 0 61 1
206 0 32 0 0 0 7 0 5 16 9 9 15 3 0 35 0 0 0 4 0 1 0 43 0 17 0 41 0 20 0 2 0 21 0
 0 0 34 0 3 0 1 0 0 0 6 42 9 182 0 16 177 0 0 0 1 0 22 0 0 0 10 0 2 0 0 1 247 0
5 1 248 0 35 0 0 0 4 0 1 0 43 0 4 0 45 0 20 0 2 0 21 0 0 0 25 0 0 0 1 0 0 0 1 17
7 0 0 0 1 0 22 0 0 0 6 0 1 0 0 2 41 0 35 0 0 0 4 0 1 0 46 0 8 0 47 0 20 0 1 0 21
 0 0 0 32 0 0 0 0 0 0 0 4 184 0 17 177 0 0 0 1 0 22 0 0 0 10 0 2 0 0 0 41 0 3 0
42 0 1 0 48 0 0 0 2 0 49]

2.5 本章小结

本章讨论了Java虚拟机在哪里寻找class文件,对类路径和-classpath命令选项有了一定了解,并且把抽象的类路径概念转变为具体的代码。

自己动手写Java虚拟机【源代码】

https://github.com/zxh0/jvmgo-book

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值