這一章介紹了Thymeleaf,Java模板引擎,用於Web和獨立環境,與Spring Boot緊密集成。它適用於有無網路的場景,讓美工和程式員分別在瀏覽器和伺服器上查看靜態與動態頁面。筆記詳細講解Thymeleaf的配置、語法,如th:text提交基本數據、th:each穿越集合,以及通過th:i... ...
原文鏈接:https://gaoyubo.cn/blogs/de1bedad.html
前言
所需前置知識為:JAVA語言、JVM知識、Go筆記
對應項目:jvmgo
一、準備環境
操作系統:Windows 11
1.1 JDK版本
openjdk version "1.8.0_382"
1.2 Go版本
go version go1.21.0 windows/amd64

1.3 配置Go工作空間

1.4 java命令指示
Java虛擬機的工作是運行Java應用程式。和其他類型的應用程式一樣,Java應用程式也需要一個入口點,這個入口點就是我們熟知的main()
方法。最簡單的Java程式是 只有一個main()
方法的類,如著名的HelloWorld程式。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
JVM如何知道從哪個類啟動呢,虛擬機規範並沒有明確,而是需要虛擬機實現。比如Oracle的JVM就是通過java
命令啟動的,主類名由命令行參數決定。
java命令有如下4種形式:
java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]
可以向java
命令傳遞三組參數:選項、主類名(或者JAR文件名) 和main()
方法參數。選項由減號(–)開頭。通常,第一個非選項參數 給出主類的完全限定名(fully qualified class name)。但是如果用戶提供了–jar選項,則第一個非選項參數表示JAR文件名,java
命令必須從這個JAR文件中尋找主類。javaw
命令和java命令幾乎一樣,唯 一的差別在於,javaw
命令不顯示命令行視窗,因此特別適合用於啟 動GUI(圖形用戶界面)應用程式。
選項可以分為兩類:標準選項和非標準選項。標準選項比較穩定,不會輕易變動。非標準選項以-X開頭,
選項 | 用途 |
---|---|
-version | 輸出版本信息,然後退出 |
-? / -help | 輸出幫助信息,然後退出 |
-cp / -classpath | 指定用戶類路徑 |
-Dproperty=value | 設置Java系統屬性 |
-Xms<size> | 設置初始堆空間 大小 |
-Xmx<size> | 設置最大堆空間 大小 |
-Xss<size> | 設置線程棧空間 大小 |
二、編寫命令行工具
環境準備完畢,接下來實現java
命令的的第一種用法。
2.1 創建目錄

創建cmd.go
Go源文件一般以.go作為尾碼,文件名全部小寫,多個單詞之間使用下劃線分隔。Go語言規範要求Go源文件必須使用UTF-8編碼,詳見https://golang.org/ref/spec
2.2 結構體存儲cmd選項
在文件中定義cmd中java命令需要的選項和參數
package ch01
// author:郜宇博
type Cmd struct {
// 標註是否為 --help
helpFlag bool
//標註是否為 --version
versionFlag bool
//選項
cpOption string
//主類名,或者是jar文件
class string
//參數
args []string
}
Go語言標準庫包
由於要處理的命令行,因此將使用到flag()
函數,此函數為Go的標準庫包之一。
Go語言的標準庫以包的方式提供支持,下表列出了Go語言標準庫中常見的包及其功能。
Go語言標準庫包名 | 功 能 |
---|---|
bufio | 帶緩衝的 I/O 操作 |
bytes | 實現位元組操作 |
container | 封裝堆、列表和環形列表等容器 |
crypto | 加密演算法 |
database | 資料庫驅動和介面 |
debug | 各種調試文件格式訪問及調試功能 |
encoding | 常見演算法如 JSON、XML、Base64 等 |
flag | 命令行解析 |
fmt | 格式化操作 |
go | Go語言的詞法、語法樹、類型等。可通過這個包進行代碼信息提取和修改 |
html | HTML 轉義及模板系統 |
image | 常見圖形格式的訪問及生成 |
io | 實現 I/O 原始訪問介面及訪問封裝 |
math | 數學庫 |
net | 網路庫,支持 Socket、HTTP、郵件、RPC、SMTP 等 |
os | 操作系統平臺不依賴平臺操作封裝 |
path | 相容各操作系統的路徑操作實用函數 |
plugin | Go 1.7 加入的插件系統。支持將代碼編譯為插件,按需載入 |
reflect | 語言反射支持。可以動態獲得代碼中的類型信息,獲取和修改變數的值 |
regexp | 正則表達式封裝 |
runtime | 運行時介面 |
sort | 排序介面 |
strings | 字元串轉換、解析及實用函數 |
time | 時間介面 |
text | 文本模板及 Token 詞法器 |
flag()函數
[]: https://studygolang.com/pkgdoc
eg:flag.TypeVar()
基本格式如下: flag.TypeVar(Type指針, flag名, 預設值, 幫助信息)
例如我們要定義姓名、年齡、婚否三個命令行參數,我們可以按如下方式定義:
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "張三", "姓名")
flag.IntVar(&age, "age", 18, "年齡")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "時間間隔")
2.3 接收處理用戶輸入的命令行指令
創建parseCmd()函數,實現接受處理用戶輸入的命令行指令
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.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])
//flag.PrintDefaults()
}
首先設置flag.Usage
變數,把printUsage()
函數賦值給它;
然後調 用flag包提供的各種Var函數設置需要解析的選項;
接著調用 Parse()
函數解析選項。
如果Parse()
函數解析失敗,它就調用 printUsage()
函數把命令的用法列印到控制台。
如果解析成功,調用flag.Args()
函數可以捕獲其他沒有被解析 的參數。其中第一個參數就是主類名,剩下的是要傳遞給主類的參數。
2.4 測試
與cmd.go文件一樣,main.go文件的包名也是main。在Go 語言中,main是一個特殊的包,這個包所在的目錄(可以叫作任何 名字)會被編譯為可執行文件。Go程式的入口也是main()函數,但 是不接收任何參數,也不能有返回值。
測試代碼如下:
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)
}
}
// 模擬啟動JVM
func startJVM(cmd *Cmd) {
fmt.Printf("classpath:%s class:%s args:%v\n",
cmd.cpOption, cmd.class, cmd.args)
}
main()
函數先調用ParseCommand()
函數解析命令行參數,如 果一切正常,則調用startJVM()
函數啟動Java虛擬機。如果解析出現錯誤,或者用戶輸入了-help選項,則調用PrintUsage()
函數列印出幫助信息。如果用戶輸入了-version
選項,則輸版本信息。因為我們還沒有真正開始編寫Java虛擬機,所以startJVM()
函數暫時只是列印一些信息而已。
在終端:
go install jvmgo\ch0
此時在工作空間的bin目錄中會生成ch01.exe的文件,運行:結果如下


