从零用Go打造一个 JVM 虚拟机 路径解析读取字节码(一)

23 篇文章 1 订阅

Java 虚拟机 基础模块

1.类的搜索

  • 不同虚拟机对类的搜索有不同的规范,Oracle的Java虚拟机实现根 据类路径(class path)来搜索类。
存放类的路径有3中类型:
  1. 启动类路径(bootstrap classpath):jre\lib jar标准库 都位于此
  2. 扩展类路径(extension classpath):jre\lib\ext 使用Java扩展机 制的类位于这个路径
  3. 用户类路径(user classpath) 自己实现的类,以及第三方类库则位于 用户类路径 (-Xbootclasspath选项修改启动类路径)

功能一 . 实现基本的 解析命令行的功能

Java 指定类目录
  1. 普通路径 java -classpath “./libs/” main 找/libs/目录下的 main.class
  2. 通配符类型 java -classpath “./libs/*” main 匹配/libs/目录下的main. jar
  3. 多个路径 java -cp .;c:\classes:d:\classes\ main
    window上分号“;” 分隔,linux上是分号“:”分隔。不支持通配符,需要列出所有jar包,用一点“.”代表当前路径。
  4. jar,zip压缩包 java -classpath “./libs/helloworld.jar” main 找到jar包下的main.class

所以 我们需要 对用户指定的 path 进行加载,首先 我们 就需要对 输入的cmd 命令 进行 解析,我们使用GO

如果只是实现 上面一条命令的话 我们用GO 先定义 一个结构体,分别存放 classpath的参数 和 class 的名字。

type Cmd struct {
	CpOption string
	Class string
}

下面 我们 实现一个最easy版本的解析 一条命令

func ParseCmd() *Cmd {

	//结构体空指针
	cmd := &Cmd{}
	//用法 的提示
	flag.Usage = PrintUsage
	//解析 -classpath 后面 跟着的参数  赋值给 &cmd.CpOption 
	
	flag.StringVar(&cmd.CpOption, "classpath", "", "classpath")
	//后边带的参数
	args := flag.Args()
	if len(args) > 0 {
		//类名
		cmd.Class = args[0]}
	return cmd 
}

func main(){
	cmd := ParseCmd()
	if cmd.Class != "" && cmd.CpOption != "" {
		fmt.Printf("收到的参数 classpath: %s classname:%s\n",cmd.CpOption,cmd.Class)
	}

}

在这里插入图片描述
这样 我们就能解析,一条最基本的 除了指定classpath 还能指定Xjre jre的路径,还有 一般 软件都会有 -help 和 -version 打印帮助信息和 版本号.

使用方法 输出提示

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

功能二.实现对类的寻找和加载

上一步 我们 获取到了 类的加载路径 ,对于一个路径 我们需要处理四种不同的 类路径形式,一般情况 我们从cmd 解析,分为4中形式,我们要对四种路径的 Url分别实现读取Class类 文件.
在这里插入图片描述

目标:我们要 根据这四种路径的 不同 形式 写出不同的读取文件的方法。

我们可以把path(目录)本身看作 对象,然后这个对象 有一个ReadClass 方法,我们只需要传入要读取的Classname 即类文件的名称,再String 用来 返回 目录.

//声明结构体
type DirEntry struct {
	// 用来存放path
	absDir string
}
//声明接口
type ClassDirEntry interface {
	readClass(className string) ([]byte, error)
	String() string
}

我们 需要让DirEntry 实现我们定义的接口ClassDirEntry,在Go中 实现接口的方法 就相当于 隐式的实现了这个接口。

上面我们分析了要处理 四种情况,我们一一实现对应的readClass方法.

//不同的 path 处理方式
func newEntry(path string) ClassDirEntry {
	//如果路径包含分隔符 表示有多个文件 
	if strings.Contains(path,pathListSeparator){
		return newCompositeEntry(path)
	}
	//如果路径 后缀名有*匹配模式
	if strings.HasSuffix(path, "*") { return newWildcardEntry(path)
	}
	//如果路径 是jar zip等压缩包形式
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
		return newZipEntry(path)
	}

	return newDirEntry(path)
}
  • 情况1. java -classpath xxx/libs/ 处理这种情况, 我们通过上一个 功能 可以实现截取 文件 xxx/libs/ 部分,有了目录只需要 直接 到目录下寻找读取对应的文件名即可。

type DirEntry struct {
	absDir string
}
//创建绝对路径
func newDirEntry(path string) *DirEntry {
	//转化为绝对路径
	absDir, err := filepath.Abs(path)
	//Go日常操作
	if err != nil {
		panic(err) }
	//新建一个RootClassDirEntry对象
	return &DirEntry{absDir}
}
//读取类名 读取类文件
func (self *DirEntry) readClass(className string) ([]byte, ClassDirEntry, error) {
	//拼接类文件目录 和 类文件名  
	// '/aaa/xxx/' + 'helloworld.class' = '/aaa/xxx/helloworld.class'
	fileName := filepath.Join(self.absDir, className)
	//读取类文件 返回 []byte 字节集
	data, err := ioutil.ReadFile(fileName)
	return data, self, err
}
//返回文件绝对路径
func (self *DirEntry) String() string {
	return  self.absDir
}

总结:上面代码,就是一个很简单根据文件名读取文件的代码,没有难度。

  • 情况2. 如果路径 是 java -classpath ./libs/helloworld.jar main 这种形式 我们需要 读取压缩包 遍历压缩包内 文件名找到匹配的类,和第一种情况类似。
//新建一个zip解析对象  ZipEntry表示ZIP或JAR文件形式的类路径
type ZipEntry struct {
	absPath string
}
func newZipEntry(path string) *ZipEntry{
	//读取压缩包的绝对路径
	absPath, err := filepath.Abs(path)
	if err != nil {
		panic(err) }
	return &ZipEntry{absPath}
}

//从jar zip 压缩包 读取类文件
func (self *ZipEntry)readClass(className string) ([]byte, ClassDirEntry, error) {
	//读取压缩包 得到zip文件对象  
	r, err := zip.OpenReader(self.absPath)
	//Go的错误处理日常
	if err != nil {
		return nil, nil, err
	}
	//函数结束 关闭Reader
	defer r.Close()
	for _,f := range r.File{
		//找到压缩包 中指定的类名
		if f.Name == className {
			//打开找到的的文件 class文件
			rc, err := f.Open()
			//Go的错误处理日常
			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)
}
//返回文件绝对路径
func (self *ZipEntry) String() string {
	return  self.absPath
}

总结:比起第一种情况,多了一步 打开 压缩包对象,然后通过得到 压缩包内的文件名 遍历匹配再读取。

  • 情况3. 如果路径 是 java -classpath xxx/libs/* 假设 是这种形式, 首先 我们会把 目录下 所有 .jar .JAR 文件全部 获取到 将它们转化成多个 ZipEntry对象 以切片存储起来,当要调用 ReadClass 我们只需要 遍历切片,然后 调用ZipEntry对象的ReadClass方法即可。
//定义 []ClassDirEntry类型的别名 将WildcardEntry作为对象 实现接口
type  WildcardEntry []ClassDirEntry

func newWildcardEntry(pathDir string) []ClassDirEntry{
	//截取通用匹配符 /aaa/* 截取掉 *
	baseDir := pathDir[:len(pathDir)-1]
	//多个 类目录对象
	compositeEntry := []ClassDirEntry{}
	//定义一个 函数作为 参数
	walkFn :=func(path string,info os.FileInfo, err error) error {
		if err != nil {return err }
		//跳过文件夹
		if info.IsDir() && path != baseDir {
			return filepath.SkipDir
		}
		//如果是 .jar  或者 .JAR 结尾的文件
		if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
			//创建一个 Zip目录对象
			jarEntry := newZipEntry(path)
			//保存
			compositeEntry = append(compositeEntry, jarEntry)
		}

		return nil
	}
	//遍历 目录下所有 .jar .JAR 文件 生成ZipEntry目录对象 放在切片中返回
	filepath.Walk(baseDir, walkFn)
	return compositeEntry
}
func(self WildcardEntry) readClass(className string)  ([]byte, ClassDirEntry, error) {
	//遍历 ZipEntry目录对象list
	for _,entry := range self{
		//调用ZipEntry目录对象 获取类文件字节码
		data, from, err := entry.readClass(className)
		//如果找到了 类文件
		if err == nil {
			//返回 字节码数据,和对应的 ZipEntry对象
			return data, from, err
		}
	}
	return nil, nil, errors.New("class not found: " + className)
}

总结:这个都区放大 就是 把 多个文件拆解成了 多个更小的ZipEntry 单元的切片作为对象,需要寻找类的时候 在遍历这些 ZipEntry对象 找到了返回即可.

  • 情况4. 如果路径 是 java -classpath xxx/libs/:xxx/jre 这种形式 在linux 中以 “:” 作为分隔符,这个 把多个路径 遍历 拆解成上面3种情况,然后存储 起来,需要读取文件名的时候再遍历这个切片即可。
//为 多个 目录对象组成的切片作起别名 同时也用来当做对象 
type CompositeEntry []ClassDirEntry

func newCompositeEntry(pathList string) CompositeEntry {
	//存放 ClassDirEntry 类目录对象  
	compositeEntry := []ClassDirEntry{}
	//分割路径 
	entrys := strings.Split(pathList,pathListSeparator)
	//遍历生成 ClassDirEntry 对象
	for _,entrydir := range entrys {
		//去判断 path 属于哪其他三种哪一种情况 生成对应的 ClassDirEntry类目录对象
		entry := newEntry(entrydir)
		//存储到切片中 
		compositeEntry = append(compositeEntry, entry);
	}
	return compositeEntry
}

func (self CompositeEntry)readClass(className string) ([]byte, ClassDirEntry, 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 _,entry := range self{
		strs = append(strs,entry.String() )
	}
	return strings.Join(strs, pathListSeparator)
}

总结:上面 实现了 4个 类,都实现了 ClassDirEntry 接口 的 ReadClass 和String 方法。用于 按目录查找 Class类文件。

功能三.实现类加载目录

前面 我们提到了 java 虚拟机默认 会先从 启动路径-》扩展类路径 -》用户类路径 按顺序依次去寻找 加载类。
那么 就会有3个 类目录对象 所以 所以 我们就要定义一个结构体去存放它。

type Classpath struct {
	bootClasspath ClassDirEntry //启动路径
	extClasspath  ClassDirEntry //扩展类路径
	userClasspath ClassDirEntry //用户类路径
}
启动路径

启动路径,其实对应 Jre 目录下lib 也就是 我们运行java 程序 必须可少的基本运行库,我们可以 通过 -Xjre 指定 如果不指定 会在当前路径下寻找jre 如果找不到 就会从我们在装java是配置的JAVA_HOME环境变量 中去寻找。

所以 获取验证环境变量的方法 的逻辑如下:

func getJreDir(jreOption string) string {
	//如果 从cmd  -Xjre 获取到目录 并且存在
	if jreOption != "" && exists(jreOption) {
	//返回目录
		return jreOption 
	}
	//如果 当前路径下 有 jre 返回目录
	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.IsNotExist(err) { return false
} }
return true }
扩展类路径

扩展类 路径一般 在启动路径 的子目录下 jre/lib/ext

func (self *Classpath) parseBootAndExtClasspath(jreOption string) { 
//获取jre 目录
jreDir := getJreDir(jreOption)
// 拼接成jre 的路径
jreLibPath := filepath.Join(jreDir, "lib", "*") 
//加载 所有底下的 jar包
self.bootClasspath = newWildcardEntry(jreLibPath)
// 拼接 扩展类 的路径
jreExtPath := filepath.Join(jreDir, "lib", "ext", "*") 
//加载 所有底下的jar包
self.extClasspath = newWildcardEntry(jreExtPath)
}

用户类路径

用户类 路径 通过前面我们提到的 -classpath 来指定 ,如果没有指定 就默认为当前路径就好

func (self *Classpath) parseUserClasspath(cpOption string) {
	//如果没有指定
	if cpOption == "" {
	// . 作为当前路径
	cpOption = "." 
	}
	//创建 类目录对象
	self.userClasspath = newEntry(cpOption)
}
按照类的加载顺序挨个查找

对于 指定文件类名取查找 我们是按 前面提到的(启动路径-》扩展类路径 -》用户类路径)顺序,没找到 就挨个查找下去。

//根据类名 分别从 bootClasspath,extClasspath,userClasspath 依次加载类目录
func (self *Classpath) ReadClass(className string) ([]byte, ClassDirEntry, 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)
}
初始化类加载 的目录

当然 最后我们要定义一个初始化函数,来作为初始函数,执行后生成一个 Classpath对象。

//jreOption 为启动类目录  cpOption 为 用户指定类目录 从cmd 命令行 中解析获取
func InitClassPath(jreOption, cpOption string) *Classpath {
	cp := &Classpath{}
	//初始化 启动类目录
	cp.parseBootAndExtClasspath(jreOption)
	//初始化 用户类目录
	cp.parseUserClasspath(cpOption)
	return cp
}
其他

当我们 输入java.lang.Object 实际上我们 要把它分割 . 替换 成 java/lang/Object 目录形式。

总结 :所以 整个流程 是这样的

在这里插入图片描述

  1. 首先 解析cmd 命令 -Xjre 系统启动目录 -classpath 用户自定义类目录
  2. 有了 用户指定的 类启动、用户目录 我们需要声明一个Classpath对象 里面存放 了启动路径 、 扩展类路径 、 用户类路径 ,我们把这些路径 转化成了一个 类目录对象 它包含 readClass 和 String 两个 方法 。readClass可以给定类名 返回类的字节码。String 则可返回对象的路径。而 启动路径 获得的方式 依次是 用户指定jre目录 或 当前目录下jre 或 系统环境变量指定jre目录 JAVA_HOME 指定的目录,扩展目录在 启动目录下 位于 jre/lib/ext,用户目录 由用户指定 不指定为默认为当前目录。
  3. 然后 我们要加载 某个 类名时 比如 java.lang.Object 我们先找 启动目录 找不到 再找 扩展目录 再 找不到 找用户目录。

成功输出 java.lang.Object 字节码文件 !!

在这里插入图片描述

— 参考书籍 :自己动手写Java虚拟机 (Java核心技术系列) 作者:张秀宏

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值