如何通過反射獲得方法的真實引數名(以及擴充套件研究)

since1986發表於2017-11-07

前段時間,在做一個小的工程時,遇到了需要通過反射獲得方法真實引數名的場景,在這裡我遇到了一些小小的問題,後來在部門老大的指導下,我解決了這個問題。通過解決這個問題,附帶著我瞭解到了很多新的知識,我覺得有必要和大家分享交流一下。

我們們先來看這樣一個小的demo:

這是一個很簡單的小demo,裡面就是一個簡簡單單的類Test1Test1有一個包含兩個引數的方法test,在Test1main方法中通過射來獲得test方法的所有引數的名字,並將其輸出到標準流。我本以為這個demo的執行結果會得到方法的引數名,結果:

驚不驚喜,意不意外?和說好的不一樣啊!

我們們先停一下,先把為什麼反射沒有拿到正確的值放到一邊,先說說我為什麼要研究“通過反射原理獲得方法引數的實際名稱”這件事呢:是因為我想仿照並實現Spring MVC中的“自動繫結”功能。大家知道Spring MVC裡有一個“自動繫結”的功能,能夠自動繫結請求引數的值到@RequestMapping方法的引數上的,而不用任何額外的操作。

這個功能我覺得很方便,所以我想嘗試自己仿造這個功能,然後用在公司的專案開發中。我猜測Spring是通過反射獲得方法的引數名後根據引數名到requestgetParam(String name)來獲得實際的值然後繫結的。因此我就嘗試著按照這個思路做,結果就遇到了上邊提到的反射獲得不了引數實際名稱的問題。我將這個問題請教了老大,老大瞭解到我的意圖後,經過驗證,得出結論:Spring MVC能不能正常使用自動繫結是與java編譯器編譯時加不加-g引數有關的,而這個-g引數是代表著java編譯器在編譯時是否會輸出除錯資訊。


其實也就是說:Spring是通過讀取java編譯器生成的除錯資訊從而獲得的方法中引數的真實名稱的。說到這裡,這個問題基本也解決了,但是我還是想再多說一點我後續的學習結果。後續我研究了一下Spring對於方法引數這塊的處理邏輯,也就是對於“自動繫結”功能的底層的實現。

那麼,Spring 到底是用了什麼“黑科技”來做到獲得方法實際引數名的呢,我們們不妨就看Spring的原始碼吧,看看Spring到底是如何實現的。

Spring海量的原始碼,從何看起呢,這裡,我是這樣解決的:我大體知道這個獲得方法實際引數名的操作應當和MethodgetParameters()方法有關,或者說它的方法裡或許會呼叫到這個方法,那麼好了,我們可以使用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代表它是無符號4byte,這個數始終應該是0xCAFEBABE

minor_versionmajor_version分別是class檔案的次版本主版本

u2 constant_pool_countcp_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

相關文章