摘要:但是這種方式對于接口和抽象方法是不管用的,因為抽象方法沒有方法體,也就沒有局部變量,自然也就沒有局部變量表了是通過接口跟語句綁定然后生成代理類來實現的,因此它無法通過解析字節碼來獲取方法參數名。
聲明:本文屬原創文章,首發于公號:程序員自學之道,轉載請注明出處!發現問題
對Java字節碼有一定了解的朋友應該知道,Java 在編譯的時候,默認會將方法參數名丟棄,因此我們無法在運行時獲取參數名稱。但是在使用 SpringMVC 的時候,我發現一個奇怪的現象,當我們需要接收請求參數的時候,相應的 Controller 方法只需要正常聲明,就可以直接接收正確的參數,例如:
注:以下例子使用 maven 進行編譯,且非 SpringBoot 項目,SpringBoot 已經自動解決了參數名解析的問題,后面咱們會討論
@RestController @RequestMapping("calculator") public class CalculatorController { @GetMapping("add") public int add(int aNum, int bNum) { return aNum + bNum; } }
當接收到 http://localhost:8080/calculator/add?aNum=12&bNum=3 這樣的請求時,會返回 15,即aNum 和 bNum 都能被正確解析。
然而,當我們使用 MyBatis 時,如果接口方法有多個參數而且我們沒有打上 @Param 注解的話,執行的時候就會報錯。例如,我們有如下的接口:
@Mapper public interface AccountMapper { @Select("select * from `account` where `name` = #{name} and mobile_phone = #{mobilePhone}") Account getByNameAndMobilePhone(String name, String mobilePhone); }
方法中包含兩個參數,但是沒有打上 @Param 注解,這時候如果調用這個方法,會報錯:
org.apache.ibatis.binding.BindingException: Parameter "name" not found. Available parameters are [arg1, arg0, param1, param2]
從錯誤信息中可以看出,是因為 MyBatis 沒有正確解析方法參數名稱導致異常。
這就很奇怪了,為什么 Spring 可以正確解析方法參數名稱,但是 MyBatis 卻不行?Java編譯的時候不是默認會將方法參數名丟棄嗎?我只是普通編譯,并沒有做特殊處理,那Spring又是從哪里找到方法參數名的呢?
帶著這些問題,我開始進行研究和探索。
獲取參數名的幾種方式通過查閱各種資料,我知道,獲取參數名稱的方式主要有兩種。
一、-g 參數當我們對 Java 源碼進行編譯時,無論是直接使用命令行還是使用 IDE 為我們編譯,實際上最終都是調用 javac 命令進行的,在編譯的時候,我們如果添加上 -g 參數,即告訴編譯器,我們需要調試信息,這時,生成的字節碼當中就會包含局部變量表的信息(方法參數也是局部變量),于是我們就可以通過解析字節碼獲取參數名了。
我們用最最經典的 HelloWorld 程序中的 main 方法為例,看一下編譯的效果:
public class HelloWorld{ public static void main(String[] argsName){ System.out.println("HelloWorld!"); } }
我們直接執行如下 javac 命令來編譯并查看生成的字節碼信息:
javac HelloWorld.java javap -verbose HelloWorld.class
可以看到,我們的參數名 argsName 已經被抹掉了。而如果字節碼中都沒有我們所需要的信息,那么在運行時,反射或者是別的方法也都無能為力了,巧婦難為無米之炊吶。
接下來,我們試一下添加 -g 參數會發生什么:
javac -g HelloWorld.java javap -verbose HelloWorld.class
可以看到,這里多了一個 LocalVariableTable,即局部變量表,其中就有我們的參數名稱 argsName!
那么,我們如何在方法運行時從字節碼信息中獲取參數名稱呢?你可以直接通過 javap 來獲取字節碼信息,然后自己去根據信息的格式去解析,然而這樣太低效了,而且太繁瑣了。
這時候如果我們請大名鼎鼎的 ASM 來當“導游”,帶著我們游覽字節碼內部構造,實現起來就輕松多了。
這個 ASM 可牛了,它不僅可以查看字節碼的信息,甚至可以動態修改類的定義或者新建一個原本沒有的類!在各種框架中被廣泛地使用,SpringAOP中使用的 CGLib 底層就是使用 ASM 來實現的。有興趣可以查看官網:https://asm.ow2.io/ 之前我也寫過一篇文章《Java用ASM寫一個HelloWorld程序》,有興趣可以看一下。
言歸正傳,如何通過 ASM 來獲取參數名稱呢? 直接上代碼:
首先添加依賴:
asm asm 3.3.1
/** * 使用字節碼工具ASM來獲取方法的參數名 */ public static String[] getMethodParamNames(final Method method) throws IOException { final int methodParameterCount = method.getParameterTypes().length; final String[] methodParametersNames = new String[methodParameterCount]; ClassReader cr = new ClassReader(method.getDeclaringClass().getName()); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cr.accept(new ClassAdapter(cw) { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); final Type[] argTypes = Type.getArgumentTypes(desc); //參數類型不一致 if (!method.getName().equals(name) || !matchTypes(argTypes, method.getParameterTypes())) { return mv; } return new MethodAdapter(mv) { @Override public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) { //如果是靜態方法,第一個參數就是方法參數,非靜態方法,則第一個參數是 this, 然后才是方法的參數 int methodParameterIndex = Modifier.isStatic(method.getModifiers()) ? index : index - 1; if (0 <= methodParameterIndex && methodParameterIndex < methodParameterCount) { methodParametersNames[methodParameterIndex] = name; } super.visitLocalVariable(name, desc, signature, start, end, index); } }; } }, 0); return methodParametersNames; } /** * 比較參數是否一致 */ private static boolean matchTypes(Type[] types, Class>[] parameterTypes) { if (types.length != parameterTypes.length) { return false; } for (int i = 0; i < types.length; i++) { if (!Type.getType(parameterTypes[i]).equals(types[i])) { return false; } } return true; }
簡而言之,ASM使用了訪問者模式,它就像一個導游,帶著我們去游覽字節碼文件中的各個“景點”。我們實現不同的 Visitor 接口就像是手上握有不同景點門票的游客,導游會帶著 ClassVisitor 去總體參觀類定義的景觀,而類內部有方法,如果你想看一下方法內部的定義,需要"額外購票",即需要實現 MethodVisitor 才能跟著導游去參觀方法定義這個景點。而在游覽各個景點的時候,我們可以只游覽我們感興趣的部分,這就可以繼承適配器(ClassAdapter和MethodAdapter分別是ClassVisitor和MethodVisitor的適配器)然后只實現我們感興趣的方法即可。
這里對于類的定義,我們只對方法感興趣,因此只實現 visitMethod 方法;在方法中,我們只對 LocalVariableTable 有興趣,因此只實現 visitLocalVariable 方法。這樣我們得到了局部變量表,再根據一些規則就可以拿到我們的參數名稱了!是不是很棒!
順便說一下,如果你使用 maven 來管理項目的話,這個 -g 參數會在編譯的時候自動加上,因此我們不需要額外添加就可以通過字節碼拿到,這也就是為什么 SpringMVC 可以拿到方法參數名稱的原因。
但是這種方式對于接口和抽象方法是不管用的,因為抽象方法沒有方法體,也就沒有局部變量,自然也就沒有局部變量表了:
MyBatis 是通過接口跟 SQL 語句綁定然后生成代理類來實現的,因此它無法通過解析字節碼來獲取方法參數名。
雖然通過字節碼的方法的確可以拿到參數名,但還是不方便,而且它對接口和抽象方法的參數名也無能為力。有沒有更方便更全面的方法呢?答案是:有的。
-parameters 參數JDK8 在反射包中引入了 java.lang.reflect.Parameter 來獲取參數相關的信息:
A small but useful example is support for method parameter names at run time: storing such names in the class file structure goes hand in hand with offering a standard API to retrieve them (java.lang.reflect.Parameter) - 《The Java Virtual Machine Specification》
但是它依賴于編譯時添加 -parameters 參數,也就是說,只有在編譯的時候添加了這個參數才能在運行時通過反射獲取參數信息。還是用我們的 HelloWorld 程序,我們來試一下添加 -parameters 參數:
javac -parameters HelloWorld.java javap -verbose HelloWorld.class
可以看到,字節碼文件中多了 MethodParameters 部分,里面存放的就直接是我們所需要在的參數名!我們可以直接通過反射獲取:
HelloWorld.class.getMethod("main",String[].class).getParameters()[0].getName()
問題來了,我們如何在編譯的時候自動加上 -parameters 這個參數呢?畢竟我們不可能只在自己的 IDE 上做設置,也不可能自己寫腳本來編譯。
如果你使用 maven 來管理項目的話,可以直接通過插件來完成:
org.apache.maven.plugins maven-compiler-plugin 3.8.0 ${java.version} true
這樣這個 -parameters 參數就會在編譯的時候自動加上了。
關于 SpringBoot文章開頭曾提到,SpringBoot 已經自動解決了參數名解析的問題,它其實就是通過 -parameters 參數來實現的。在 spring-boot-starter-parent.pom 文件中它為我們添加了上面提到的插件及參數:
有了這個參數而且是在 JDK8+ 中運行的話無論是 SpringMVC 還是 MyBatis 都可以獲取到正確的方法參數名了!
獲取參數名稱的方式主要有兩種:
編譯時添加 -g 參數,然后通過解析字節碼讀取局部變量表獲取
maven在編譯時會自動添加這個參數,但是用的時候需要解析字節碼,而且對于接口和抽象方法無能為力,因為接口和抽象方法沒有方法體,也就沒有局部變量,因此也就沒有局部變量表,所以無法通過局部變量表來獲取參數名稱。
JDK8+ 編譯時添加 -parameters 參數,然后通過反射獲取
可以通過配置插件自動添加,使用非常方便,直接通過反射即可拿到參數信息。但是需要 JDK8 及以上才能使用。
SpringMVC 和 MyBatis :
有 -parameters 參數的場景,兩個框架都可以正確解析參數名。
只有 -g 參數時:
SpringMVC 通過解析字節碼獲取 Controller 的方法參數以綁定請求參數
MyBatis 需要與接口綁定,而 -g 參數對接口和抽象類無效,因此不能正確解析參數名
-g 和 -parameters 都沒有時,兩者都無法正確解析參數名
后記不知不覺寫了這么多,現在也快凌晨兩點了。
對于獲取方法參數名這個問題的探究最早其實是來源于我在寫 http-api-invoker (github 地址:https://github.com/dadiyang/h... 這個框架的時候意識到的。這個框架跟MyBatis類似,它將接口與 url 進行綁定然后生成代理類來發送 http 請求,我們無需關注參數拼接和序列化、請求發送和返回值處理的過程,只需要定義好我們的接口并打上注解即可。
在不斷優化和使用的過程中我發現,每個接口方法都需要打 @Param 注解太麻煩,而 MyBatis 也同樣有這個問題,然而 SpringMVC 卻可以解決。因此為了更加完善這個框架,我開始一探究竟。做了很多的功課,把整個來龍去脈都了解清楚了,但是一直沒有時間整理。
現在終于忙里偷閑趁著周末把這篇文章寫出來了,可惜由于最近我讓 http-api-invoker 框架兼容到 JDK6,還沒有想好怎樣讓它在支持 JDK6 的前提下更好地利用 JDK8+ 的 -parameters 特性。這個留到以后再做進一步的探索吧。
更多原創好文,請關注程序員自學之道
參考文獻:asm4-guide
The Java Virtual Machine Specification
spring mvc如何實現參數名綁定
使用ASM獲得JAVA類方法參數名
How to compile Spring Boot applications with Java 8 --parameter flag
Java用ASM寫一個HelloWorld程序
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77803.html
摘要:阿里云推出國內首個基于英偉達的優化容器月日,在云棲大會深圳峰會上,阿里云宣布與英偉達云合作,開發者可以在云市場下載云鏡像和運行容器,來使用阿里云上的計算平臺。阿里云成為中國首家與加速的容器合作的云廠商。 摘要: 3月28日,在2018云棲大會·深圳峰會上,阿里云宣布與英偉達GPU 云 合作 (NGC),開發者可以在云市場下載NVIDIA GPU 云鏡像和運行NGC 容器,來使用阿里云上...
閱讀 1296·2021-11-04 16:09
閱讀 3483·2021-10-19 11:45
閱讀 2395·2021-10-11 10:59
閱讀 1009·2021-09-23 11:21
閱讀 2762·2021-09-22 10:54
閱讀 1128·2019-08-30 15:53
閱讀 2599·2019-08-30 15:53
閱讀 3476·2019-08-30 12:57