前段時間,在做一個小的工程時,遇到了需要通過反射獲得方法真實引數名的場景,在這裡我遇到了一些小小的問題,後來在部門老大的指導下,我解決了這個問題。通過解決這個問題,附帶著我瞭解到了很多新的知識,我覺得有必要和大家分享交流一下。
我們們先來看這樣一個小的demo:
這是一個很簡單的小demo,裡面就是一個簡簡單單的類Test1
,Test1
有一個包含兩個引數的方法test
,在Test1
的main
方法中通過射來獲得test
方法的所有引數的名字,並將其輸出到標準流。我本以為這個demo的執行結果會得到方法的引數名,結果:
驚不驚喜,意不意外?和說好的不一樣啊!
我們們先停一下,先把為什麼反射沒有拿到正確的值放到一邊,先說說我為什麼要研究“通過反射原理獲得方法引數的實際名稱”這件事呢:是因為我想仿照並實現Spring MVC中的“自動繫結”功能。大家知道Spring MVC裡有一個“自動繫結”的功能,能夠自動繫結請求引數的值到@RequestMapping
方法的引數上的,而不用任何額外的操作。
這個功能我覺得很方便,所以我想嘗試自己仿造這個功能,然後用在公司的專案開發中。我猜測Spring是通過反射獲得方法的引數名後根據引數名到request
中getParam(String name)
來獲得實際的值然後繫結的。因此我就嘗試著按照這個思路做,結果就遇到了上邊提到的反射獲得不了引數實際名稱的問題。我將這個問題請教了老大,老大瞭解到我的意圖後,經過驗證,得出結論:Spring MVC能不能正常使用自動繫結是與java編譯器編譯時加不加-g
引數有關的,而這個-g
引數是代表著java編譯器在編譯時是否會輸出除錯資訊。
其實也就是說:Spring是通過讀取java編譯器生成的除錯資訊從而獲得的方法中引數的真實名稱的。說到這裡,這個問題基本也解決了,但是我還是想再多說一點我後續的學習結果。後續我研究了一下Spring對於方法引數這塊的處理邏輯,也就是對於“自動繫結”功能的底層的實現。
那麼,Spring 到底是用了什麼“黑科技”來做到獲得方法實際引數名的呢,我們們不妨就看Spring的原始碼吧,看看Spring到底是如何實現的。
Spring海量的原始碼,從何看起呢,這裡,我是這樣解決的:我大體知道這個獲得方法實際引數名的操作應當和Method
的getParameters()
方法有關,或者說它的方法裡或許會呼叫到這個方法,那麼好了,我們可以使用idea
提供的“檢視呼叫棧”的功能,來順藤摸瓜,看看在Spring中有沒有呼叫到這個方法,如果有,那麼解決方案應當就在呼叫方法的附近。
我們可以看到,果不其然,在呼叫棧裡就有org.spring
包中的方法,其中有兩個方法都是StandardReflectionParameterNameDiscoverer
類的方法,其實我們已經找到了,看這個類的名字就能知道,它是處理ParameterName
的Discoverer的(在這裡我想再說點題外話,我個人非常贊同Spring這種全命名的編碼風格,看到命名就能看明白這個類是在幹什麼,所以說程式碼應當是能“自描述”的)
好,我們再回到程式碼中來,繼續看這個類:發現它有一段簡要的註釋:
大意就是這個類是針對使用了JDK8基於-parameters
編譯引數的ParameterNameDiscoverer
的實現,這裡這個-parameters
引數是怎麼回事我們們先放一邊,繼續向上看StandardReflectionParameterNameDiscoverer
所實現的這個介面ParameterNameDiscoverer
,開啟ParameterNameDiscoverer
這個介面,我們用idea的檢視子類的功能,能夠看到它一共有包括StandardReflectionParameterNameDiscoverer
在內的8
個子類
其中有一個名字裡帶“Default”的子類DefaultParameterNameDiscoverer
,按照一般套路來說,帶Default的都是預設的實現,那麼好了我們優先看它吧。
開啟DefaultParameterNameDiscoverer
,我們發現,他做的大體就是通過判斷standardReflectionAvailable
這個值來走向不同分支流程:一個是走向剛才提到的利用JDK8編譯引數的StandardReflectionParameterNameDiscoverer
另一個是走向了LocalVariableTableParameterNameDiscoverer
好,現在又出現了熟悉的StandardReflectionParameterNameDiscoverer
了,那麼我們返回去看吧,一會再看另一個分支的LocalVariableTableParameterNameDiscoverer
。
我們回到StandardReflectionParameterNameDiscoverer
中,再來看剛才那個-parameters
編譯引數,這是個什麼黑科技?既然他是個編譯引數,那麼我們們不妨試著用它編譯一下我們們的程式碼試一下吧。
我們將idea
設定上-parameters
編譯引數從新執行剛才的demo,發現這回的輸出結果是:
已經能夠拿到引數的真實名稱了。那麼,這個-parameters
到底是什麼呢:我們可以來看一下oracle官方提供的javac文件:
通過文件可以看出加上這個引數後,編譯器會生成後設資料,從而使方法引數的反射能夠拿到引數的資訊。
這個功能是jdk8的新特性,我們就不仔細展開了,詳情可以檢視這兩篇文件:
JDK 8 Features
JEP 118: Access to Parameter Names at Runtime
-parameters
這個黑科技我們們已經瞭解了,利用這個編譯引數是可以獲得方法引數的真實名稱的,但是這個引數是jdk8之後才有的,那麼之前的版本如何獲得呢?我們繼續看Spring原始碼吧。現在我們來看另一個分支:LocalVariableTableParameterNameDiscoverer
,開啟這個類:
其實看註釋就明白了,這個LocalVariableTableParameterNameDiscoverer
是通過ASM library
分析LocalVariableTable
來實現獲得引數實際名稱的,ASM
是一個第三方的位元組碼操縱庫,用這個庫可以讀取寫入class檔案,這個庫有很廣泛的應用,具體的我不展開介紹了。
我們重點說一下這個LocalVariableTable
吧,這個LocalVariableTable
是什麼呢?我們不用文字來說明了,直接來看程式碼吧:
我們這次不看原始檔了,來直接看編譯後的class檔案。用idea
開啟Test1.class
:
然後在View
選單中點選Show Bytecode
:
在彈出視窗中,我們可以看到,idea
以大綱的方式把class
檔案的資訊列了出來,而在其中就有LocalVariableTable
存在,而且在“LocalVariableTable”附近我們可以看到我們定義方法的引數的真實名稱。現在我們也就明白了,對於8以下的jdk編譯環境,Spring是使用ASM來讀取class
檔案中LocalVariableTable
資訊從而獲得引數真實名稱的。
到此為止,我們已經基本瞭解了Spring中自動繫結背後的黑科技了。
這裡我還想繼續再多說一點,有關LocalVariableTable
和Java class
檔案:class
檔案可以說是Java實現跨平臺特性的根本!不管在什麼平臺下,只要編譯出來的class
檔案符合規範,虛擬機器就能夠正常的執行。瞭解一下class
檔案的相關知識其實對於理解各類class
檔案操縱庫以及基於class
操縱的AOP
等等程式設計模式的原理是很有幫助的,所以我們可以瞭解一下class
檔案是什麼樣的結構的。想要了解class
檔案的結構,最權威的莫過於官方的《Java虛擬機器規範了》,在Java虛擬機器規範中,第四章是有關class
檔案結構的內容,我們可以大致過一遍。
通過閱讀,我們可以大致瞭解到class
的結構:
A class file consists of a stream of 8-bit bytes. All 16-bit, 32-bit, and 64-bit
quantities are constructed by reading in two, four, and eight consecutive 8-bit
bytes, respectively. Multibyte data items are always stored in big-endian order,
where the high bytes come first. In the Java SE platform, this format is supported
by interfaces java.io.DataInput and java.io.DataOutput and classes such as
java.io.DataInputStream and java.io.DataOutputStream.
class檔案可以用一個結構來表示:
這個結構中每一項大致的含義我們來簡單說明一下吧(詳情請檢視虛擬機器規範):
開頭的magic
u4
叫做“魔數”,Java虛擬器通過讀取這個數來判斷當前檔案是不是有效的u4
代表它是無符號
的4
個byte
,這個數始終應該是0xCAFEBABE
;
minor_version
、major_version
分別是class
檔案的次版本
和主版本
;
u2
constant_pool_count
、cp_info
constant_pool[constant_pool_count-1]
代表常量池中專案數和代表了常量池本身;
u2
access_flags
: 代表class
訪問標記,例如:public protected;
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]
; java class
檔案內部屬性資訊集合大小和內部屬性資訊集合本身。這裡提一下,我們前面的提到的LocalVariableTable
的資訊就儲存在這裡。
到了這裡我們大致回顧一下吧,我們從嘗試解決反射獲得方法引數真實名稱開始,瞭解了Java編譯引數、Spring自動繫結相關處理原理、jdk8編譯引數新特性、以及Java class
檔案的結構。通過這個過程,我們看到,就一個“自動繫結”這個平常都感覺不到它存在的小功能背後,還有這莫多深層次的技術在裡面,由此可見,Spring之所以如此強大而且易用,離不開各類底層技術的支援,這就讓我想起以前看到過的一位技術博主的標語:“只有深入,方能淺出”,想想確實是這個道理。
注:
在研究過程中我參考以下幾位的文章,在此表示感謝:
反射獲取一個方法中的引數名(不是型別)
Java 執行時獲取方法引數名
java Class檔案內部結構解析
深入理解JVM : class檔案結構之常量池(1)
深入理解JVM : class檔案結構之類資訊描述、欄位表、方法表(2)
觸控java常量池
實現一個Java Class解析器的實力程式碼分享
Java class file
Tutorial: Java Class file format, revealed…
The Java Class File Format
The Java class file lifestyle
An introduction to the basic structure and lifestyle of the Java class file