spring mvc @PathVariable動態參數
spring mvc中的@PathVariable是用來獲得請求url中的動態參數的,十分方便。
@Controller public class TestController { @RequestMapping(value="/user/{userId}/roles/{roleId}",method = RequestMethod.GET) public String getLogin(@PathVariable("userId") String userId, @PathVariable("roleId") String roleId){ System.out.println("User Id : " + userId); System.out.println("Role Id : " + roleId); return "hello"; } @RequestMapping(value="/product/{productId}",method = RequestMethod.GET) public String getProduct(@PathVariable("productId") String productId){ System.out.println("Product Id : " + productId); return "hello"; } @RequestMapping(value="/javabeat/{regexp1:[a-z-]+}", method = RequestMethod.GET) public String getRegExp(@PathVariable("regexp1") String regexp1){ System.out.println("URI Part 1 : " + regexp1); return "hello"; } }
spring mvc是如何做到根據參數名動態綁定參數的?
使用過SpringMVC的同學都知道,當我們需要在Controller層接收客戶端的請求參數時,只需要在形參上加@RequestParam注解,SpringMVC就會自動幫我們做參數綁定,如下示例:
@GetMapping("test1") public void test1(@RequestParam("name") String name, @RequestParam("age") Integer age) { }
客戶端請求示例:
curl http://127.0.0.1:8080/test1?name=root&age=18
每個參數都加注解寫起來非常的麻煩,因此SpringMVC還可以根據參數名自動匹配,只要方法的參數名和客戶端請求的參數名相同即可綁定,代碼可以簡化為:
@GetMapping("test2") public void test2(String name, Integer age) throws Exception { }
SpringMVC是如何做到的呢???
反射獲取參數名
熟悉SpringMVC的同學都知道,SpringMVC通過一個DispatcherServlet來分發客戶端的請求,根據請求的URI映射對應的處理器Handler,將請求交給對應的Handler處理,說白了就是通過反射的方式調用Controller的方法,然后將請求的參數解析,并和方法的形參做匹配并傳遞過去。
要想綁定參數,首先要做的就是知曉Controller的方法需要的參數名是什么???
對于第一種寫法,很好理解,方法想要的參數名就是@RequestParam注解的值,只需要通過反射來獲取即可,如下代碼:
public static void main(String[] args) throws Exception { Method test1 = UserController.class.getMethod("test1", String.class, Integer.class); for (Parameter parameter : test1.getParameters()) { RequestParam requestParam = parameter.getAnnotation(RequestParam.class); System.err.println("test1-參數名:" + requestParam.value()); } } 控制臺輸出: test1-參數名:name test1-參數名:age
但是對于第二種簡化的寫法,是無法通過反射來獲取參數名稱的,如下:
public static void main(String[] args) throws Exception { Method test2 = UserController.class.getMethod("test2", String.class, Integer.class); for (Parameter parameter : test2.getParameters()) { System.err.println("test2-參數名:"+parameter.getName()); } }
你們猜猜拿到的參數名是什么???
竟然是沒有任何意義的arg0、arg1!!!
這是為什么呢???
熟悉JVM的同學都知道,Java代碼要想在JVM里執行,首先需要通過javac命令編譯成字節碼Class文件,而這個編譯的過程會直接將方法的參數名稱丟棄,變成無意義的arg0、arg1…,因此通過反射是無法獲取參數名稱的。
-parameters參數
既然反射獲取不到參數名是因為編譯時丟棄了,那么有沒有辦法讓javac編譯時將參數名保留下來呢???答案是有的,那就是-parameters參數。
JDK8加入了一個新功能,編譯時加上-parameters參數,即可保留參數名,通過parameter.getName()就可以獲取到正常的參數名了。
示例
有如下測試類:
public class Demo { public void test(String name, Integer age) { } }
javac Demo.java #默認的編譯方式 javap -verbose Demo
javac -parameters Demo.java #加-parameters參數編譯 javap -verbose Demo
可以看到,加了-parameters參數后,字節碼文件會使用額外的MethodParameters區域來保存方法的參數名稱。這樣反射的時候通過parameter.getName()就可以獲取到參數名了。
注意:只支持JDK8及以上版本!!!
-g參數
由于-parameters要求JDK至少是8版本,而SpringMVC肯定是要支持低版本JDK的,那么還有沒有其他方法可以保留參數名呢???
答案依然是有的,那就是-g參數。
編譯時,加上-g參數就是告訴編譯器,我們需要調試類的信息,這時編譯器在編譯時,就會保留局部變量表的信息,參數也是局部變量表的一部分。
可以看到,加上-g后就可以從局部變量表中獲取參數的名稱了。
使用Maven來管理項目的話,編譯會默認加-g參數,不需要開發者介入。
注意:雖然-g會將局部變量表的信息保存下來,但是依然無法通過反射parameter.getName()的方式來獲取參數名,需要開發者去解析Class字節碼文件來獲取,這是和-parameters的一個重大區別!!!
ASM
ASM是一個通用的Java字節碼操作和分析框架。 它可以用于修改現有類或直接以二進制形式動態生成類。 ASM提供了一些常見的字節碼轉換和分析算法,可以從中構建自定義復雜轉換和代碼分析工具。 ASM提供與其他Java字節碼框架類似的功能,但專注于性能。 因為它的設計和實現盡可能小而且快,所以它非常適合在動態系統中使用(但當然也可以以靜態方式使用,例如在編譯器中)。
編譯時加上-g參數可以將參數名保留下來,但是依然無法通過反射來獲取,需要解析字節碼文件自己獲取。
有沒有好用的工具包來幫我們解析字節碼文件呢???
答案依然是:有的。
Java通過ASM就可以很方便的操作字節碼文件,很多開源框架都用到了ASM,例如CGLIB。
下面寫一個例子,通過ASM來獲取方法的參數名。
1、引入依賴
<dependency> <groupId>asm</groupId> <artifactId>asm-util</artifactId> <version>3.3.1</version> </dependency>
2、代碼示例
public class Demo { public void test(String name, Integer age) { } /** * 通過ASM來訪問參數名 * @param args * @throws Exception */ public static void main(String[] args) throws Exception { Class<Demo> clazz = Demo.class; Method method = clazz.getMethod("test", String.class, Integer.class); InputStream in = clazz.getResourceAsStream("/" + clazz.getName().replace('.', '/') + ".class"); ClassReader cr = new ClassReader(in); ClassNode cn = new ClassNode(); cr.accept(cn, ClassReader.EXPAND_FRAMES); List<MethodNode> methodNodes = cn.methods; for (MethodNode methodNode : methodNodes) { if (method.getName().equals(methodNode.name)) { System.err.println("test方法參數:"); List<LocalVariableNode> localVariables = methodNode.localVariables; for (LocalVariableNode localVariable : localVariables) { System.err.println(localVariable.name); } } } } }
控制臺輸出:
test方法參數:
this
name
age
注意:這種方式對接口和抽象方法沒有用,因為抽象方法沒有方法體,也就沒有局部變量表。這也就是為什么MyBatis在xml中無法根據接口方法的參數名去綁定參數的原因!!!
至此,我們已經知道,Java獲取方法的參數名有兩種方式,分別是加-parameters參數反射獲取、-g參數通過ASM解析字節碼文件獲取。
那SpringMVC用的是哪種呢???
SpringMVC的處理方式
SpringMVC是如何解決參數名稱的問題的呢?是通過-parameters參數嗎???
當然不是,首先-parameters參數是JDK8才提供的,老版本的JDK根本沒這個功能,SpringMVC是要支持JDK8之前的版本的,而且這種解決方案強制要求開發者編譯時手動加參數,也很不友好。
要想知道SpringMVC的解決方案,必須看源碼!!!
Debug跟蹤源碼的過程筆者就不詳敘了,感興趣的同學可以自己去跟蹤一下。
SpringMVC將一個方法處理器封裝為一個HandlerMethod類,方法的參數則用MethodParameter表示:
MethodParameter有一個獲取參數名的方法getParameterName():
獲取參數名的的任務其實是交給ParameterNameDiscoverer去完成了,這是一個接口,主要的作用就是解析方法的參數名稱。
MethodParameter的ParameterNameDiscoverer實現類是PrioritizedParameterNameDiscoverer。
距離真相只剩一步之遙了,去看看LocalVariableTableParameterNameDiscoverer實現吧。
只要看inspectClass()方法就知道真相了。
可以看到,LocalVariableTableParameterNameDiscoverer底層就是用的ASM的技術來獲取方法的參數名的。只是Spring并沒有直接依賴ASM,而是將他們封裝到了自己的org.springframework.asm包下。
總結
SpringMVC獲取Controller方法的參數名有三種方式,如下:
方案 | 限制 | 優缺點 |
---|---|---|
參數加注解 | 不受限 | 編寫麻煩 |
-parameters | JDK8及以上才支持 | 直接通過parameter.getName()獲取,方便 |
-g | 不受限,編譯加-g參數即可 | 解析比較麻煩,依賴于ASM |
- 如果加了@RequestParam則優先使用注解解析。
- 如果沒有注解,則采用StandardReflectionParameterNameDiscoverer解析,通過Parameter.getName()反射獲取,前提是JDK版本為8以上,且開啟了-parameters編譯參數。
- 如果前面2種都無法獲取,則采用LocalVariableTableParameterNameDiscoverer通過ASM技術來解析。
注意:如果編譯不加-g參數,即使是用ASM也無法解析。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/cc_yy_zh/article/details/78953548