之前我們已經介紹了Java中框架常用的技術---反射。可以這么說反射方便了我們的開發。今天我們來說說他的短板,或者說我們今天在反射的基礎上在進行方便化。
反射操作方法
在上一章節中我們學會了通過反射去調用方法。
public class App {
public void test(String str, Integer integer) {
System.out.println(str);
System.out.println(integer);
}
}
這個時候如果我想獲取test方法對象的話應該這么做
Method testMethod = App.class.getMethod("test", String.class, Integer.class);
這里就不在贅述如何通過Method對象調用方法了。文章末尾會給出上一章節的地址。今天我們要研究的是Method如何獲取方法參數這一塊。看似簡單卻又是那么的傳奇。我們看看下面一段代碼執行的效果
public static void main(String[] args) throws ParseException, NoSuchMethodException {
Method[] methods = App.class.getMethods();
Method testMethod = App.class.getMethod("test", String.class, Integer.class);
Class<?>[] parameterTypes = testMethod.getParameterTypes();
Parameter[] parameters = testMethod.getParameters();
for (Parameter parameter : parameters) {
System.out.println(parameter.getName());
}
}
那么輸出的兩個參數名稱是什么呢?一開始筆者這里想當然的認為是 str , name 。 相信此時的你應該和我一樣認為是str , name 。
對的,你沒看錯返回的居然是無意義的名稱 , arg0 , arg1.這就奇怪了。至於為什么呢?我現在還不想告訴你。下面會慢慢告訴你。
Spring的方法的優點
做過Javaweb開發的肯定都用過spring,springmvc , 在寫controller層的時候我們都會在方法里直接寫key值的名稱,然后在請求地址中給相應的key賦值。
@RequestMapping(value = "/deptId", method = RequestMethod.GET)
public PagedResult<SysDept> selectSysDeptsByPK(Integer pageNumber, Integer pageSize) {
return sysDeptService.selectSysDeptsByPK(deptId, pageNumber, pageSize);
}
上述的controller我們在前端發送請求后會這樣發送
http://{ip}:{port}/{projectName}/deptId?pageNumber=1&pageSize=5
這里我問一下你們有沒有想過為什么springmvc它能夠通過你傳遞的參數一一進行對應呢?我們上面已經嘗試過通過反射是無法獲取方法參數名稱的。而springmvc無非就是反射操作方法的。這里是不是很神奇,不得不佩服springmvc的強大。強大到讓人害怕。
反射如何實現Spring的方法
上面兩個案例揭露了反射的缺點以及springmvc的強大。這里需要借助springmvc提供的一個工具ParameterNameDiscoverer
。 這個類顧名思義就是發現參數名稱。在使用這個類之前我們先來了解下為什么反射獲取不到方法名稱。
這里需要簡單說說Java執行過程,Java之所以可以跨容器是因為Java針對各個系統提供了不同jvm,所以我們開發前都需要安裝不同版本的jdk,jdk里面提供了jvm,java 代碼運行期間是通過jvm去操作class文件的。但是我們平時都是開發java文件的。所以在jvm執行之前會有一個編譯期間。javac就是用來變異java文件為class文件的。
package com.zxhtom.test;
/**
* Hello world!
*/
public class App {
public void test(String str, Integer integer) {
}
}
針對上述代碼我們通過javac進行編譯下試試看看效果。
javac App.java
編譯完成之后會出現一個同名的class文件
然后我們在通過命令查看下這個class文件
javap -verbose App.class
通過查看App.java對應的字節碼發現在javac編譯的時候對於方法的名稱根本不會去記錄的。想想也對我執行方法的時候只需要按順序將參數放進去就行了。根本不需要關心參數名稱是什么。那么問題顯而易見了jvm不需要參數名所以編譯時過率了。但是我們反射想通過參數名稱一一對應這樣效率更快。那么是springmvc是如何解決的呢。
private static final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
public static void main(String[] args) throws ParseException, NoSuchMethodException {
java.lang.reflect.Method testMethod = App.class.getMethod("test", String.class, Integer.class);
String[] parameterNames = parameterNameDiscoverer.getParameterNames(testMethod);
for (String parameterName : parameterNames) {
System.out.println(parameterName);
}
}
對,就是ParameterNameDiscoverer
這個方法幫助了我們。這里簡單說說ParameterNameDiscoverer
作用。springmvc中會有一個默認的ParameterNameDiscoverer
解釋器DefaultParameterNameDiscoverer
該類繼承PrioritizedParameterNameDiscoverer
。PrioritizedParameterNameDiscoverer
這個類就是getParameterNames去獲取方法名的。在springmvc中通過addDiscoverer
方法有三個類注冊到PrioritizedParameterNameDiscoverer
- KotlinReflectionParameterNameDiscoverer : Spring5.0提供 ,但是也得jdk8及以上版本使用
- StandardReflectionParameterNameDiscoverer : Spring4.0提供 ,但是也得jdk8及以上版本使用
- LocalVariableTableParameterNameDiscoverer :Spring2.0就有了,對JDK版本沒啥要求,完全Spring自己實現的獲取字段名稱,邏輯復雜些,效率稍微低一點
總結一下就是在springmvc4.0之前springmvc都是通過自己實現的一套代碼去獲取字節碼然后分析的。這里能力有限就不分析了。
在4.0以后因為Java8的推出彌補了這個bug.springmvc也就都采用jdk提供的功能獲取參數名了。下面我們來看看jdk8是如何解決這個問題的。
Java字節碼
在上一節我們通過javac , javap命令進行了Java的編譯了查看。我們發現class字節碼中記錄的信息有【常量區,類,方法】其中對於代碼的記錄有位置,堆,棧,行號等等。這也是我們jvm調優的依據。但是這僅僅是我們使用簡單的javac的編譯。
javac -g : 編譯更加全面點
經過對比發現javac 和javac -g 的區別好像是javac -g 編譯信息多出LocalVariableTable信息。
名稱 | 解釋 |
---|---|
LineNumberTable | 屬性表存放方法的行號信息 |
LocalVariableTable | 屬性表中存放方法的局部變量信息 |
上圖中通過javac -g 編譯的信息中LocalVarableTable有三條數據,是因為在編譯期間每個非靜態方法第一個參數都是this.去除第一條剩下的其實就是我們需要的參數信息。但是我們這個時候去執行一下看看效果。
高級反射注意點
所謂的高級反射其實就是對jdk版本的要求,只要是jdk8的版本,就可以用jdk提供的parameter方法獲取參數名了。在編譯的時候需要加上 -parameters
javac的彩蛋
續點
每日一笑
今天公司放假,和老婆商量好一起去她家,出發前夕,老婆說:給我家里人的禮物買好了么?然后我去卧室全搬出來了;給她說:這是你舅的,這是你叔的,這是你爺爺的,這是你奶奶的,這是你爸的,這是你媽的,這是…………然后我倆就打起來了!
上期答案
無