三、獲取類路徑
已經完成了JAVA應用程式如何啟動:命令行啟動,並獲取到了啟動時需要的選項和參數。
但是,如果要啟動一個最簡單的“Hello World”程式(如下),也需要載入很多所需的類進入JVM
中
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
載入HelloWorld類之前,需要載入該類的父類(超類),也就是java.lang.Object
,main函數的參數為String[]
類型,因此也需要將java.lang.String
類和java.lang.String[]
載入,輸出字元串又需要載入java.lang.System
類,等等。接下來就來解決如何獲取這些類的路徑。
3.1類路徑介紹
Java虛擬機規範並沒有規定虛擬機應該從哪裡尋找類,因此不同的虛擬機實現可以採用不同的方法。
Oracle的Java虛擬機實現根據類路徑(class path)來搜索類。
按照搜索的先後順序,類路徑可以 分為以下3個部分:
- 啟動類路徑(bootstrap classpath)
- 擴展類路徑(extension classpath)
- 用戶類路徑(user classpath)
啟動類路徑預設對應jre\lib
目錄,Java標準庫(大部分在rt.jar
里) 位於該路徑。
擴展類路徑預設對應jre\lib\ext
目錄,使用Java擴展機制的類位於這個路徑。
用戶類路徑為自己實現的類,以及第三方類庫的路徑。可以通過-Xbootclasspath
選項修改啟動類路徑,不過一般不需要這樣做。
用戶類路徑的預設值是當前目錄,也就是.
。可以設置 CLASSPATH環境變數來修改用戶類路徑,但是這樣做不夠靈活,所以不推薦使用。
更好的辦法是給java命令傳遞-classpath
(或簡寫為-cp
)選項。-classpath/-cp
選項的優先順序更高,可以覆蓋CLASSPATH環境變數
設置。如下:
java -cp path\to\classes ...
java -cp path\to\lib1.jar ...
java -cp path\to\lib2.zip ...
3.2解析用戶類路徑
該功能建立在命令行工具上,因此複製上次的代碼,並創建classpath子目錄。
Java虛擬機將使用JDK的啟動類路徑來尋找和載入Java 標準庫中的類,因此需要某種方式指定jre目錄的位置。
命令行選項可以獲取,所以增加一個非標準選項-Xjre。
修改Cmd結構體,添加XjreOption欄位;parseCmd()函數也要相應修改:
type Cmd struct {
// 標註是否為 --help
helpFlag bool
//標註是否為 --version
versionFlag bool
//選項
cpOption string
//主類名,或者是jar文件
class string
//參數
args []string
// jre路徑
XjreOption 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
}
3.3獲取用戶類路徑
可以把類路徑想象成一個大的整體,它由啟動類路徑、擴展類路徑和用戶類路徑三個小路徑構成。
三個小路徑又分別由更小的路徑構成。是不是很像組合模式(composite pattern)
?
接下來將使用組合模式來設計和實現類路徑。
1.Entry介面
定義一個Entry
介面,作為所有類的基準。
package classpath
import "os"
// :(linux/unix) or ;(windows)
const pathListSeparator = string(os.PathListSeparator)
type Entry interface {
// className: fully/qualified/ClassName.class
readClass(classpath string) ([]byte, Entry, error)
String() string
}
常量pathListSeparator是string類型,存放路徑分隔符,後面會用到。
Entry介面中有個兩方法。
-
readClass()方法:負責尋找和載入class 文件。
參數是class文件的相對路徑,路徑之間用斜線/
分隔,文件名有.class
尾碼。比如要讀取java.lang.Object
類,傳 入的參數應該是java/lang/Object.class
。返回值是讀取到的位元組數據、最終定位到class文件的Entry,以及錯誤信息。 -
String()方法:作用相當於Java中的
toString()
,用於返回變數 的字元串表示。
Go的函數或方法允許返回多個值,按照慣例,可以使用最後一個返回值作為錯誤信息。
還需要一個類似於JAVA構造函數的函數,但在Go語言中沒有構造函數的概念,對象的創建通常交由一個全局的創建函數來完成,以NewXXX來命令,表示"構造函數"
newEntry()函數根據參數創建不同類型的Entry實例,代碼如下:
func newEntry(path string) Entry {
////如果路徑包含分隔符 表示有多個文件
if strings.Contains(path, pathListSeparator) {
return newCompositeEntry(path)
}
//包含*,則說明要將相應目錄下的所有class文件載入
if strings.HasSuffix(path, "*") {
return newWildcardEntry(path)
}
//包含.jar,則說明是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)
}
2.實現類
存在四種類路徑指定方式:
- 普通路徑形式:gyb/gyb
- jar/zip形式:/gyb/gyb.jar
- 通配符形式:gyb/*
- 多個路徑形式:gyb/1:/gyb/2
DirEntry(普通形式)
創建entry_dir.go
,定義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) {
//拼接類文件目錄 和 類文件名
// '/gyb/xxx/' + 'helloworld.class' = '/gyb/xxx/helloworld.class'
fileName := filepath.Join(self.absDir, className)
data, err := ioutil.ReadFile(fileName)
return data, self, err
}
func (self *DirEntry) String() string {
return self.absDir
}
DirEntry
只有一個欄位,用於存放目錄的絕對路徑。
和Java語言不同,Go結構體不需要顯示實現介面,只要方法匹配即可。
ZipEntry(jar/zip形式)
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
}
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
}
首先打開ZIP文件,如果這一步出錯的話,直接返回。然後遍歷 ZIP壓縮包里的文件,看能否找到class文件。如果能找到,則打開 class文件,把內容讀取出來,並返回。如果找不到,或者出現其他錯 誤,則返回錯誤信息。有兩處使用了defer語句來確保打開的文件得 以關閉。
CompositeEntry(多路徑形式)
CompositeEntry
由更小的Entry
組成,正好可以表示成[]Entry。
在Go語言中,數組屬於比較低層的數據結構,很少直接使用。大部分情況下,使用更便利的slice類型。
構造函數把參數(路徑列表)按分隔符分成小路徑,然後把每個小路徑都轉換成具體的 Entry實例。
package classpath
import "errors"
import "strings"
type CompositeEntry []Entry
func newCompositeEntry(pathList string) CompositeEntry {
compositeEntry := []Entry{}
for _, path := range strings.Split(pathList, pathListSeparator) {
//去判斷 path 屬於哪其他三種哪一種情況 生成對應的 ClassDirEntry類目錄對象
entry := newEntry(path)
compositeEntry = append(compositeEntry, entry)
}
return compositeEntry
}
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)
}
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(通配符形式)
WildcardEntry
實際上也是CompositeEntry
,所以就不再定義新的類型了。
首先把路徑末尾的星號去掉,得到baseDir,然後調用filepath
包的Walk()
函數遍歷baseDir創建ZipEntry
。Walk()
函數的第二個參數 也是一個函數。
在walkFn
中,根據尾碼名選出JAR文件
,並且返回SkipDir跳過子目錄(通配符類路徑不能遞歸匹配子目錄下的JAR文件)。
package classpath
import "os"
import "path/filepath"
import "strings"
func newWildcardEntry(path string) CompositeEntry {
//截取通用匹配符 /gyb/* 截取掉 *
baseDir := path[:len(path)-1] // remove *
//多個 類目錄對象
compositeEntry := []Entry{}
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") {
jarEntry := newZipEntry(path)
compositeEntry = append(compositeEntry, jarEntry)
}
return nil
}
//遍歷 目錄下所有 .jar .JAR 文件 生成ZipEntry目錄對象 放在切片中返回
//walFn為函數
filepath.Walk(baseDir, walkFn)
return compositeEntry
}
3.4實現類目錄
前面提到了 java 虛擬機
預設 會先從啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,載入類。
那麼就會有3個類目錄對象,所以就要定義一個結構體去存放它。
type Classpath struct {
BootClasspath Entry
ExtClasspath Entry
UserClasspath Entry
}
啟動類路徑
啟動路徑,其實對應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) {
jreDir := getJreDir(jreOption)
// 拼接成jre 的路徑
// jre/lib/*
jreLibPath := filepath.Join(jreDir, "lib", "*")
//載入 所有底下的 jar包
self.BootClasspath = newWildcardEntry(jreLibPath)
// 拼接 擴展類 的路徑
// jre/lib/ext/*
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)
}
實現類的載入
對於指定文件類名取查找 我們是按前面提到的(啟動路徑--->擴展類路徑 --->用戶類路徑
按順序依次去尋找,載入類),沒找到就挨個查找下去。
如果用戶沒有提供-classpath/-cp
選項,則使用當前目錄作為用 戶類路徑。ReadClass()
方法依次從啟動類路徑、擴展類路徑和用戶 類路徑中搜索class文件,
//根據類名 分別從 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
}
註意,傳遞給ReadClass()方法的類名不包含“.class”尾碼。
3.5總結

3.6測試
成功獲取到class文件!

四、解析Class文件
4.1 class文件介紹
作為類/介面信息的載體,每一個class文件
都完整的定義了一個類,為了使Java程式可以實現“編寫一次,處處運行”,java虛擬機對class文件的格式進行了嚴格的規範。
但是對於從哪裡載入class文件
,給予了高度自由空間:第三節中說過,可以從文件系統讀取jar/zip文件
中的class文件
,除此之外,也可以從網路下載,甚至是直接在運行中生成class文件
。
構成class文件
的基本數據單位是位元組,可以把整個class文件當 成一個位元組流來處理。稍大一些的數據由連續多個位元組構成,這些數據在class文件中以大端(big-endian)
方式存儲。
為了描述class文件格式,Java虛擬機規範定義了u1
、u2
和u4
三種數據類型來表示1、 2和4位元組無符號整數,分別對應Go語言的uint8
、uint16
和uint32
類型。
相同類型的多條數據一般按表(table)
的形式存儲在class文件中。表由表頭
和表項(item)
構成,表頭是u2或u4整數。假設表頭是 n,後面就緊跟著n個表項數據。
Java虛擬機規範使用一種類似C語言的結構體語法來描述class 文件格式。整個class文件被描述為一個ClassFile結構,代碼如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

!表示大小不定。
4.2解析class文件
Go語言內置了豐富的數據類型,非常適合處理class文件。
如下為Go和Java語言基本數據類型對照關係:
Go語言類型 | Java語言類型 | 說明 |
---|---|---|
int8 | byte | 8比特有符號整數 |
uint8(別名byte) | N/A | 8比特無符號整數 |
int16 | short | 16比特有符號整數 |
uint16 | char | 16比特無符號整數 |
int32(別名rune) | int | 32比特有符號整數 |
uint32 | N/A | 32比特無符號整數 |
int64 | long | 64比特有符號整數 |
uint64 | N/A | 64比特無符號整數 |
float32 | float | 32比特IEEE-754浮點數 |
float64 | double | 64比特IEEE-754浮點數 |
4.2.1讀取數據
解析class文件
的第一步是從裡面讀取數據。雖然可以把class文件
當成位元組流來處理,但是直接操作位元組很不方便,所以先定義一個結構體ClassReader
來幫助讀取數據,創建class_reader.go。
package classfile
import "encoding/binary"
type ClassReader struct {
data []byte
}
func (self *ClassReader) readUint8() uint8 {...} // u1
func (self *ClassReader) readUint16() uint16 {...} // u2
func (self *ClassReader) readUint32() uint32 {...} // u4
func (self *ClassReader) readUint64() uint64 {...}
func (self *ClassReader) readUint16s() []uint16 {...}
func (self *ClassReader) readBytes(length uint32) []byte {...}
ClassReader
只是[]byte
類型的包裝而已。readUint8()
讀取u1
類型數據。
ClassReader
並沒有使用索引記錄數據位置,而是使用Go 語言的reslice語法
跳過已經讀取的數據
實現代碼如下:
// u1
func (self *ClassReader) readUint8() uint8 {
val := self.data[0]
self.data = self.data[1:]
return val
}
// u2
func (self *ClassReader) readUint16() uint16 {
val := binary.BigEndian.Uint16(self.data)
self.data = self.data[2:]
return val
}
// u4
func (self *ClassReader) readUint32() uint32 {
val := binary.BigEndian.Uint32(self.data)
self.data = self.data[4:]
return val
}
func (self *ClassReader) readUint64() uint64 {
val := binary.BigEndian.Uint64(self.data)
self.data = self.data[8:]
return val
}
func (self *ClassReader) readUint16s() []uint16 {
n := self.readUint16()
s := make([]uint16, n)
for i := range s {
s[i] = self.readUint16()
}
return s
}
func (self *ClassReader) readBytes(n uint32) []byte {
bytes := self.data[:n]
self.data = self.data[n:]
return bytes
}
Go標準庫encoding/binary包中定義了一個變數BigEndian
,可以從[]byte
中解碼多位元組數據。
4.2.2解析整體結構
有了ClassReader,可以開始解析class文件了。創建class_file.go文件,在其中定義ClassFile結構體
,與4.1中的class文件中欄位對應。
package classfile
import "fmt"
type ClassFile struct {
//magic uint32
minorVersion uint16
majorVersion uint16
constantPool ConstantPool
accessFlags uint16
thisClass uint16
superClass uint16
interfaces []uint16
fields []*MemberInfo
methods []*MemberInfo
attributes []AttributeInfo
}
在class_file.go文件中實現一系列函數和方法。
func Parse(classData []byte) (cf *ClassFile, err error) {...}
func (self *ClassFile) read(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {...}
func (self *ClassFile) MinorVersion() uint16 {...} // getter
func (self *ClassFile) MajorVersion() uint16 {...} // getter
func (self *ClassFile) ConstantPool() ConstantPool {...} // getter
func (self *ClassFile) AccessFlags() uint16 {...} // getter
func (self *ClassFile) Fields() []*MemberInfo {...} // getter
func (self *ClassFile) Methods() []*MemberInfo {...} // getter
func (self *ClassFile) ClassName() string {...}
func (self *ClassFile) SuperClassName() string {...}
func (self *ClassFile) InterfaceNames() []string {...}
相比Java語言,Go的訪問控制非常簡單:只有公開和私有兩種。
所有首字母大寫的類型、結構體、欄位、變數、函數、方法等都是公開的,可供其他包使用。
首字母小寫則是私有的,只能在包內部使用。
解析[]byte
Parse()函數把[]byte解析成ClassFile結構體。
func Parse(classData []byte) (cf *ClassFile, err error) {
defer func() {
//嘗試捕獲 panic,並將其存儲在變數 r 中。如果沒有發生 panic,r 將為 nil。
if r := recover(); r != nil {
var ok bool
//判斷 r 是否是一個 error 類型
err, ok = r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
}
}()
cr := &ClassReader{classData}
cf = &ClassFile{}
cf.read(cr)
return
}
順序解析
read() 方法依次調用其他方法解析class文件,順序一定要保證正確,與class文件相對應。
func (self *ClassFile) read(reader *ClassReader) {
//讀取並檢查類文件的魔數。
self.readAndCheckMagic(reader)
//讀取並檢查類文件的版本號。
self.readAndCheckVersion(reader)
//解析常量池,常量池類還沒寫
self.constantPool = readConstantPool(reader)
//讀取類的訪問標誌
self.accessFlags = reader.readUint16()
//讀取指向當前類在常量池中的索引
self.thisClass = reader.readUint16()
//父類在常量池中的索引
self.superClass = reader.readUint16()
//讀取介面表的數據
self.interfaces = reader.readUint16s()
//讀取類的欄位信息
self.fields = readMembers(reader, self.constantPool)
//讀取類的方法信息
self.methods = readMembers(reader, self.constantPool)
//讀取類的屬性信息(類級別的註解、源碼文件等)
self.attributes = readAttributes(reader, self.constantPool)
}
self.readAndCheckMagic(reader)
: 這是一個ClassFile
結構的方法,用於讀取並檢查類文件的魔數。魔數是類文件的標識符,用於確定文件是否為有效的類文件。self.readAndCheckVersion(reader)
: 這個方法用於讀取並檢查類文件的版本號。Java類文件具有版本號,標識了它們的Java編譯器版本。這裡會對版本號進行檢查。self.constantPool = readConstantPool(reader)
: 這一行代碼調用readConstantPool
函數來讀取常量池部分的數據,並將其存儲在ClassFile
結構的constantPool
欄位中。常量池是一個包含各種常量信息的表格,用於支持類文件中的各種符號引用。self.accessFlags = reader.readUint16()
: 這一行代碼讀取類的訪問標誌,它標識類的訪問許可權,例如public
、private
等。self.thisClass = reader.readUint16()
: 這行代碼讀取指向當前類在常量池中的索引,表示當前類的類名。self.superClass = reader.readUint16()
: 這行代碼讀取指向父類在常量池中的索引,表示當前類的父類名。self.interfaces = reader.readUint16s()
: 這行代碼讀取介面表的數據,表示當前類實現的介面。self.fields = readMembers(reader, self.constantPool)
: 這行代碼調用readMembers
函數,以讀取類的欄位信息,並將它們存儲在fields
欄位中。欄位包括類的成員變數。self.methods = readMembers(reader, self.constantPool)
: 這行代碼類似於上一行,但它讀取類的方法信息,並將它們存儲在methods
欄位中。self.attributes = readAttributes(reader, self.constantPool)
: 最後,這行代碼調用readAttributes
函數,以讀取類的屬性信息,並將它們存儲在attributes
欄位中。屬性包括類級別的註解、源碼文件等信息。
以下均為類似於Java的getter方法,以後將不再贅述。
func (self *ClassFile) MinorVersion() uint16 {
return self.minorVersion
}
func (self *ClassFile) MajorVersion() uint16 {
return self.majorVersion
}
func (self *ClassFile) ConstantPool() ConstantPool {
return self.constantPool
}
func (self *ClassFile) AccessFlags() uint16 {
return self.accessFlags
}
func (self *ClassFile) Fields() []*MemberInfo {
return self.fields
}
func (self *ClassFile) Methods() []*MemberInfo {
return self.methods
}
ClassName從常量池中獲取,SuperClassName同理,常量池還未實現。
所有類的超類(父類),Object是java中唯一沒有父類的類,一個類可以不是Object的直接子類,但一定是繼承於Object並拓展於Object。
func (self *ClassFile) ClassName() string {
return self.constantPool.getClassName(self.thisClass)
}
func (self *ClassFile) SuperClassName() string {
if self.superClass > 0 {
return self.constantPool.getClassName(self.superClass)
}
//Object類
return ""
}
Java的類是單繼承,多實現的,因此獲取介面應該使用迴圈,也從常量池中獲取。
func (self *ClassFile) InterfaceNames() []string {
interfaceNames := make([]string, len(self.interfaces))
for i, cpIndex := range self.interfaces {
interfaceNames[i] = self.constantPool.getClassName(cpIndex)
}
return interfaceNames
}
解析魔數
很多文件格式都會規定滿足該格式的文件必須以某幾個固定位元組開頭,這幾個位元組主要起標識作用,叫作魔數(magic number)
。
- PDF文件以4位元組“%PDF”(0x25、0x50、0x44、0x46)開頭
- ZIP 文件以2位元組“PK”(0x50、0x4B)開頭
- class文件的魔數 是“0xCAFEBABE” 。

因此readAndCheckMagic()方法的代碼如下。
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
magic := reader.readUint32()
if magic != 0xCAFEBABE {
panic("java.lang.ClassFormatError: magic!")
}
}
Java虛擬機規範規定,如果載入的class文件不符合要求的格式,Java虛擬機實現就拋出java.lang.ClassFormatError異常。
但是因為我們才剛剛開始編寫虛擬機,還無法拋出異常,所以暫時先調用 panic()方法終止程式執行。
版本號
解析版本號
魔數之後是class文件的次版本號和主版本號,都是u2類型
。
假設某class文件的主版本號是M,次版本號是m,那麼完整的版本號 可以表示成M.m
的形式。
次版本號只在J2SE 1.2
之前用過,從1.2 開始基本上就沒什麼用了(都是0)。
主版本號在J2SE 1.2之前是45, 從1.2開始,每次有大的Java版本發佈,都會加1。
Java 版本 | 類文件版本號 |
---|---|
Java 1.1 | 45.3 |
Java 1.2 | 46.0 |
Java 1.3 | 47.0 |
Java 1.4 | 48.0 |
Java 5 | 49.0 |
Java 6 | 50.0 |
Java 7 | 51.0 |
Java 8 | 52.0 |
特定的Java虛擬機實現只能支持版本號在某個範圍內的class文 件。
Oracle的實現是完全向後相容的,比如Java SE 8
支持版本號為 45.0~52.0的class文件。
如果版本號不在支持的範圍內,Java虛擬機 實現就拋出java.lang.UnsupportedClassVersionError
異常。參考 Java 8,支持版本號為45.0~52.0的class文件。如果遇到其他版本號, 調用panic()
方法終止程式執行。
如下為檢查版本號代碼:
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {
self.minorVersion = reader.readUint16()
self.majorVersion = reader.readUint16()
switch self.majorVersion {
case 45:
return
case 46, 47, 48, 49, 50, 51, 52:
if self.minorVersion == 0 {
return
}
}
panic("java.lang.UnsupportedClassVersionError!")
}
解析類訪問標識
版本號之後是常量池,但是由於常量池比較複雜,所以放到4.3 節介紹。
常量池之後是類訪問標誌,這是一個16位的bitmask
,指出class文件定義的是類還是介面,訪問級別是public
還是private
,等等。
本章只對class文件進行初步解析,並不做完整驗證,所以只是讀取類訪問標誌以備後用。
ClassFileTest的類訪問標誌為:0X21
:
解析類和父類索引
類訪問標誌之後是兩個u2類型的常量池索引,分別給出類名和超類名。
class文件存儲的類名類似完全限定名,但是把點換成了 斜線,Java語言規範把這種名字叫作二進位名binary names
。
因為每個類都有名字,所以thisClass
必須是有效的常量池索引。
除 java.lang.Object
之外,其他類都有超類,所以superClass
只在 Object.class
中是0,在其他class文件中必須是有效的常量池索引。如下,ClassFileTest的類索引是5,超類索引是6。

解析介面索引表
類和超類索引後面是介面索引表,表中存放的也是常量池索引,給出該類實現的所有介面的名字。ClassFileTest沒有實現介面, 所以介面表是空的

解析欄位和方法表
介面索引表之後是欄位表和方法表,分別存儲欄位和方法信息。
欄位和方法的基本結構大致相同,差別僅在於屬性表。
下麵是 Java虛擬機規範給出的欄位結構定義
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
和類一樣,欄位和方法也有自己的訪問標誌。訪問標誌之後是一個常量池索引,給出欄位名或方法名,然後又是一個常量池索引,給出欄位或方法的描述符,最後是屬性表。
為了避免重覆代 碼,用一個結構體統一表示欄位和方法。
package classfile
type MemberInfo struct {
cp ConstantPool
accessFlags uint16
nameIndex uint16
descriptorIndex uint16
attributes []AttributeInfo
}
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {...}
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {...}
func (self *MemberInfo) AccessFlags() uint16 {...} // getter
func (self *MemberInfo) Name() string {...}
func (self *MemberInfo) Descriptor() string {...}
cp欄位保存常量池指針,後面會用到它。readMembers()讀取欄位表或方法表,代碼如下:
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {
memberCount := reader.readUint16()
members := make([]*MemberInfo, memberCount)
for i := range members {
members[i] = readMember(reader, cp)
}
return members
}
readMember()函數讀取欄位或方法數據。
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {
return &MemberInfo{
cp: cp,
accessFlags: reader.readUint16(),
nameIndex: reader.readUint16(),
descriptorIndex: reader.readUint16(),
attributes: readAttributes(reader, cp),
}
}
Name()從常量 池查找欄位或方法名,Descriptor()從常量池查找欄位或方法描述 符
func (self *MemberInfo) Name() string {
return self.cp.getUtf8(self.nameIndex)
}
func (self *MemberInfo) Descriptor() string {
return self.cp.getUtf8(self.descriptorIndex)
}
4.2.3解析常量池
常量池占據了class文件很大一部分數據,裡面存放著各式各樣的常量信息,包括數字和字元串常量、類和介面名、欄位和方法名,等等
創建constant_pool.go
文件,裡面定義 ConstantPool類型
。
package classfile
type ConstantPool []ConstantInfo
func readConstantPool(reader *ClassReader) ConstantPool {...}
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {...}
func (self ConstantPool) getNameAndType(index uint16) (string, string) {...}
func (self ConstantPool) getClassName(index uint16) string {...}
func (self ConstantPool) getUtf8(index uint16) string {...}
常量池實際上也是一個表,但是有三點需要特別註意。
表頭給出的常量池大小比實際大1。假設表頭給出的值是n,那麼常量池的實際大小是n–1。
有效的常量池索引是1~n–1。0是無效索引,表示不指向任何常量。
CONSTANT_Long_info
和 CONSTANT_Double_info
各占兩個位置。也就是說,如果常量池中存在這兩種常量,實際的常量數量比n–1還要少,而且1~n–1的某些 數也會變成無效索引。
常量池由readConstantPool()
函數讀取,代碼如下:
func readConstantPool(reader *ClassReader) ConstantPool {
cpCount := int(reader.readUint16())
cp := make([]ConstantInfo, cpCount)
// 索引從1開始
for i := 1; i < cpCount; i++ {
cp[i] = readConstantInfo(reader, cp)
switch cp[i].(type) {
//占兩個位置
case *ConstantLongInfo, *ConstantDoubleInfo:
i++
}
}
return cp
}
getConstantInfo()
方法按索引查找常量
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
if cpInfo := self[index]; cpInfo != nil {
return cpInfo
}
panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}
getNameAndType()
方法從常量池查找欄位或方法的名字和描述符
func (self ConstantPool) getNameAndType(index uint16) (string, string) {
ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
name := self.getUtf8(ntInfo.nameIndex)
_type := self.getUtf8(ntInfo.descriptorIndex)
return name, _type
}
getClassName()
方法從常量池查找類名
func (self ConstantPool) getClassName(index uint16) string {
classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
return self.getUtf8(classInfo.nameIndex)
}
getUtf8()
方法從常量池查找UTF-8字元串
func (self ConstantPool) getUtf8(index uint16) string {
utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
return utf8Info.str
}
ConstPool介面
由於常量池中存放的信息各不相同,所以每種常量的格式也不同。
常量數據的第一位元組是tag,用來區分常量類型。
下麵是Java 虛擬機規範給出的常量結構
cp_info {
u1 tag;
u1 info[];
}
Java虛擬機規範一共定義了14種常量。創建constant_info.go
文件,在其中定義tag常量值,代碼如下:
package classfile
// Constant pool tags
const (
CONSTANT_Class = 7
CONSTANT_Fieldref = 9
CONSTANT_Methodref = 10
CONSTANT_InterfaceMethodref = 11
CONSTANT_String = 8
CONSTANT_Integer = 3
CONSTANT_Float = 4
CONSTANT_Long = 5
CONSTANT_Double = 6
CONSTANT_NameAndType = 12
CONSTANT_Utf8 = 1
CONSTANT_MethodHandle = 15
CONSTANT_MethodType = 16
CONSTANT_InvokeDynamic = 18
)
定義ConstantInfo介面來表示常量信息
type ConstantInfo interface {
readInfo(reader *ClassReader)
}
//讀取常量信息
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {...}
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {...}
readInfo()方法讀取常量信息,需要由具體的常量結構體實現。 readConstantInfo()函數先讀出tag值,然後調用newConstantInfo()函數創建具體的常量,最後調用常量的readInfo()方法讀取常量信息, 代碼如下:
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
tag := reader.readUint8()
c := newConstantInfo(tag, cp)
c.readInfo(reader)
return c
}
newConstantInfo()根據tag值創建具體的常量,代碼如下:
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
switch tag {
case CONSTANT_Integer:
return &ConstantIntegerInfo{}
case CONSTANT_Float:
return &ConstantFloatInfo{}
case CONSTANT_Long:
return &ConstantLongInfo{}
case CONSTANT_Double:
return &ConstantDoubleInfo{}
case CONSTANT_Utf8:
return &ConstantUtf8Info{}
case CONSTANT_String:
return &ConstantStringInfo{cp: cp}
case CONSTANT_Class:
return &ConstantClassInfo{cp: cp}
case CONSTANT_Fieldref:
return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_Methodref:
return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_InterfaceMethodref:
return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_NameAndType:
return &ConstantNameAndTypeInfo{}
case CONSTANT_MethodType:
return &ConstantMethodTypeInfo{}
case CONSTANT_MethodHandle:
return &ConstantMethodHandleInfo{}
case CONSTANT_InvokeDynamic:
return &ConstantInvokeDynamicInfo{}
default:
panic("java.lang.ClassFormatError: constant pool tag!")
}
}
CONSTANT_Integer_info
CONSTANT_Integer_info
使用4位元組存儲整數常量,其JVM結構定義如下:
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Integer_info
和後面將要介紹的其他三種數字常量無論是結構,還是實現,都非常相似,所以把它們定義在同一個文件中。創建cp_numeric.go
文件,在其中定義 ConstantIntegerInfo結構體
,代碼如下:
package classfile
import "math"
type ConstantIntegerInfo struct {
val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}
readInfo()先讀取一個uint32
數據,然後把它轉型成int32
類型, 代碼如下
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = int32(bytes)
}
CONSTANT_Float_info
CONSTANT_Float_info
使用4位元組存儲IEEE754單精度浮點數
常量,JVM結構如下:
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
在cp_numeric.go
文件中定義ConstantFloatInfo結構體
,代碼如下:
type ConstantFloatInfo struct {
val float32
}
func (self *ConstantFloatInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = math.Float32frombits(bytes)
}
CONSTANT_Long_info
CONSTANT_Long_info
使用8位元組存儲整數常量,結構如下:
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
在cp_numeric.go
文件中定義ConstantLongInfo結構體
,代碼如下:
type ConstantLongInfo struct {
val int64
}
func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = int64(bytes)
}
CONSTANT_Double_info
最後一個數字常量是CONSTANT_Double_info
,使用8位元組存儲IEEE754雙精度浮點數
,結構如下:
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
在cp_numeric.go
文件中定義ConstantDoubleInfo
結構體,代碼如下:
type ConstantDoubleInfo struct {
val float64
}
func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = math.Float64frombits(bytes)
}
CONSTANT_Utf8_info
CONSTANT_Utf8_info常量里放的是MUTF-8編碼的字元串, 結構如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
Java類文件中使用MUTF-8(Modified UTF-8)編碼而不是標準的UTF-8,是因為MUTF-8在某些方面更適合於在Java虛擬機內部處理字元串。以下是一些原因:
- 空字元的表示: 在標準的UTF-8編碼中,空字元(U+0000)會使用單個位元組0x00表示,這與C字元串中的字元串終止符相同,可能引起混淆。在MUTF-8中,空字元會使用0xC0 0x80來表示,避免了混淆。
- 編碼長度: MUTF-8編碼中的每個字元都使用1至3個位元組來表示,這與UTF-8編碼相比更緊湊。對於大多數常見的字元集,這可以減少存儲和傳輸開銷。
- 字元的編碼範圍: MUTF-8編碼對字元的範圍進行了限制,只包含Unicode BMP(基本多文種平面)範圍內的字元。這些字元通常足夠用於表示Java標識符和字元串文字。
- 相容性: 早期版本的Java使用的是MUTF-8編碼,因此為了保持與早期版本的相容性,後續版本也繼續使用MUTF-8。這有助於確保Java類文件的可互操作性。
創建cp_utf8.go
文件,在其中定義 ConstantUtf8Info結構體
,代碼如下:
type ConstantUtf8Info struct {
str string
}
func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
length := uint32(reader.readUint16())
bytes := reader.readBytes(length)
self.str = decodeMUTF8(bytes)
}
Java序列化機制也使用了MUTF-8編碼。
java.io.DataInput和 java.io.DataOutput介面分別定義了
readUTF()
和writeUTF()
方法,可以讀寫MUTF-8編碼的字元串。
如下為簡化版的java.io.DataInputStream.readUTF()
// mutf8 -> utf16 -> utf32 -> string
func decodeMUTF8(bytearr []byte) string {
utflen := len(bytearr)
chararr := make([]uint16, utflen)
var c, char2, char3 uint16
count := 0
chararr_count := 0
for count < utflen {
c = uint16(bytearr[count])
if c > 127 {
break
}
count++
chararr[chararr_count] = c
chararr_count++
}
for count < utflen {
c = uint16(bytearr[count])
switch c >> 4 {
case 0, 1, 2, 3, 4, 5, 6, 7:
/* 0xxxxxxx*/
count++
chararr[chararr_count] = c
chararr_count++
case 12, 13:
/* 110x xxxx 10xx xxxx*/
count += 2
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", count))
}
chararr[chararr_count] = c&0x1F<<6 | char2&0x3F
chararr_count++
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx*/
count += 3
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-2])
char3 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", (count - 1)))
}
chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0
chararr_count++
default:
/* 10xx xxxx, 1111 xxxx */
panic(fmt.Errorf("malformed input around byte %v", count))
}
}
// The number of chars produced may be less than utflen
chararr = chararr[0:chararr_count]
runes := utf16.Decode(chararr)
return string(runes)
}
- 初始化
chararr
數組,用於存儲UTF-16字元。 - 遍歷MUTF-8位元組數組中的位元組,根據位元組的值來判斷字元的編碼方式。
- 如果位元組值小於128,表示ASCII字元,直接轉換為UTF-16並存儲。
- 如果位元組值在特定範圍內,表示多位元組字元,需要根據UTF-8編碼規則進行解碼。
- 如果遇到不符合規則的位元組,拋出異常來處理錯誤情況。
- 最後,將解碼後的UTF-16字元轉換為Go字元串。
CONSTANT_String_info
CONSTANT_String_info
常量表示java.lang.String
字面量,結構如下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
可以看到,CONSTANT_String_info
本身並不存放字元串數據
只存了常量池索引
,這個索引指向一個CONSTANT_Utf8_info常量
。
下創建cp_string.go
文件,在其中定義 ConstantStringInfo結構體
type ConstantStringInfo struct {
cp ConstantPool
stringIndex uint16
}
func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
self.stringIndex = reader.readUint16()
}
String()
方法按索引從常量池中查找字元串:
func (self *ConstantStringInfo) String() string {
return self.cp.getUtf8(self.stringIndex)
}
CONSTANT_Class_info
CONSTANT_Class_info
常量表示類或者介面的符號引用
他是對類或者介面的符號引用。它描述的可以是當前類型的信息,也可以描述對當前類的引用,還可以描述對其他類的引用。JVM結構如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
和CONSTANT_String_info
類似,name_index
是常量池索引,指向CONSTANT_Utf8_info
常量。
創建 cp_class.go
文件,定義ConstantClassInfo結構體
type ConstantClassInfo struct {
cp ConstantPool
nameIndex uint16
}
func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
self.nameIndex = reader.readUint16()
}
func (self *ConstantClassInfo) Name() string {
return self.cp.getUtf8(self.nameIndex)
}
CONSTANT_NameAndType_info
CONSTANT_NameAndType_info
給出欄位或方法的名稱和描述符。
CONSTANT_Class_info
和CONSTANT_NameAndType_info
加在 一起可以唯一確定一個欄位或者方法。其結構如下:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
欄位或方法名由name_index
給出,欄位或方法的描述符由 descriptor_index
給出。
name_index
和descriptor_index
都是常量池索引,指向CONSTANT_Utf8_info常量
。
Java虛擬機規範定義了一種簡單的語法來描述欄位和方法,可以根據下麵的規則生成描述符。
一、類型描述符
- 基本類型byte、short、char、int、long、float和double的描述符是單個字母,分別對應B、S、C、I、J、F和D。註意,long的描述符是J 而不是L。
- 引用類型的描述符是L+類的完全限定名+分號。
- 數組類型的描述符是[+數組元素類型描述符
二、欄位描述符
欄位類型的描述符
三、方法描述符
分號分隔的參數類型描述符+返回值類型描述符,其中void返回值由單個字母V表示。
