手写JAVA虚拟机(三)——搜索class文件并读出内容
查看手写JAVA虚拟机系列可以进我的博客园主页查看。
前面我们介绍了准备工作以及命令行的编写。既然我们的任务实现命令行中的java命令,同时我们知道java命令是将class文件(字节码)转换成机器码,那么我们现在的任务就是读出这个class文件里面的内容。
正文:
java虚拟机规范中是没有规定虚拟机该从哪里找类,也就是找class文件的,而oracle的是根据类路径,也就是classpath来搜索类的。搜索的优先级:启动类路径(bootstrap classpath)>扩展类路径(extension classpath)>用户类路径(user classpath)。
启动类路径(bootstrap classpath):默认为指定的jre\lib目录。
扩展类路径(extension classpath):默认为指定的jre\lib\ext目录。
看一下我们现在的工作目录结构(具体工作目录看我博客首页前面的文章)。


与前一章看起来还是有一些差别的,后面会一一介绍。
类路径的设计我们我们采用组合模式。类路径由启动类路径、扩展类路径和用户类路径组成,这三个路径又由更小的路径构成。
首先定义一个接口来表示类路径。在ch02\classpath目录下创建entry.go文件,在其中定义Entry接口:
package classpath import "os"
import "strings" //存放路径分隔符
const pathListSeparator = string (os.PathListSeparator) //定义Entry接口
type Entry interface{
readClass(className string)([]byte,Entry,error)//查找和加载class文件
String() string//类似于java中toString()函数
} //类似于java的构造函数,根据参数创建不同类型的Entry
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)
}
由newEntry()方法可能会猜到我们对Entry接口有4个实现,分别是DirEntry、ZipEntry、CompositeEntry和WildcardEntry,因此我们在classpath文件夹下面分别建立四个go文件,如图:

