你以為反射真的無所不能?至少JDK8以后很強大


之前我們已經介紹了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

然后我們在通過命令查看下這個class文件
javap -verbose App.class
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);
    }
}

spring方法
對,就是ParameterNameDiscoverer這個方法幫助了我們。這里簡單說說ParameterNameDiscoverer作用。springmvc中會有一個默認的ParameterNameDiscoverer解釋器DefaultParameterNameDiscoverer該類繼承PrioritizedParameterNameDiscovererPrioritizedParameterNameDiscoverer這個類就是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 : 編譯更加全面點
javacg
經過對比發現javac 和javac -g 的區別好像是javac -g 編譯信息多出LocalVariableTable信息。

名稱 解釋
LineNumberTable 屬性表存放方法的行號信息
LocalVariableTable 屬性表中存放方法的局部變量信息

上圖中通過javac -g 編譯的信息中LocalVarableTable有三條數據,是因為在編譯期間每個非靜態方法第一個參數都是this.去除第一條剩下的其實就是我們需要的參數信息。但是我們這個時候去執行一下看看效果。
對比

高級反射注意點

所謂的高級反射其實就是對jdk版本的要求,只要是jdk8的版本,就可以用jdk提供的parameter方法獲取參數名了。在編譯的時候需要加上 -parameters
編譯

javac的彩蛋

彩蛋

續點

每日一笑

今天公司放假,和老婆商量好一起去她家,出發前夕,老婆說:給我家里人的禮物買好了么?然后我去卧室全搬出來了;給她說:這是你舅的,這是你叔的,這是你爺爺的,這是你奶奶的,這是你爸的,這是你媽的,這是…………然后我倆就打起來了!

上期答案

加入戰隊

# 加入戰隊

微信公眾號

微信公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM