[JVM] - 一份<自己動手寫Java虛擬機器>的測試版
配置GOROOT(一般是自動的),配置GOPATH(如果想自己改的話)
參照<自己動手寫Java虛擬機器>
> 第一章 指令集和直譯器
生成了ch01.exe檔案
這裡還生成了一個gopkgs.exe檔案
執行以上操作,這裡說明:go開發java虛擬機器實際上這段模擬的是命令列在安裝好java JDK後的一些輸入,比如檢視java的version資訊.
這裡已經在程式碼中寫好了.
main.go :
package main import "fmt" 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){ fmt.Printf("classpath:%s class:%s args:%v\n",cmd.cpOption,cmd.class,cmd.args) }
cmd.go :
package main import "flag" import "fmt" // import "os"type Cmd struct { helpFlag bool versionFlag bool cpOption string class string args []string } func parseCmd() *Cmd { cmd := &Cmd{} //首先設定flag.Usage變數,把printUsage()函式賦值給它 flag.Usage = printUsage //然後呼叫flag包提供的各種Var()函式設定需要解析的選項 flag.BoolVar(&cmd.helpFlag,"help",false,"print help message") flag.BoolVar(&cmd.helpFlag,"?",false,"print help message") flag.BoolVar(&cmd.versionFlag,"version",false,"print versionand exit") flag.StringVar(&cmd.cpOption,"classpath","","classpath") flag.StringVar(&cmd.cpOption,"cp","","classpath") //接著呼叫Parse()函式解析選項.如果Parse()函式解析失敗,它就呼叫printUsage()函式把命令的用法列印到控制檯 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...]") }
在Java語言中,API一般以類庫的形式提供.在Go語言中,API則是以包(package)的形式提供.
包可以向用戶提供常量,變數,結構體以及函式等.Java內建了豐富的類庫,Go同樣也內建了功能強大的包(package).
注意,與cmd.go檔案一樣,main.go檔案的包名也是main. 在Go語言中,main是一個特殊的包,這個包所在的目錄(可以叫做任何名字)會被編譯為可執行檔案.
Go程式的入口也是main()函式,但是不接收任何引數,也不能有返回值.
go中,main()函式先呼叫ParseCommand()函式解析命令列引數,如果一切OK,則呼叫startJVM()函式啟動Java虛擬機器.
如果解析出現錯誤,或者使用者輸入了-help選項,則呼叫PrintUsage()函式列印幫助資訊.如果輸入-version.......
到這裡,只是進行了模式.
在使用教程時,看到作者的github上的這個庫已經更新了.程式碼跟這剛開始的差別很大.而且import "os"會在vscode中報錯.
> 第二章 搜尋class檔案
首先看一段親切的程式碼:
載入HelloWorld類之前,首先要載入它的超類,也就是java.lang.Object . 在呼叫main()方法之前,因為虛擬機器需要準備好引數陣列,所以需要載入
java.lang.String和java.lang.String[]類. 把字串列印到控制檯還需要載入java.lang.System類,等等.那麼Java虛擬機器從哪裡 尋找這些類呢?
Java虛擬機器規範並沒有規定虛擬機器應該從哪裡尋找類,因此不同的虛擬機器實現可以採用不同的方法. Oracle的Java虛擬機器實現根據類路徑(class path)來搜尋類.
按照搜尋的先後順序, 類路徑可以分為下面3個部分:
- 1. 啟動類路徑 (bootstrap classpath)
- 2. 擴充套件類路徑 (extension classpath)
- 3. 使用者類路徑 (user classpath)
啟動類路徑預設對應jre\lib目錄,Java標準庫(大部分在rt.jar裡)位於該路徑.
擴充套件類路徑預設對應jre\lib\ext目錄,使用Java擴充套件機制的類位於這個路徑.
我們自己實現的類,以及第三方類庫則位於使用者類路徑.
可以通過-Xbootclasspath選項修改啟動類路徑,不過通常並不需要這樣做.
使用者類路徑的預設值是當前目錄,也就是"." 可以設定CLASSPATH環境變數來修改使用者類路徑,但是這不夠靈活.
更好的辦法:給java命令傳遞-classpath(或簡寫為-cp)選項. -classpath/-cp選項的優先順序更高,可以覆蓋CLASSPATH環境變數設定.
-classpath或-cp選項既可以知道目錄,也可以指定JAR檔案或者ZIP檔案.
java -cp path\to\classes java -cp path\to\lib1.jar java -cp path\to\lib2.zip
還可以同時指定多個目錄或檔案,用分隔符分開即可. 分隔符因作業系統而異.
在Windows系統下是分號,在類UNIX(包括Linux,Mac OS X等)系統下是冒號.可以使用萬用字元 *
第二章的程式碼建立在第一章的程式碼基礎上.
Java虛擬機器將使用JDK的啟動類路徑來尋找和載入Java標準庫中的類,因此需要某種方式指定jre目錄的位置.
命令列不錯,所以增加一個非標選項 -Xjre
classpath資料夾新建Entry.go :
package classpath import "os" import "strings" //常量,存放路徑分隔符 const pathListSeparator = string(os.pathListSeparator) type Entry interface{ //負責尋找和載入class檔案; readClass(className string)([]byte, Entry,error) //該String()方法作用相當於java中的toString()用於返回變數的字串表示 String() string } func newEntry(path string) Entry {...}
readClass()方法的引數是class檔案的相對路徑,路徑之間用斜線(/)分隔,檔名有.class字尾.
比如要讀取java.lang.Object類,傳入的引數應該是java/lang/Object.class
返回值是讀取到的位元組資料,最終定位到class檔案的Entry.以及錯誤資訊.
Go的函式或方法允許返回多個值. 按照慣例. 可以使用最後一個返回值做為錯誤資訊.
newEntry()函式根據引數建立不同型別的Entry例項.
具體Entry.go :
package classpath import "os" import "strings" //常量,存放路徑分隔符 const pathListSeparator = string(os.pathListSeparator) type Entry interface{ //負責尋找和載入class檔案; readClass(className string)([]byte, Entry,error) //該String()方法作用相當於java中的toString()用於返回變數的字串表示 String() string } //Entry介面有四個實現,分別是DirEntry,ZipEntry,CompositeEntry和WildcardEntry. func newEntry(path string) Entry { if strings.Contais(path,pathListSeparator){ return newCompositeEntry(path) } if strings.HasSuffix(path,"*"){ return newWildcardEntry(path) } if strings.HasSuffix(path,".jar") || strings.HasSuffix(path,".JAR") || stirngs.HasSuffix(path,".zip") || strings.HasSuffix(path,".ZIP"){ return newZipEntry(path) } return newDirEntry(path) }
上面os依舊報錯了
在上面說的四種實現中,DirEntry相對簡單一些,表示目錄形式的類路徑.
在ch02\classpath目錄下建立entry_dir.go檔案,在其中定義DirEntry結構體:
package classpath import "io/ioutil" import "path/filepath" type DirEntry struct { absDir string } func newDirEntry(path string) *DirEntry {} func (self *DirEntry) readClass(className string)([]byte,Entry,error){} func (self *DirEntry) String() string {}
newDirEntry() 先把引數轉換成絕對路徑,如果轉換過程出現錯誤, 則呼叫panic()函式終止程式執行.
否則建立DirEntry例項並返回.
完整的entry_dir.go :
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 }
之後建立entry_zip.go
package classpath import "archive/zip" import "errors" import "io/ioutil" import "path/filepath" type ZipEntry struct { absPath string //absPath欄位存放ZIP或JAR檔案的絕對路徑 } func new ZipEntry(path string) *ZipEntry { absPath,err := filepath,Abs(path) if err != nil{ panic(err) } return &ZipEntry{absPath} } func (self *ZipEntry) String() string { return self.absPath } //從ZIP檔案中提取class檔案: 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 } } 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) }
該程式碼首先開啟ZIP檔案, 如果這一步出錯的話,直接返回. 然後遍歷ZIP壓縮包裡的檔案, 看能否找到class檔案.
如果能找到,則開啟class檔案,把內容讀取出來,並返回.
如果找不到,或者出現其它錯誤,則返回錯誤資訊.有兩處使用了defer語句來確保開啟的檔案得以關閉.
readClass()方法每次都要開啟和關閉ZIP檔案,因此效率不是很高.
CompositeEntry由更小的Entry組成,正好可以表示成[]Entry. 在Go語言中,陣列屬於比較低層的資料結構,很少直接使用.大部分情況下,
使用更便利的slice型別. 建構函式把引數(路徑列表)按分隔符分成小路徑. 然後把每個小路經都轉換成具體的Entry例項.
package classpath import ( "errors" "strings" ) type CompositeEntry struct { entries []Entry } func newCompositeEntry(pathList string) *CompositeEntry { compoundEntry := &CompositeEntry{} for _, path := range strings.Split(pathList, pathListSeparator) { entry := newEntry(path) compoundEntry.addEntry(entry) } return compoundEntry } func (self *CompositeEntry) addEntry(entry Entry) { self.entries = append(self.entries, entry) } func (self *CompositeEntry) readClass(className string) (Entry, []byte, error) { for _, entry := range self.entries { entry, data, err := entry.readClass(className) if err == nil { return entry, data, nil } } return self, nil, errors.New("class not found: " + className) } func (self *CompositeEntry) String() string { strs := make([]string, len(self.entries)) for i, entry := range self.entries { strs[i] = entry.String() } return strings.Join(strs, pathListSeparator) }
以上為完整版.
這裡應該明白了一件事,DirEntry也好,ZipEntry,CompositeEntry,WildcardEntry也好,可以讀取被編譯好的java庫的class封裝包(jar或其它格式).
這個readClass()方法,依次呼叫每一個路徑的readClass()方法,如果成功讀取到class資料,返回資料即可.
如果收到錯誤資訊,則繼續;如果遍歷完所有的子路徑還沒有找到class檔案,則返回錯誤.
String()方法也不復雜,呼叫每一個子路徑的String()方法,然後把得到的字串用路徑分隔符拼接起來即可.
接下來是WildcardEntry:
WildcardEntry實際上也是CompositeEntry, 所以就不再定義新的型別了.
package classpath import ( "os" "path/filepath" "strings" ) type WildcardEntry struct { CompositeEntry } func newWildcardEntry(path string) *WildcardEntry { baseDir := path[:len(path)-1] // remove * entry := &WildcardEntry{} 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) entry.addEntry(jarEntry) } return nil } filepath.Walk(baseDir, walkFn) return entry }
首先newWildcardEntry這個構造器方法把傳入路徑的星號去掉,得到baseDir,然後呼叫filepath的包的Walk()函式遍歷baseDir建立ZipEntry.
Walk()函式的第二個引數也是一個函式,瞭解函數語言程式設計的讀者應該一眼就可以認出這個用法(即函式可做為引數) .
walkFn中,根據字尾名選出JAR檔案,並且返回SkipDir跳過子目錄(萬用字元類路徑不能遞迴匹配子目錄下的JAR檔案).
接下來是classpath結構體.
package classpath import ( "path/filepath" "strings" "github.com/zxh0/jvm.go/jvmgo/options" ) type ClassPath struct { CompositeEntry } func Parse(cpOption string) *ClassPath { cp := &ClassPath{} cp.parseBootAndExtClassPath() cp.parseUserClassPath(cpOption) return cp } func (self *ClassPath) parseBootAndExtClassPath() { // jre/lib/* jreLibPath := filepath.Join(options.AbsJavaHome, "lib", "*") self.addEntry(newWildcardEntry(jreLibPath)) // jre/lib/ext/* jreExtPath := filepath.Join(options.AbsJavaHome, "lib", "ext", "*") self.addEntry(newWildcardEntry(jreExtPath)) } func (self *ClassPath) parseUserClassPath(cpOption string) { if cpOption == "" { cpOption = "." } self.addEntry(newEntry(cpOption)) } // className: fully/qualified/ClassName func (self *ClassPath) ReadClass(className string) (Entry, []byte, error) { className = className + ".class" return self.readClass(className) } func (self *ClassPath) String() string { userClassPath := self.CompositeEntry.entries[2] return userClassPath.String() } func IsBootClassPath(entry Entry) bool { if entry == nil { // todo return true } return strings.HasPrefix(entry.String(), options.AbsJreLib) }
上面的路徑還未加入.
getJreDir()函式優先使用使用者輸入的-Xjre選項作為jre目錄. 如果沒有輸入該選項,則在當前目錄下尋找jre目錄,如果找不到,嘗試使用JAVA_HOME環境變數.
exists()函式用於判斷目錄是否存在.
如果使用者沒有提供-classpath/-cp選項,則使用當前目錄作為使用者類路徑.
ReadClass()方法依次從啟動類路徑,擴充套件類路徑和使用者類路徑中搜索class檔案,程式碼.注意,傳遞給
ReadClass()方法的類名不包含".class"字尾. 最後, String()方法返回使用者類路徑的字串表示.
其實在更新後的程式碼不太準確.稍後將初始版的ch01和ch02上傳到github供後來人參考.
待上傳