不要爲了讀文章而讀文章,必定要帶着問題來讀文章,勤思考。java
更多關於Java的技術和資訊能夠關注個人專欄:【架構名人堂】 專欄免費給你們分享Java架構的學習資料和視頻
對Java字節碼有必定了解的朋友應該知道,Java 在編譯的時候,默認不會保留方法參數名,所以咱們沒法在運行時獲取參數名稱。可是在使用 SpringMVC 的時候,我發現一個奇怪的現象:當咱們須要接收請求參數的時候,相應的 Controller 方法只須要正常聲明,就能夠直接接收正確的參數,例如:apache
注:如下例子使用 maven 進行編譯,且非 SpringBoot 項目,SpringBoot 已經自動解決了參數名解析的問題,後面我們會討論bash
@RestController
@RequestMapping("calculator")
public class CalculatorController {
@GetMapping("add")
public int add(int aNum, int bNum) {
return aNum + bNum;
}
}複製代碼
當接收到 /calculator/add?aNum=12&bNum=3 這樣的請求時,會返回 15,即aNum 和 bNum 都能被正確解析。然而,當咱們使用 MyBatis 時,若是接口方法有多個參數並且咱們沒有打上 @Param 註解的話,執行的時候就會報錯。例如,咱們有以下的接口:架構
@Mapper
public interface AccountMapper {
Account getByNameAndMobilePhone(String name, String mobilePhone);
}複製代碼
方法中包含兩個參數,可是沒有打上 @Param 註解,這時候若是調用這個方法,會報錯:app
org.apache.ibatis.binding.BindingException: Parameter ‘name’ not found.
Available parameters are [arg1, arg0, param1, param2]複製代碼
從錯誤信息中能夠看出,是由於 MyBatis 沒有正確解析方法參數名稱致使異常。這就很奇怪了,爲何 Spring 能夠正確解析方法參數名稱,可是 MyBatis 卻不行?Java編譯的時候默認會將方法參數名抹除,但我並無作特殊處理,Spring 又是從哪裏找到方法參數名的呢?帶着這些問題,我開始進行研究和探索。框架
經過查閱各類資料,我知道了獲取參數名稱的方式。maven
-g 參數ide
當咱們對 Java 源碼進行編譯時,不管是直接使用命令行仍是使用 IDE 爲咱們編譯,實際上最終都是調用 javac 命令進行的,在編譯的時候,咱們若是添加上 -g 參數,即告訴編譯器,咱們須要調試信息,這時,生成的字節碼當中就會包含局部變量表的信息(方法參數也是局部變量),因而咱們就能夠經過解析字節碼獲取參數名了。工具
咱們用最最經典的 HelloWorld 程序中的 main 方法爲例,看一下編譯的效果:學習
public class HelloWorld{
public static void main(String[] argsName){
System.out.println("HelloWorld!");
}
}複製代碼
咱們直接執行以下 javac 命令來編譯並使用 javap 命令查看生成的字節碼信息:
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 來實現的。
言歸正傳,如何經過 ASM 來獲取參數名稱呢? 直接上代碼:
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>複製代碼
使用字節碼工具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 語句綁定而後生成代理類來實現的,所以它沒法經過解析字節碼來獲取方法參數名。
---------------------