上一篇通过flag包实现了命令行参数的解析,其实就是将输入的参数保存到一个结构体中,上一篇说过的例如java -classpath hello.jar HelloWorld这种命令,那么HelloWorld这个类是怎么找出来的呢?是直接在hello.jar中去找吗?

  还记得java的类加载机制吗?有个叫做双亲委托机制,就比如我们自己定义一个String类为什么没用呢?虽然说编译时可以通过的,但是在运行的时候却会报错,如下所示,为什么提示String类中没有main方法呢,明明我就写了呀!其实是在类加载的时候,首先会把String类交给启动类加载器加载,也就是在jdk中jre/lib目录下去找;没有的话就使用扩展类加载器去加载,也就是在jdk中jre/lib/ext中去加载,最后才是我们用户的类路径下找,默认是当前路径,也可以通过-classpath命令指定用户类路径;

  而String类很明显在启动类路径下rt.jar包中,所以加载当的是官方的String类,当然没有main方法啦!

  再回到最开始的问题,例如java -classpath hello.jar HelloWorld这种命令,HelloWorld这个类在哪里找,现在就很清楚了,现在jdk下jre/lib中找,找不到就到jre/lib/ext中找,还找不到就在-classpath指定的路径中找,下面就用go代码实现一下,文件目录如下,这次的目录是ch02,基于上一篇的ch01实现,classpath是一个目录,cmd.go和main.go是文件

 一.命令行添加jre路径

  为了可以更好的指定jre路径,我们命令行中添加一个参数-Xjre,例如ch2 -Xjre “D:\java\jdk8” java.lang.String,如果命令行中没有指定-Xjre参数,那么就去你计算机环境变量中获取JAVA_HOME了,这就不多说了,所以我们要把cmd.go这里结构体做一个修改,以及对应的解析也添加一个,不多说;

  

  cmd.go

package main

import (
"flag"
"fmt"
"os"
) //这个结构体用保存命令行输入的参数,例如:.\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object
type Cmd struct {
helpFlag bool
versionFlag bool
cpOption string
XjreOption string
class string
args []string
} //命令行输入 .\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object
func parseCmd() *Cmd {
cmd := &Cmd{}
//这里的意思就是如果解析失败的话,就调用printUsage函数
flag.Usage = printUsage
//下面这些在控制台中表示的是-xxx,要匹配上,没有匹配上就报错
//匹配上了的话,在-xxx后面的都是【表情】属于参数,其中第一个表示的是类名,后面的都是其他的参数
flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
//解析jre类路径
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 [-options] class [args]\n", os.Args[0])
}

二.定义类路径接口

  在classpath目录下定义Entry接口,这个接口是找到制指定class文件的入口,这个接口很重要,根据-classpath后面实际上传进去的路径,可以判断应该获取哪个实例去该路径下读取class字节码文件;其中有四种类型的结构体:CompositeEntry,WildcardEntry,ZipEntry和DirEntry,这四种结构体都要实现Entry接口,我们先别在意这四种是怎么实现的,假设已经实现好了,我们直接拿来用;

package classpath

import (
"os"
"strings"
) //这里存放类路径的分隔符,这里是分号,因为-classpath命令行中后面可以指定多个目录名称,用分号分隔
const pathListSeparator = string(os.PathListSeparator) type Entry interface {
//这个接口用于寻找和加载class文件
//className是类的相对路径,斜线分隔,以.class文件结尾,比如要读取java.lang.Object,应该传入java/lang/Object.class
//返回的数据有该class文件的字节数组
readClass(className string) ([]byte, Entry, error) //相当于java中的toString方法
String() string
} //根据参数不同创建不同类型的Entry
func newEntry(path string) Entry {
//如果有多个类以分号的形式传进来,就实例化CompositeEntry这个Entry
//例如java -classpath path\to\classes;lib\a.jar;lib\b.jar;lib\c.zip ...这种路径形式
if strings.Contains(path, pathListSeparator) {
return newCompositeEntry(path)
} //传进去的类全路径path字符串是以*号结尾
//例如java -classpath lib\*...
if strings.HasSuffix(path, "*") {
return newWildcardEntry(path)
} //传进去的类的全路径名是以jar,JAR,zip,ZIP结尾的字符串
//例如java -classpath hello.jar ... 或者 java -classpath hello.zip ...
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
return newZipEntry(path)
} //这种就是该java文件在当前目录下
return newDirEntry(path)
}

