1. 程式人生 > >《自己動手寫java虛擬機器》學習筆記(三)-----搜尋class檔案(go)

《自己動手寫java虛擬機器》學習筆記(三)-----搜尋class檔案(go)

    專案地址:https://github.com/gongxianshengjiadexiaohuihui

我們都知道,.java檔案編譯後會形成.class檔案,然後class檔案會被載入到虛擬機器中,被我們使用,那麼虛擬機器如何從那裡尋找這些class檔案呢,java虛擬機器規範並沒有規定虛擬機器從哪裡尋找,Oracle的java虛擬機器實現根據類路徑來搜尋類按照先後順序,了路徑可分為

  •     啟動類路徑(bootstrap classpath)
  •     擴充套件類路徑(extension classpath)
  •     使用者類路徑(user classpath)

    這三種路徑呢又有不同的格式 

  1.    直接指定路徑,後面跟類名 -cp aaa/bbb/ccc ddd arg1 arg2
  2.    指定類所在的jar檔案的路徑,後面跟類名: -cp aaa/bbb/ccc.jar ddd arg1 arg2
  3.    指定若干個路徑,後面跟類名 -cp aaa1/bbb/ccc;aaa2/bbb/ccc;aaa3/bbb/ccc; ddd arg1 arg2
  4.    指定一個模糊路徑,後面跟類名 -cp aaa/bbb/* ddd arg1 arg2   

   所以能我們先實現不同格式的類路徑,然後在用它們組合成上面的三種路徑,很像組合模式

    新建一個classpath的包

    先定義一個介面表示類路徑,然後分別用4種方式去實現這個介面以達到我們的目的

     Entry介面

     

package classpath 

import "os"
import "strings"

const pathListSeparator=string(os.PathListSeparator)

type Entry interface {
	readClass(className string)([]byte,Entry,error)
	String() string
}
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)
}

   注意一點go結構體不需要顯示實現介面,只要方法匹配即可。go沒有專門的建構函式,統一用new開頭的函式建立結構體例項

DirEntry 第一種格式

package classpath 

import "io/ioutil"
import "path/filepath"

type DirEntry struct{
	absDir string
}

func newDirEntry(path string)*DirEntry{
	absDir,err := filepath.Abs(path)
	if err != nil{
		panic(err)
	}
	return &DirEntry{absDir}
}
func (self *DirEntry)readClass(className string)([]byte,Entry,error){
    //拼接路徑
	fileName := filepath.Join(self.absDir,className)
	data,err := ioutil.ReadFile(fileName)
	return data,self,err

}
func (self *DirEntry)String()string{
	return self.absDir
}

ZipEntry 第二種格式的路徑

package classpath 

import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"

type ZipEntry struct{
	absPath string
}
func newZipEntry(path string) *ZipEntry{
	absPath,err := filepath.Abs(path)
	if err!= nil{
		panic(err)
	}
	return &ZipEntry{absPath}
}
//遍歷該壓縮檔案裡的所有檔案
func (self *ZipEntry)readClass(className string)([]byte,Entry,error){
    r,err := zip.OpenReader(self.absPath)
    if err!= nil {
    	return nil,nil,err
    }

    defer r.Close()
    for _,f := range r.File{
    	if f.Name == className{
    		rc,err := f.Open()
    		if err != nil {
    			return nil,nil,err
    		}
            //在return後執行,避免資源未被釋放,多個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)
}
func (self *ZipEntry)String() string{
	return self.absPath
}

CompositeEntry 第三種格式路徑

CompositeEntry其實是由多個其它格式的Entry組成,所以表示成Entry陣列,先把路徑根據分隔符拆分成小路徑,然後用對應路徑的方法遍歷即可

package classpath 

import "errors"
import "strings"

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
}

func (self CompositeEntry) readClass(className string)([]byte,Entry,error){
	for _,entry := range self{
		data,form,err := entry.readClass(className)
		if(err == nil){
			return data,form,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)
}

WildcardEntry 第四種格式的路徑

第四種格式的路徑其實和第三種的幾乎一樣,我們遍歷a.*的子檔案,找出所有的.jar檔案,把路徑放入CompositeEntry即可

package classpath 

import "os"
import "path/filepath"
import "strings"

func newWildcardEntry(path string) CompositeEntry{
	baseDir := path[:len(path)-1]
	compositeEntry := []Entry{}
	walkFn := func(path string,info os.FileInfo,err error) error{
		if err != nil {
			return err
		}
		//萬用字元路徑不能遞迴匹配子目錄下的jar檔案,所以除了根目錄路徑,其它目錄跳過,不執行,例如a.* 只能是a.b a.c .....
		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 
	}
	filepath.Walk(baseDir,walkFn)
	return compositeEntry
}

介面實現完了,接下來,就是解析穿過來的引數,然後分別從三種路徑種尋找class檔案即可

classpath.go

package classpath 

import "os"
import "path/filepath"

type Classpath struct{
	//啟動類路徑 jre/lib/*
	bootClasspath Entry
	//擴充套件類路徑 jre/lib/ext/* 
    extClasspath Entry
    //使用者類路徑 預設是當前路徑
    userClasspath Entry
}
//Parse()函式用-Xjre選項解析啟動類路徑和擴充套件類路徑,使用-classpath/-cp解析使用者類路徑
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,"ext","*")
    self.extClasspath =newWildcardEntry(jreExtPath)

}
	//1優先使用使用者輸入的-Xjre選項作為jre目錄
	//2當前目錄下尋找jre目錄
	//3用JAVA_HOME環境變數
func getJreDir(jreOption string)string{
	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{
	//返回檔案型別的描述FileInfo
   if _,err:=os.Stat(path); err != nil {
   	if os.IsNotExist(err){
   		return false
   	}
   }
   return false
}
//解析使用者類路徑
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();

}

接下來驗證我們寫的程式碼

更改

main.go

呼叫我們寫的搜尋類的函式,返回位元組碼

package main

import "fmt"
import "strings"
import "jvmgo/classpath"

func main(){
	//呼叫解析命令列的行數,接受解析結果
    cmd:=parseCmd()
    if cmd.versionFlag{
    	fmt.Println("version 0.0.1")
    }else if cmd.helpFlag||cmd.class==""{
    	printUsage()
    }else{
    	startJVM(cmd)
    }
}

func startJVM(cmd *Cmd){
	//解析類路徑
	cp := classpath.Parse(cmd.XjreOption,cmd.cpOption)
	fmt.Printf("classpath:%v class:%v args:%v\n",cp,cmd.class,cmd.args)
    //func Replace(s, old, new string, n int) string
    //返回將s中前n個不重疊old子串都替換為new的新字串,如果n<0會替換所有old子串。
	className := strings.Replace(cmd.class,".","/",-1)
	classData, _, err := cp.ReadClass(className)
	if err != nil {
		fmt.Printf("Cound not find or load main class %s\n",cmd.class)
		return 
	}
	fmt.Printf("class data:%v\n",classData)
}

結果

參考資料:《自己動手寫java虛擬機器》