Java 虚拟机 基础模块
1.类的搜索
- 不同虚拟机对类的搜索有不同的规范,Oracle的Java虚拟机实现根 据类路径(class path)来搜索类。
存放类的路径有3中类型:
- 启动类路径(bootstrap classpath):jre\lib jar标准库 都位于此
- 扩展类路径(extension classpath):jre\lib\ext 使用Java扩展机 制的类位于这个路径
- 用户类路径(user classpath) 自己实现的类,以及第三方类库则位于 用户类路径 (-Xbootclasspath选项修改启动类路径)
功能一 . 实现基本的 解析命令行的功能
Java 指定类目录
- 普通路径 java -classpath “./libs/” main 找/libs/目录下的 main.class
- 通配符类型 java -classpath “./libs/*” main 匹配/libs/目录下的main. jar
- 多个路径 java -cp .;c:\classes:d:\classes\ main
window上分号“;” 分隔,linux上是分号“:”分隔。不支持通配符,需要列出所有jar包,用一点“.”代表当前路径。 - 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 目录形式。
总结 :所以 整个流程 是这样的
- 首先 解析cmd 命令 -Xjre 系统启动目录 -classpath 用户自定义类目录
- 有了 用户指定的 类启动、用户目录 我们需要声明一个Classpath对象 里面存放 了启动路径 、 扩展类路径 、 用户类路径 ,我们把这些路径 转化成了一个 类目录对象 它包含 readClass 和 String 两个 方法 。readClass可以给定类名 返回类的字节码。String 则可返回对象的路径。而 启动路径 获得的方式 依次是 用户指定jre目录 或 当前目录下jre 或 系统环境变量指定jre目录 JAVA_HOME 指定的目录,扩展目录在 启动目录下 位于 jre/lib/ext,用户目录 由用户指定 不指定为默认为当前目录。
- 然后 我们要加载 某个 类名时 比如 java.lang.Object 我们先找 启动目录 找不到 再找 扩展目录 再 找不到 找用户目录。
成功输出 java.lang.Object 字节码文件 !!
— 参考书籍 :自己动手写Java虚拟机 (Java核心技术系列) 作者:张秀宏