三.实现双亲委托机制

  上面是定义好了具体的针对不同路径进行解析的结构体,下面我们就实现双亲委托机制就行了,其实比较容易,大概的逻辑就是:例如命令行输入的是.\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object,那么首先会判断我们提供的jre目录"D:\java\jdk8\jre"是否存在,不存在的话就获取环境变量的jre,反正就是获取jre路径;

  然后就是获取jre下的lib/*和lib/ext/*,将这两个目录分别实例化两个Entry实例(其实每一种Entry实例就是对每一种不同路径下的文件进行io流读取),分别对应着启动类路径和扩展类路径;最后就是判断有没有提供-classpath参数,没有提供的话就默认当前目录下所有文件对于这用户类路径

package classpath

import (
"os"
"path/filepath"
) //三种类路径对应的Entry
type Classpath struct {
//启动类路径
bootClasspath Entry
//扩展类路径
extClasspath Entry
//用户自定义的类路径
userClasspath Entry
} //jreOption这个参数用于读取启动类和扩展类
//cpOption这个参数用于解析用户类
//命令行输入 .\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object
func Parse(jreOption string, cpOption string) *Classpath {
cp := &Classpath{}
//解析启动类路径和扩展类路径
cp.parseBootAndClasspath(jreOption)
cp.parseUserClasspath(cpOption)
return cp
} //拼接启动类和扩展类的的路径,然后实例化对应的Entry
func (this *Classpath) parseBootAndClasspath(jreOption string) {
//这里就是判断这个jre路径是否存在,不存在就在环境变量中获取JAVA_HOMR变量+jre
//总之就是想尽办法获取jdk下的jre文件夹全路径
jreDir := getJreDir(jreOption) //由于找到了jdk下的jre文件夹,那么下一步就是找到启动类和扩展类所在的目录
//拼接路径:jre/lib/*
jreLibPath := filepath.Join(jreDir, "lib", "*")
this.bootClasspath = newWildcardEntry(jreLibPath) //拼接路径:jre/lib/ext/*
jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
this.extClasspath = newWildcardEntry(jreExtPath)
} //这个函数就是获取正确的jre文件夹,注意jreOption是绝对路径哦
func getJreDir(jreOption string) string {
//传进来的文件路径存在的话,那就返回
if jreOption != "" && exists(jreOption) {
return jreOption
}
//传进来的路径不存在,那么判断当前路径下是否有jre文件夹
if exists("./jre") {
return "./jre"
}
//当前路径不存在,当前路径下也没有jre文件夹,那么就直接获取jdk下的jre全路径
if jh := os.Getenv("JAVA_HOME"); jh != "" {
return filepath.Join(jh, "jre")
}
//都没有的话就抛出没有这个文件夹
panic("can not find jre folder ") } //判断一个目录是否存在,存在的话就返回true,不存在就返回false
func exists(jreOption string) bool {
if _, err := os.Stat(jreOption); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
} //加载用户类,如果-classpath的参数为空,那么就默认当前路径为用户类所在的路径
func (this *Classpath) parseUserClasspath(cpOption string) {
if cpOption == "" {
cpOption = "."
}
this.userClasspath = newEntry(cpOption)
} //此方法可以看到实现了双亲委托机制
//在jdk中遍历启动类,扩展类和用户定义的类,这个ReadClass是个公开方法,在其他包中可以调用
func (this *Classpath) ReadClass(className string) ([]byte, Entry, error) {
className = className + ".class"
if data, entry, err := this.bootClasspath.readClass(className); err == nil {
return data, entry, err
}
if data, entry, err := this.extClasspath.readClass(className); err == nil {
return data, entry, err
}
return this.userClasspath.readClass(className)
} func (this *Classpath) String() string {
return this.userClasspath.String()
}

四.修改main.go文件

  之前这里startJVM函数就是随便打印了一行数据,现在我们就可以调用上面的Parse方法,根据命令行传入的jre和类,根据双亲委托机制在jre(注意,这里指定的jre路径不存在的话就会获取环境变量中的jre)中找指定的类,加载该类的class字节码文件到内存中,然后给打印出来;

package main

import (
"firstGoPrj0114/jvmgo/ch02/classpath"
"fmt"
"strings"
) //命令行输入 .\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object func main() {
cmd := parseCmd()
if cmd.versionFlag {
fmt.Println("version 1.0.0")
} else if cmd.helpFlag || cmd.class == "" {
printUsage()
} else {
startJVM(cmd)
} } //主要是修改这个函数
func startJVM(cmd *Cmd) {
//传入jdk中的jre全路径和类名,就会去里面lib中去找或者lib/ext中去找对应的类
//命令行输入 .\ch02.exe -Xjre "D:\java\jdk8\jre" java.lang.Object
cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
fmt.Printf("classpath:%v class:%v args:%v\n", cp, cmd.class, cmd.args)
//将全类名中的.转为/,以目录的形式去读取class文件,例如上面的java.lang.Object就变成了java/lang/Object
className := strings.Replace(cmd.class, ".", "/", -1)
//去读取指定类的时候,会有一个顺序,首先去启动需要的类中尝试去加载,然后再到扩展类目录下去加载,最后就是到用户定义的目录加载
//其中用户定义的目录,可以有很多中方式,可以指定是.zip方式,也可以是.jar方式
classData, _, err := cp.ReadClass(className)
if err != nil {
fmt.Printf("Could not find or load mian class %s\n", cmd.class)
return
}
fmt.Printf("class data:%v\n", classData) }

5.Entry接口的实现类

  为什么这个放到最后再说呢?因为这个我感觉不是最核心的吧,把前面基本的逻辑弄清楚了,然后就是对几种不同路径的文件进行查找然后读取;

  前面说过,我们传进去的-classpath后面的参数可以有很多种,例如:

//对应DirEntry
java -classpath path\to\service HelloWorld
//对应WildcardEntry
java -classpath path\to\* HelloWorld
//对应ZipEntry
java -classpath path\to\lib2.zip HelloWorld
java -classpath path\to\hello.jar HelloWorld
//由于可以有多个路径,对应CompositeEntry
java -classpath path\to\classes\*;lib\a.jar;lib\b.jar;lib\c.zip HelloWorld

  5.1 DirEntry

  这个是最容易的,该结构体中只是存了一个绝对路径

package classpath

import (
"io/ioutil"
"path/filepath"
) //结构体相当于类,newDirEntry相当于构造方法,下面的readClass和String就是实现接口的方法
type DirEntry struct {
//这里用于存放绝对路径
absString string
} //返回一个DirEntry实例
func newDirEntry(path string) *DirEntry {
//将参数转为绝对路径,如果是在命令行中使用,那么就会非常精确到当前文件父文件+当前文件
//如果是在编辑器中使用,那么在这里就是当前只会到当前项目路径+文件路径
absDir, err := filepath.Abs(path)
if err != nil {
panic(err) //终止程序运行
}
return &DirEntry{absString: absDir} } //DirEntry实现了Entry的readClass方法,拼接class字节码文件的绝对路径,然后用ioUtil包中提供的ReadFile函数去读取
func (self *DirEntry) readClass(className string) ([]byte, Entry, error) {
fileName := filepath.Join(self.absString, className)
data, err := ioutil.ReadFile(fileName)
return data, self, err
} //也实现了Entry的String方法
func (self *DirEntry) String() string {
return self.absString
}

  5.2 ZipEntry

  这个比较容易,因为zip压缩包中可以有多个文件,所以只是遍历,比较文件名就行了

package classpath

import (
"archive/zip"
"errors"
"io/ioutil"
"path/filepath"
) //里面也是存了一个绝对路径
type ZipEntry struct {
absPath string
} //构造函数
func newZipEntry(path string) *ZipEntry {
abs, err := filepath.Abs(path)
if err != nil {
panic(err)
}
return &ZipEntry{absPath: abs}
} //从zip包中解析class文件,这里比较关键
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
//go中专门有个zip包读取zip类型的文件
reader, err := zip.OpenReader(self.absPath)
if err != nil {
return nil, nil, err
}
//这个关键字后面的方法是在当前readClass方法执行完之后就会执行
defer reader.Close()
//遍历zip包中的文件名有没有和命令行中提供的一样
for _, f := range reader.File {
if f.Name == className {
rc, err := f.Open()
if err != nil {
return nil, nil, err
}
//defer关键字是用于关闭已打开的文件
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) } //实现接口的String方法
func (self *ZipEntry) String() string {
return self.absPath
}

  5.3 CompositeEntry

  注意,这是对应多个路径的情况啊!

package classpath

import (
"errors"
"strings"
) //注意,这是一个[]Entry类型哦,因为这种Entry可以对应的命令行中是多个路径的
//多个路径是用分号分隔的,于是我们就用分号分割成多个路径,每一个都可以实例化一个Entry
//我们把实例化的Entry都放到这个切片中存着
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 }
//由于有多个Entry,我们就遍历一下,调用每一个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) }
func (self CompositeEntry) String() string {
strs := make([]string, len(self))
for i, entry := range self {
strs[i] = entry.String()
}
return strings.Join(strs, pathListSeparator)
}

  5.4 WildcardEntry

  这种对应的是带有通配符*的路径,其实这种也是一种CompositeEntry;

package classpath

import (
"os"
"path/filepath"
"strings"
) func newWildcardEntry(path string) CompositeEntry {
//移除路径最后的*
baseDir := path[:len(path)-1] // remove *
//其实这种Entry就是CompositeEntry
compositeEntry := CompositeEntry{}
//一个函数,下面就是把函数作为参数传递,这种用法还不是很熟悉,不过我感觉就是跟jdk8中传Lambda
//可以作为参数是一样的吧
walkFn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
//跳过子目录,说明带有通配符*下的子目录中的jar包是不能被搜索到的
if info.IsDir() && path != baseDir {
return filepath.SkipDir
}
//如果是jar包文件,就实例化ZipEntry,然后添加到compositeEntry里面去
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
jarEntry := newZipEntry(path)
compositeEntry = append(compositeEntry, jarEntry)
}
return nil
}
//这个函数就是遍历baseDir中所有文件
filepath.Walk(baseDir, walkFn)
return compositeEntry
}

六.测试

  其实这样就差不多了,根据命令行中输入的命令,利用双亲委托机制,在指定的jre(或者环境变量的jre)中找指定的类,没有的话就在用户当前目录中找,找到字节码文件之后就读取该文件,最终目录如下:

  就比如我们要输出jdk8中的Object类的字节码文件,我们首先要根据上一篇我们说的方式进行go install一下,就会在workspace下的bin目录下有个ch02.exe可执行文件,也可以不指定-Xjre参数,都可以得到相同的结果;

  也可以测试前面自己压缩成的jar包,注意,指定jar包的全路径啊!

  至于上面那些数字什么,这就是字节码文件,每一个字节码文件的格式都是几乎一样的,就是魔数,次版本号,主版本号,线程池大小,线程池等等组成,很容易的!下一篇再说怎么解析这个字节码文件。。。

go实现java虚拟机02的更多相关文章

  1. 带着新人看java虚拟机02

    上一节是把大概的流程给过了一遍,但是还有很多地方没有说到,后续的慢慢会涉及到,敬请期待! 这次我们说说垃圾收集器,又名gc,顾名思义,就是收集垃圾的容器,那什么是垃圾呢?在我们这里指的就是堆中那些没人 ...

  2. Java虚拟机JVM学习02 类的加载概述

    Java虚拟机JVM学习02 类的加载概述 类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对 ...

  3. 《深入理解Java虚拟机》虚拟机性能监控与故障处理工具

    上节学习回顾 从课本章节划分,<垃圾收集器>和<内存分配策略>这两篇随笔同属一章节,主要是从理论+实验的手段来讲解JVM的内存处理机制.好让我们对JVM运行机制有一个良好的概念 ...

  4. Java虚拟机5:Java垃圾回收(GC)机制详解

    哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象.那么如何找到这些对象? 1.引用计数法 这个算法的实现是,给对象中添 ...

  5. Java虚拟机JVM学习01 流程概述

    Java虚拟机JVM学习01 流程概述 Java虚拟机与程序的生命周期 一个运行时的Java虚拟机(JVM)负责运行一个Java程序. 当启动一个Java程序时,一个虚拟机实例诞生:当程序关闭退出,这 ...

  6. 深入理解java虚拟机(5)---字节码执行引擎

    字节码是什么东西? 以下是百度的解释: 字节码(Byte-code)是一种包含执行程序.由一序列 op 代码/数据对组成的二进制文件.字节码是一种中间码,它比机器码更抽象. 它经常被看作是包含一个执行 ...

  7. 《深入Java虚拟机学习笔记》- 第9章 垃圾收集

    一.Java内存组成 组成图 堆(Heap) 运行时数据区域,所有类实例和数组的内存均从此处分配.Java虚拟机启动时创建.对象的堆内存由称为垃圾回收器的自动内存管理系统回收. 组成 组成 详解 Yo ...

  8. java虚拟机学习-JVM内存管理:深入垃圾收集器与内存分配策略(4)

    Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来. 概述: 说起垃圾收集(Garbage Collection,下文简称GC),大部分人都把这项 ...

  9. Java虚拟机15:再谈四种引用状态

    JVM的四种引用状态 在Java虚拟机5:Java垃圾回收(GC)机制详解一文中,有简单提到过JVM的四种引用状态,当时只是简单学习,知道有这么一个概念,对四种引用状态理解不深.这两天重看虚拟机这部分 ...

随机推荐

  1. [bzoj4825] [loj#2018] [Hnoi2017] 单旋

    Description \(H\) 国是一个热爱写代码的国家,那里的人们很小去学校学习写各种各样的数据结构.伸展树(\(splay\))是一种数据 结构,因为代码好写,功能多,效率高,掌握这种数据结构 ...

  2. 在eclipse中用java调用python报错 Exception in thread "main" ImportError: Cannot import site module and its dependencies

    最近做项目需要用java调用python,配置了jython后,运行了例子代码: 获得一个元组里面的元素: import org.python.util.PythonInterpreter; publ ...

  3. 「 从0到1学习微服务SpringCloud 」10 服务网关Zuul

    系列文章(更新ing): 「 从0到1学习微服务SpringCloud 」06 统一配置中心Spring Cloud Config 「 从0到1学习微服务SpringCloud 」07 RabbitM ...

  4. 部署Maven项目到tomcat报错:java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderLi

    Maven项目下update maven后Eclipse报错:java.lang.ClassNotFoundException: ContextLoaderL     严重: Error config ...

  5. 【java面试】线程篇

    1.什么是线程? 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位. 2.线程和进程有什么区别? 线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任 ...

  6. IO系统-基本知识

    注:本文档主要整理了Linux下IO系统的基本知识,是整理的网易云课堂的学习笔记,老师讲得很不错,链接如下:Linux IO系统 1.Linux操作系统的基本构成 内核:操作系统的核心,负责管理系统的 ...

  7. NOI2.5 1817:城堡问题

    描述 1 2 3 4 5 6 7 ############################# 1 # | # | # | | # #####---#####---#---#####---# 2 # # ...

  8. Python中的 if __name__ == '__main__' 是什么意思?

    最近在看Python代码的时候,因为是Python初学者,看到这个if __name__ == '__main__' 的判断,并且下面还有代码语句,看了其他地方的说明,还是没搞明白是什么意思, 在看到 ...

  9. show processlist详解

    摘自:https://blog.csdn.net/sunqingzhong44/article/details/70570728?utm_source=copy 如果您有root权限,您可以看到所有线 ...

  10. artTemplate--模板使用自定义函数(1)

    案例 因为公司业务需要频繁调用接口,后端返回的都是json树对象,需要有些特殊的方法做大量判断和数据处理,显然目前简单语法已经不能满足业务需要了,需要自己定制一些 方法来处理业务逻辑. 例如后台返回的 ...