你以為反射真的不能為所欲為?至少JDK8以後很強

  • 2019 年 10 月 11 日
  • 筆記

反射操作方法

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的彩蛋

原文:https://www.cnblogs.com/zhangxinhua/p/11543653.html