其实这4种实现的基本逻辑都是类似的,我们以DirEntry为例详细说明(也就是entry_dir.go文件):
package classpath import "io/ioutil"
import "path/filepath" type DirEntry struct {
absDir string //存放绝对路径
} //用path创建一个DirEntry实例并返回
func newDirEntry(path string) *DirEntry{
//将path转换为绝对路径,如果出错则panic,无错则创建DirEntry实例并返回
absDir,err:=filepath.Abs(path)
if err!=nil{
panic(err)
}
return &DirEntry{absDir}
} //将指定class的内容读出
func (self *DirEntry) readClass(className string) ([]byte,Entry,error){
//讲绝对路径和文件名拼接在一起,并使用ioutil包读取该指定文件内容,返回结果
fileName :=filepath.Join(self.absDir,className)
data,err:=ioutil.ReadFile(fileName)
return data,self,err
} func (self *DirEntry) String() string{
return self.absDir
}
首先引入了两个包,io/ioutil之前介绍过,类似于C的输入输出流,path/filepath用于对路径进行处理。然后定义了DirEntry这个结构体,里面只有一个absDir字段,类型为string,这个字段是用来存储绝对路径的。
再往下是三个函数。第一个newDirEntry(path string) *DirEntry,由于go语言中没有像java那样自带构造函数,所以为了方便,对于这些结构体我们都会自己写一个“构造函数”。传入路径值path,通过filepath包的Abs方法来处理并返回一个绝对路径和err信息。java中函数只支持不返回值void和返回一个值int、boolean等,go中支持返回多个值,像这里的absDir和err。如果err为nil(即空),则返回一个包含绝对路径的DirEntry实例,err不为空,则返回错误信息,panic类似于java中的throw。
第二个是readClass方法。先利用filepath包中的Join方法拼接绝对路径和类名,获取fileName为文件名。然后通过ioutil包中的ReadFile方法来读取fileName对应文件中的内容,返回data,self(指该DirEntry实例),err。
第三个String方法返回绝对路径值。
下一个实现ZipEntry(也就是entry_zip.go文件):
package classpath import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath" type ZipEntry struct {
//存放绝对路径
absPath string
} //创建一个ZipEntry实例
func newZipEntry(path string) *ZipEntry {
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
return &ZipEntry{absPath}
} //从zip文件中提取class文件
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
//利用archive/zip包打开zip文件,出错则返回错误
r, err := zip.OpenReader(self.absPath)
if err != nil {
return nil, nil, err
} //defer保证在return前执行
defer r.Close()
//遍历指定路径中的File
for _, f := range r.File {
//如果找到与className相同的文件,读出rc(为ReadCloser接口提供读取文件内容的方法),如果出错,返回错误信息
if f.Name == className {
rc, err := f.Open()
if err != nil {
return nil, nil, err
}
//defer保证在return前执行,即保证关闭
defer rc.Close()
//通过rc读出其中内容为data,返回
data, err := ioutil.ReadAll(rc)
if err != nil {
return nil, nil, err
}
return data, self, nil
}
}
//遍历完成,没有找到对应的文件,返回class not found信息
return nil, nil, errors.New("class not found: " + className)
} func (self *ZipEntry) String() string {
return self.absPath
}
这个实现稍微复杂一点。先引入4个包,然后声明ZipEntry结构体。再往后依次是三个方法,构造方法,readClass方法,String方法。构造方法和String方法与上面DirEntry类似,下面说一下这里的readClass方法。
首先使用archive/zip包来打开这个绝对路径,出错则返回。这里有一个defer r.close(),这个defer类似于java里面的finally,保证在return前执行,也就是说即使这里出现err需要return,也会先执行r.close()再return。如果没有err则继续,for循环遍历这个zip下的file,如果找到文件名与给定的文件名相同的,就打开这个文件,打开之后利用ioutil包中的ReadAll读取其中的内容,返回data。如果出错,则进行相应的处理。
下面直接给出entry_composite.go和entry_wildcard.go代码:
package classpath import "errors"
import "strings" type CompositeEntry []Entry //创建一个CompositeEntry实例
func newCompositeEntry(pathList string) CompositeEntry {
compositeEntry := []Entry{}
//将传入的pathList按分隔符分成小路径
for _, path := range strings.Split(pathList, pathListSeparator) {
entry := newEntry(path)
compositeEntry = append(compositeEntry, entry)
}
return compositeEntry
} //遍历并调用每个子路径的readClass方法,读取class数据并返回
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,再拼接返回
func (self CompositeEntry) String() string {
strs := make([]string, len(self))
for i, entry := range self {
strs[i] = entry.String()
} return strings.Join(strs, pathListSeparator)
}
package classpath import "os"
import "path/filepath"
import "strings" //类似CompositeEntry,不定义新的类型 //创建一个WildcardEntry实例
func newWildcardEntry(path string) CompositeEntry {
//删除末尾*
baseDir := path[:len(path)-]
compositeEntry := []Entry{}
//根据后缀名选出jar文件,并跳过子目录
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
}
//遍历baseDir创建ZipEntry
filepath.Walk(baseDir, walkFn) return compositeEntry
}
这样我们的接口和4个实现就介绍完了,这仅仅是找到了文件并打开了文件,我们还需要用jre“翻译”这些内容,jre在哪呢?对,就是在我们一开始说的classpath中,现在就是要设计classpath结构。
前面说了classpath有三个路径:启动类路径(bootstrap classpath)>扩展类路径(extension classpath)>用户类路径(user classpath)。
在classpath文件夹下新建一个classpath.go文件:
package classpath import "os"
import "path/filepath" type Classpath struct {
bootClasspath Entry //启动类路径,默认为jre/lib目录
extClasspath Entry//扩展类路径,默认为jre/lib/ext
userClasspath Entry//用户类路径
} //解析启动类路径和扩展类路径
func Parse(jreOption, cpOption string) *Classpath {
cp := &Classpath{}
cp.parseBootAndExtClasspath(jreOption)//找启动类路径和扩展类路径
cp.parseUserClasspath(cpOption)//找用户类路径
return cp
} 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)
} //找jre目录
func getJreDir(jreOption string) string {
//找用户输入的路径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
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
} //用户未输入-classpath/-cp参数,默认使用当前目录作为用户类路径
func (self *Classpath) parseUserClasspath(cpOption string) {
if cpOption == "" {
cpOption = "."
}
self.userClasspath = newEntry(cpOption)
} //依次从启动类路径、扩展类路径和用户类路径中搜索class文件
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结构体,该结构体中有三个字段,分别对应启动类路径(bootstrap classpath)、扩展类路径(extension classpath)、用户类路径(user classpath)。然后是方法,主要的方法有三个,Parse、ReadClass、String。
Parse函数使用-Xjre选项解析启动类路径和扩展类路径,用-classpath/-cp选项解析用户类路径。getJreDir方法,在这里我们定义优先使用-Xjre作为jre目录,然后是当前目录下找jre,如果都没有才去我们的环境变量JAVA_HOME里面找。exists()用于判断目录是否存在。
ReadClass方法就是依次从启动类路径、扩展类路径和用户类路径中搜索class文件。String返回用户路径字符串。
工具完成,来修改一下main函数(即main.go文件),标红的地方为与上一章不同的地方:
package main import "fmt"
import "strings"
import "jvmgo/ch02/classpath" func main() {
cmd:=parseCmd()
if cmd.versionFlag{
fmt.Println("version 0.0.1")
}else if cmd.helpFlag||cmd.class==""{
printUsage()
}else{
stratJVM(cmd)
} } func stratJVM(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)
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)
}
红色的部分:首先是Parse解析-Xjre和-cp,然后打印出命令行参数。className为从命令行获取的类名,通过ReadClass方法读取出里面的内容classData,如果无err则打印出classData。
附上还需要的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)>{
cmd.class=args[]
cmd.args=args[:]
} return cmd } func printUsage() {
fmt.Printf("Usage:%s[-options] class [args...]\n",os.Args[])
}
到这里,搜索class文件并读出内容就完成了,现在来测试一下。
打开一个命令行,输入go install jvmgo\ch02:

表示go程序编译成功。会在工作空间的bin下出现ch02.exe,在bin目录下打开命令行,输入ch02 -Xjre "" java.lang.Object:

由于我们没有输入-Xjre路径,这样会自动找到我们环境变量JAVA_HOME目录,用其中的jre来解析Object类并显示。
我们得到了输入,现在我们要证明这个就是我们要的输出。这里解析的是Object类,我们要找到这个类的class文件。在我们在JAVA_HOME环境变量里面找jre/lib,目录下面有一个rt.jar,如图:

解压这个jar(有可能解压不了,那就复制到其他盘解压),解压之后打开:

可以在lang下面找到Object.class,正好对应我们上面命令行里面输入的java.lang.Object。用记事本或者sublime打开这个class:

可是这个跟我们打印出来的也不一样啊:

因为我们解析出来的是10进制,而直接打开的class里面是16进制,因此我们需要转换一下。这里给出一个我写的java转换程序,将打开的class文件的内容复制到D:\yff.txt,然后运行:
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader; public class Test {
public static String txt2String(File file){
StringBuilder result = new StringBuilder();
try{
BufferedReader br = new BufferedReader(new FileReader(file));//构造一个BufferedReader类来读取文件
String s = null;
while((s = br.readLine())!=null){//使用readLine方法,一次读一行
result.append(System.lineSeparator()+s);
}
br.close();
}catch(Exception e){
e.printStackTrace();
}
return result.toString();
}
public static int hixTo(StringBuffer sb){
int sum=0;
if(sb.charAt(0)>=48&&sb.charAt(0)<=57){
sum+=(sb.charAt(0)-48)*16;
}else{
sum+=((sb.charAt(0)-96)+9)*16;
}
if(sb.charAt(1)>=48&&sb.charAt(1)<=57){
sum+=(sb.charAt(1)-48);
}else{
sum+=((sb.charAt(1)-96)+9);
}
return sum;
}
public static void main(String[] arts){
File file = new File("D:\\yff.txt");
String str=txt2String(file);
StringBuffer sbBefore=new StringBuffer(str);
StringBuffer sbAfter=new StringBuffer();
for(int i=0;i<sbBefore.length();i++){
if((sbBefore.charAt(i)>=48&&sbBefore.charAt(i)<=57)||(sbBefore.charAt(i)>=97&&sbBefore.charAt(i)<=122)){
//System.out.print(sbBefore.charAt(i));
sbAfter.append(sbBefore.charAt(i));
}
}
System.out.println(sbAfter);
System.out.println();
for(int i=0;i<sbAfter.length();i=i+2){
System.out.print(hixTo(new StringBuffer(""+sbAfter.charAt(i)+sbAfter.charAt(i+1)))+" ");
if(i!=0&&i%100==0)
System.out.println();
}
}
}
运行结果如图:
对比红框中的内容发现我们通过命令行导出的class内容是正确的。
至此整个搜索class文件并读出文件的内容就完成了。
手写JAVA虚拟机(三)——搜索class文件并读出内容的更多相关文章
- 手写JAVA虚拟机(二)——实现java命令行
查看手写JAVA虚拟机系列可以进我的博客园主页查看. 我们知道,我们编译.java并运行.class文件时,需要一些java命令,如最简单的helloworld程序. 这里的程序最好不要加包名,因为加 ...
- Java虚拟机之搜索class文件
Java命令 Java虚拟机的工作是运行Java应用程序.和其他类型的应用程序一样,Java应用程序也需要一个入口点,这个入口点就是我们熟知的main()方法.如果一个类包含main()方法,这个类就 ...
- 手写java虚拟机(一)——搭建环境
毕业设计打算做一个java虚拟机,首先要对java虚拟机有一个简单的了解(jvm).目前市面上有众多的jvm,如sun公司的HotSpot VM.Classic VM,IBM公司的J9 VM等等,这里 ...
- [JVM] - 一份<自己动手写Java虚拟机>的测试版
go语言下载 配置GOROOT(一般是自动的),配置GOPATH(如果想自己改的话) 参照<自己动手写Java虚拟机> > 第一章 指令集和解释器 生成了ch01.exe文件 这里还 ...
- opencv 手写选择题阅卷 (三)训练分类器
opencv 手写选择题阅卷 (三)训练分类器 1,分类器选择:SVM 本来一开始用的KNN分类器,但这个分类器目前没有实现保存训练数据的功能,所以选择了SVN分类器; 2,样本图像的预处理和特征提取 ...
- 6 手写Java LinkedHashMap 核心源码
概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...
- 3 手写Java HashMap核心源码
手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...
- TensorFlow 入门之手写识别CNN 三
TensorFlow 入门之手写识别CNN 三 MNIST 卷积神经网络 Fly 多层卷积网络 多层卷积网络的基本理论 构建一个多层卷积网络 权值初始化 卷积和池化 第一层卷积 第二层卷积 密集层连接 ...
- Java虚拟机三:OutOfMemoryError异常分析
根据Java虚拟机规范,虚拟机内存中除过程序计数器之外的运行时数据区域都会发生OutOfMemoryError(OOM),本文将通过实际例子验证分析各个数据区域OOM的情况.为了更贴近生产,本次所有例 ...
随机推荐
- ELK学习总结(2-4)bulk 批量操作-实现多个文档的创建、索引、更新和删除
bulk 批量操作-实现多个文档的创建.索引.更新和删除 ----------------------------------------------------------------------- ...
- java子类重写父类的要点
子类不能重写父类的静态方法,私有方法.即使你看到子类中存在貌似是重写的父类的静态方法或者私有方法,编译是没有问题的,但那其实是你重新又定义的方法,不是重写.具体有关重写父类方法的规则如下:重写规则之一 ...
- Python入门之函数的介绍/定义/定义类型/函数调用/Return
本篇目录: 一. 函数的介绍 二. 函数的定义 三. 定义函数的三种类型 四. 函数调用的阶段 五. Return返回值 ======================================= ...
- ps图层的基本使用
图层的使用 图层的基本使用一:复制,选择多个,背景图上添加图片,同时移动多个图层 复制图层:图层里的内容位置会变化,而拷贝的图层,图层里的位置不变,跟原来的图层一样 选择多个图层:shift选中多个图 ...
- Delphi X10.2 + FireDAC 使用 SQL 语句 INSERT
// CREATE TABLE [tabusers]( // [id] INTEGER PRIMARY KEY AUTOINCREMENT, // [username] CHAR NOT NULL, ...
- WPF 字符串溢出判断,字符串长度是否超过控件宽度
TextBloc可以将TextTrimming属性设置为CharacterEllipsis 其他控件可以在控件大小变更或者其他事件上附加下列方法 private void OnEllipsis(obj ...
- Unity中的基础光照
渲染包含了两大部分:决定一个像素的可见性,决定这个像素上的光照计算. 光照模型就是用于决定在一个像素上进行怎样的光照计算. 一.光源 在实时渲染中我们通常把光源当做一个没有体积的点. 1.1 辐照度 ...
- .Net Core 通过依赖注入和动态加载程序集实现宿程序和接口实现类库完全解构
网上很多.Net Core依赖注入的例子代码,例如再宿主程序中要这样写: services.AddTransient<Interface1, Class1>(); 其中Interface1 ...
- C#之FTP上传下载(二)
这个类几乎包含了对FTP常用的方法,有不对的地方,欢迎批评指正 public class FtpClient { #region 构造函数 /// <summary> /// 创建FTP工 ...
- Spring-cloud(五) 使用Ribbon进行Restful请求
写在前面 本文由markdown格式写成,为本人第一次这么写,排版可能会有点乱,还望各位海涵. 主要写的是使用Ribbon进行Restful请求,测试各个方法的使用,代码冗余较高,比较适合初学者,介意 ...