手寫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)-1]
    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)>0{
        cmd.class=args[0]
        cmd.args=args[1:]
    }

    return cmd
    
}

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

  到這里,搜索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文件並讀出文件的內容就完成了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM