Java ASM詳解:類的結(jié)構(gòu)(二)
上次專欄講解了普通類的結(jié)構(gòu),這篇專欄將繼續(xù)講解接口、注解、枚舉和記錄這四種特殊的類。
一.接口
接口類似于抽象類,內(nèi)部含有靜態(tài)方法、公開抽象方法、公開默認(rèn)實(shí)例方法、私有實(shí)例方法和常量字段。
聲明一個(gè)接口,需要在ClassWriter::visit時(shí)同時(shí)使用ACC_ABSTRACT和ACC_INTERFACE訪問標(biāo)志。如果只含有ACC_INTERFACE標(biāo)志,在加載這個(gè)類的時(shí)候JVM就會(huì)拋出java.lang.ClassFormatError: Illegal class modifiers in class *: 0x200的異常。接口的繼承本質(zhì)其實(shí)是實(shí)現(xiàn),也就是說接口的父類仍然是Object,但是實(shí)現(xiàn)接口列表可以加入其它的接口。
如果接口是一個(gè)內(nèi)部類,在使用時(shí)類似于靜態(tài)內(nèi)部類,也就是說內(nèi)部接口不需要外部類實(shí)例作為依托。(但是在使用visit時(shí)不用寫ACC_STATIC,這是一種等效)同樣的,如果接口內(nèi)部有內(nèi)部類,那么內(nèi)部類也等效于靜態(tài)內(nèi)部類。
緊接著說說接口的字段。接口內(nèi)只能存在一種字段,那就是公開常量字段,也就是PSF(public static final)字段。在visitField時(shí)必須同時(shí)使用ACC_PUBLIC、ACC_STATIC和ACC_FINAL訪問標(biāo)志,否則在加載這個(gè)類的時(shí)候就會(huì)拋出java.lang.ClassFormatError: Illegal field modifiers in class *: *的異常。
接著來看看方法。接口不存在構(gòu)造函數(shù),并且只允許兩種訪問修飾符:public和private(引入于Java 9),protected不能在接口中使用,不寫訪問修飾符則默認(rèn)公開。
接口內(nèi)可以定義靜態(tài)方法,與普通的類沒有太多差別,只是訪問修飾有差別。對(duì)于接口內(nèi)的實(shí)例方法,在Java層如果不定義default或者private那就會(huì)自動(dòng)加上abstract,但是對(duì)于字節(jié)碼來說,除了訪問修飾外所有的定義都和普通的類一樣。
接口也可以使用橋接方法。
下面是個(gè)例子,要生成下面的類:
將使用下面的代碼:
二.注解
注解類型是特殊的接口,除了接口都具有的特性之外還增加了一些限制。
先說一下聲明。注解類型除了接口要求的兩個(gè)訪問標(biāo)志外還需要添加一個(gè)ACC_ANNOTATION標(biāo)志,且必須只實(shí)現(xiàn)(或者說是Java層的繼承)于java.lang.annotation.Annotation。(注意:JVM對(duì)實(shí)現(xiàn)Annotation接口這件事不加以檢查,但是如果你使用了反射嘗試獲取這個(gè)注解時(shí)會(huì)報(bào)錯(cuò))
注解對(duì)于方法的要求很嚴(yán)格:要求不能有私有方法、默認(rèn)方法和靜態(tài)方法,只能存在公開抽象方法。
到這里你可能會(huì)問:注解方法的默認(rèn)值是怎么實(shí)現(xiàn)的?其實(shí)默認(rèn)值不是方法體,而是使用了visitAnnotationDefault這個(gè)方法用于寫入默認(rèn)值。
下面我們要生成這個(gè)注解:
可以使用下面的代碼生成:
三.枚舉
枚舉是一種特殊的類,主要用于存貯常量。和普通的類相比,它有下面的性質(zhì):
所有枚舉都繼承于java.lang.Enum,并且都為final。
枚舉的所有構(gòu)造函數(shù)都是私有的。
自動(dòng)生成values和valueOf方法。
作為內(nèi)部類時(shí)等效靜態(tài)。
寫入一個(gè)枚舉,在visit時(shí)要將ACC_ENUM和ACC_FINAL訪問標(biāo)志同時(shí)寫入,并且父類必須寫為java/lang/Enum。由于Enum帶有泛型,所以signature也要寫入。例如,下面這個(gè)枚舉:
在聲明時(shí)必須用下面的代碼:
在使用Java編寫枚舉類時(shí)可以寫兩種字段:一種是普通的字段,這個(gè)和普通的類一樣,沒有限制;另一種就是枚舉字段,它必須是枚舉類的對(duì)象,并且是PSF字段還帶有ACC_ENUM訪問標(biāo)志。例如下方的枚舉字段A:
在聲明時(shí)應(yīng)該遵照下面的方式:
在字節(jié)碼中,除了上面的兩種字段外,枚舉中還有一個(gè)字段是系統(tǒng)生成用于保存所有枚舉字段的私有常量,$VALUES。它的訪問標(biāo)志除了ACC_PRIVATE、ACC_STATIC和ACC_FINAL外還帶有ACC_SYNTHETIC,這代表它是編譯時(shí)自動(dòng)生成的。它的類型是這個(gè)類的數(shù)組。
$VALUES存在的價(jià)值是為了values方法和Enum的索引。在介紹它的用途之前先來說說$VALUES和枚舉字段的初始化。
枚舉字段和普通常量的初始化一樣,也是簡單的創(chuàng)建、調(diào)用構(gòu)造函數(shù)和賦值。但是不同的是,枚舉的構(gòu)造函數(shù)和普通的構(gòu)造函數(shù)不同,它默認(rèn)帶有兩個(gè)形參。實(shí)際上,枚舉的默認(rèn)構(gòu)造函數(shù)是這樣的:
枚舉默認(rèn)構(gòu)造函數(shù)的寫入如下:
如果枚舉類有定義構(gòu)造函數(shù),那么在字節(jié)碼中仍然需要將這兩個(gè)形參添加到Java源代碼的形參列表之前,且必須調(diào)用Enum的這個(gè)父類構(gòu)造函數(shù)。
所以枚舉字段的創(chuàng)建對(duì)于上面的A來說就像下面這樣:
$VALUES的賦值不太一樣,它是委托到了另一個(gè)方法$values生成的。這個(gè)方法帶有ACC_PRIVATE、ACC_STATIC和ACC_SYNTHETIC訪問標(biāo)志,且方法返回值是該類的數(shù)組,形參列表為空。它的作用就是創(chuàng)建一個(gè)數(shù)組,并將所有枚舉字段按順序存儲(chǔ)進(jìn)數(shù)組之中并返回。對(duì)于上面的Test就是這樣的:
在靜態(tài)初始化中,$VALUES直接由$values的返回值賦值:
我們用到的values是另一個(gè)方法,也是由編譯器自動(dòng)生成的。它返回的是$VALUES的副本,Java的代碼像這樣:
// 可以注意到這個(gè)方法不存在try-catch,即使clone定義了拋出CloneNotSupportedException。JVM對(duì)這種異常處理不檢查,可以說在字節(jié)碼范圍內(nèi),異常處理是可有可無的。
字節(jié)碼像這樣:
另一個(gè)會(huì)自動(dòng)創(chuàng)建的方法,valueOf,用Java表示是這樣的:
字節(jié)碼寫入如下:
到這里一個(gè)完整的枚舉才寫完??梢钥吹轿覀儽仨殞懗?VALUES、$values、values、valueOf這些字段和方法,非常的麻煩。即使一個(gè)非常簡單的枚舉也必須有所有這些要素,所以枚舉的寫入很繁瑣,還要注意別忘了它的組件。
四.記錄
記錄也是一種特殊的類,它在Java 14開始加入。和普通的類相比,它有下面的不同之處:
不能單獨(dú)定義實(shí)例字段,所有終態(tài)實(shí)例字段都要在類之后的括號(hào)定義。
繼承于java.lang.Record,且都為final。
自動(dòng)生成toString、hashCode和equals。(除非自行定義)
作為內(nèi)部類時(shí)等效靜態(tài)。
寫入一個(gè)記錄,在visit時(shí)必須帶有ACC_FINAL和ACC_RECORD訪問標(biāo)志,并且要繼承java/lang/Record。接下來以下面的記錄作為例子:
在寫入時(shí)要這樣定義:
對(duì)于記錄的終態(tài)實(shí)例字段(也可以叫記錄字段),它只能含有ACC_PRIVATE和ACC_FINAL這兩個(gè)訪問標(biāo)志。它需要兩次定義:一次是普通的字段定義,使用visitField;另一次是記錄組件的定義,使用visitRecordComponent,在定義字段之前寫入。這里的a就像下面這樣定義:
每個(gè)記錄字段都有自動(dòng)生成的對(duì)應(yīng)的getter,代碼很簡單,就像下面這樣:
字節(jié)碼像這樣寫:
記錄的默認(rèn)構(gòu)造函數(shù)和記錄字段有關(guān),形參列表正好和記錄字段的順序一致。對(duì)于上面的Test,構(gòu)造函數(shù)是這樣的:
轉(zhuǎn)換成字節(jié)碼如下:
記錄必須有toString、hashCode、equals這三個(gè)方法,這是因?yàn)樵赗ecord中聲明了它們3個(gè)是抽象的。如果我們不自己寫這三個(gè)方法,那么系統(tǒng)在編譯的時(shí)候會(huì)自動(dòng)生成。
自動(dòng)生成的這三個(gè)方法都用到了invokedynamic,使用的引導(dǎo)方法都是java.lang.runtime.ObjectMethods.bootstrap。這個(gè)方法的定義是:
其中names是記錄實(shí)例字段的名稱序列,用分號(hào);隔開。
下面僅給出toString的代碼,另兩個(gè)除了methodName和type不同外沒有差別。
到這里記錄才算寫入完畢。

到這里類的結(jié)構(gòu)就結(jié)束了,接下來的文章將討論好玩的東西(因?yàn)檫€沒想出來)。
這系列專欄沒有特殊聲明都是Java 17的字節(jié)碼,請注意使用。
如果對(duì)文章內(nèi)容有問題或文章有錯(cuò)誤可以評(píng)論區(qū)或私信指出。