如何利用緩存機制實現JAVA類反射性能提高30倍

一次性能提升30倍的JAVA類反射性能優化實踐html

文章來源:宜信技術學院 & 宜信支付結算團隊技術分享第4期-支付結算部支付研發團隊高級工程師陶紅《JAVA類反射技術&優化》java

分享者:宜信支付結算部支付研發團隊高級工程師陶紅node

原文首發於宜信支付結算技術團隊公號:野指針程序員

在實際工做中的一些特定應用場景下,JAVA類反射是常常用到、必不可少的技術,在項目研發過程當中,咱們也遇到了不起不運用JAVA類反射技術的業務需求,而且不可避免地面臨這個技術固有的性能瓶頸問題。編程

經過近兩年的研究、嘗試和驗證,咱們總結出一套利用緩存機制、大幅度提升JAVA類反射代碼運行效率的方法,和沒有優化的代碼相比,性能提升了20~30倍。本文將與你們分享在探索和解決這個問題的過程當中的一些有價值的心得體會與實踐經驗。緩存

簡述:JAVA類反射技術

首先,用最簡短的篇幅介紹JAVA類反射技術。性能優化

若是用一句話來概述,JAVA類反射技術就是:數據結構

繞開編譯器,在運行期直接從虛擬機獲取對象實例/訪問對象成員變量/調用對象的成員函數。架構

抽象的概念很少講,用代碼說話……舉個例子,有這樣一個類:框架

public class ReflectObj {
    private String field01;
    public String getField01() {
        return this.field01;
    }
    public void setField01(String field01) {
        this.field01 = field01;
    }
}

 

若是按照下列代碼來使用這個類,就是傳統的「建立對象-調用」模式:

ReflectObj obj = new ReflectObj();
    obj.setField01("value01");
    System.out.println(obj.getField01());

 

若是按照以下代碼來使用它,就是「類反射」模式:

// 直接獲取對象實例
    ReflectObj obj = ReflectObj.class.newInstance();
    // 直接訪問Field
    Field field = ReflectObj.class.getField("field01");
    field.setAccessible(true);
    field.set(obj, "value01");
    // 調用對象的public函數
    Method method = ReflectObj.class.getMethod("getField01");
    System.out.println((String) method.invoke(obj));

 

類反射屬於古老而基礎的JAVA技術,本文再也不贅述。

從上面的代碼能夠看出:

  • 相比較於傳統的「建立對象-調用」模式,「類反射」模式的代碼更抽象、通常狀況下也更加繁瑣;
  • 類反射繞開了編譯器的合法性檢測——好比訪問了一個不存在的字段、調用了一個不存在或不容許訪問的函數,由於編譯器設立的防火牆失效了,編譯可以經過,可是運行的時候會報錯;
  • 實際上,若是按照標準模式編寫類反射代碼,效率明顯低於傳統模式。在後面的章節會提到這一點。

緣起:爲何使用類反射

前文簡略介紹了JAVA類反射技術,在與傳統的「建立對象-調用」模式對比時,提到了類反射的幾個主要弱點。可是在實際工做中,咱們發現類反射無處不在,特別是在一些底層的基礎框架中,類反射是應用最爲廣泛的核心技術之一。最多見的例子:Spring容器。

這是爲何呢?咱們不妨從實際工做中的具體案例出發,分析類反射技術的不可替代性。

你們幾乎天天都和銀行打交道,經過銀行進行存款、轉賬、取現等金融業務,這些動帳操做都是經過銀行核心系統(包括交易核心/帳務核心/對外支付/超級網銀等模塊)完成的,由於歷史緣由形成的技術路徑依賴,銀行核心系統的報文幾乎都是xml格式,並且以這種格式最爲廣泛:

<?xml version='1.0' encoding='UTF-8'?>
<service>
    <sys-header>
        <data name="SYS_HEAD">
            <struct>
                <data name="MODULE_ID">
                    <field type="string" length="2">RB</field>
                </data>
                <data name="USER_ID">
                    <field type="string" length="6">OP0001</field>
                </data>
                <data name="TRAN_TIMESTAMP">
                    <field type="string" length="9">003026975</field>
                </data>
                <!-- 其它字段略過 -->
            </struct>
        </data>
    </sys-header>
    <!-- 其它段落略過 -->
    <body>
        <data name="REF_NO">
            <field type="string" length="23">OPS18112400302633661837</field>
        </data>
    </body>
</service>

 

和經常使用的xml格式進行對比:

<?xml version="1.0" encoding="UTF-8"?>
<recipe>
        <recipename>Ice Cream Sundae</recipename>
        <ingredlist>
            <listitem>
                <quantity>3</quantity>
                <itemdescription>chocolate syrup or chocolate fudge</itemdescription>
            </listitem>
            <listitem>
                <quantity>1</quantity>
                <itemdescription>nuts</itemdescription>
            </listitem>
            <listitem>
                <quantity>1</quantity>
                <itemdescription>cherry</itemdescription>
            </listitem>
        </ingredlist>
        <preptime>5 minutes</preptime>
</recipe>

 

銀行核心系統的xml報文不是用標籤的名字區分元素,而是用屬性(name屬性)區分,在解析的時候,無論是用DOM、SAX,仍是Digester或其它方案,都要用條件判斷語句、分支處理,僞代碼以下:

// ……
接口類實例 obj = new 接口類();
List<Node> nodeList = 獲取xml標籤列表
for (Node node: nodeList) {
if (node.getProperty("name") == "張三") obj.set張三 (node.getValue());
    else if (node.getProperty("name") == "李四") obj.set李四 (node.getValue());
    // ……
}
// ……

 

顯而易見,這樣的代碼很是粗劣、不優雅,每解析一個接口的報文,都要寫一個專門的類或者函數,堆砌大量的條件分支語句,難寫、難維護。若是報文結構簡單還好,若是有一百個甚至更多的字段,怎麼辦?絕不誇張,在實際工做中,我遇到過一個銀行核心接口有140多個字段的狀況,並且這還不是最多的!

試水:優雅地解析XML

當咱們碰到這種結構的xml、並且字段還特別多的時候,解決問題的鑰匙就是類反射技術,基本思路是:

  • 從xml中解析出字段的name和value,以鍵值對的形式存儲起來;
  • 用類反射的方法,用鍵值對的name找到字段或字段對應的setter(這是有規律可循的);
  • 而後把value直接set到字段,或者調用setter把值set到字段。

接口類應該是這樣的結構:

  • nodes是存儲字段的name-value鍵值對的列表,MessageNode就是鍵值對,結構以下:
public class MessageNode {
    private String name;
    private String value;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    public MessageNode() {
        super();
    }
}

 

  • createNode是在解析xml的時候,把鍵值對添加到列表的函數;
  • initialize是用類反射方法,根據鍵值對初始化每一個字段的函數。

這樣,解析xml的代碼能夠變得很是優雅、簡潔。若是用Digester解析以前列舉的那種格式的銀行報文,能夠這樣寫:

Digester digester = new Digester();
    digester.setValidating(false);
    digester.addObjectCreate("service/sys-header", SysHeader.class);
    digester.addCallMethod("service/sys-header/data/struct/data", "createNode", 2);
    digester.addCallParam("service/sys-header/data/struct/data", 0, "name");
    digester.addCallParam("service/sys-header/data/struct/data/field", 1);
    parseObj = (SysHeader) digester.parse(new StringReader(msg));
    parseObj.initialize();

 

initialize函數的代碼,能夠寫在一個基類裏面,子類繼承基類便可。具體代碼以下:

public void initialize() {
for (MessageNode node: nodes) {
        try {
            /**
             * 直接獲取字段、而後設置字段值
             */
            //String fieldName = StringUtils.camelCaseConvert(node.getName());
            // 只獲取調用者本身的field(private/protected/public修飾詞皆可)
            //Field field = this.getClass().getDeclaredField(fieldName);
            // 獲取調用者本身的field(private/protected/public修飾詞皆可)和從父類繼承的field(必須是public修飾詞)
            //Field field = this.getClass().getField(fieldName);
            // 把field設爲可寫
            //field.setAccessible(true);
            // 直接設置field的值
            //field.set(this, node.getValue());
            /**
             * 經過setter設置字段值
             */
            Method method = this.getSetter(node.getName());
            // 調用setter
            method.invoke(this, node.getValue());
        } catch (Exception e) {
            log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e);
        };
    }
    }

 

上面被註釋的段落是直接訪問Field的方式,下面的段落是調用setter的方式,兩種方法在效率上沒有差異。

考慮到JAVA語法規範(書寫bean的規範),調用setter是更通用的辦法,由於接口類多是被繼承、派生的,子類沒法訪問父類用private關鍵字修飾的Field。

getSetter函數很簡單,就是用Field的名字反推setter的名字,而後用類反射的辦法獲取setter。代碼以下:

private Method getSetter(String fieldName) throws NoSuchMethodException, SecurityException {
    String methodName = String.format("set%s", StringUtils.upperFirstChar(fieldName));
    // 獲取field的setter,只要是用public修飾的setter、無論是本身的仍是從父類繼承的,都能取到
    return this.getClass().getMethod(methodName, String.class);
    }

 

若是設計得好,甚至能夠用一個解析函數處理全部的接口,這涉及到Digerser的運用技巧和接口類的設計技巧,本文不做深刻講解。

2017年,咱們在一個和銀行有關的金融增值服務項目中使用了這個解決方案,取得了很是不錯的效果,以後在公司內部推廣開來成爲了通用技術架構。通過一年多的實踐,證實這套架構性能穩定、可靠,極大地簡化了代碼編寫和維護工做,顯著提升了生產效率。

問題:類反射性能差

可是,隨着業務量的增長,2018年底在進行壓力測試的時候,發現解析xml的代碼佔用CPU資源居高不下。進一步分析、定位,發現問題出在類反射代碼上,在某些極端的業務場景下,甚至會佔用90%的CPU資源!這就提出了性能優化的迫切要求。

類反射的性能優化不是什麼新課題,所以有一些成熟的第三方解決方案能夠參考,好比運用比較普遍的ReflectASM,據稱能夠比未經優化的類反射代碼提升1/3左右的性能。

(參考資料:Java高性能反射工具包ReflectASMReflectASM-invoke,高效率java反射機制原理

在研究了ReflectASM的源代碼之後,咱們決定不使用現成的第三方解決方案,而是從底層入手、自行解決類反射代碼的優化問題。主要基於兩點考慮:

  • ReflectASM的基本技術原理,是在運行期動態分析類的結構,把字段、函數創建索引,而後經過索引完成類反射,技術上並不高深,性能也談不上完美;
  • 類反射是咱們系統使用的關鍵技術,使用場景、調用頻率都很是高,從自主掌握和控制基礎、核心技術,實現系統的性能最優化角度考慮,應該儘可能從底層技術出發,獨立、可控地完成優化工做。

思路和實踐:緩存優化

前面提到ReflectASM給類的字段、函數創建索引,藉此提升類反射效率。進一步分析,這其實是變相地緩存了字段和函數。那麼,在咱們面臨的業務場景下,能不能用緩存的方式優化類反射代碼的效率呢?

咱們的業務場景須要以類反射的方式頻繁調用接口類的setter,這些setter都是用public關鍵字修飾的函數,先是getMethod()、而後invoke()。基於以上特色,咱們用以下邏輯和流程進行了技術分析:

  • 用調試分析工具統計出每一句類反射代碼的執行耗時,結果發現性能瓶頸在getMethod();
  • 分析JAVA虛擬機的內存模型和管理機制,尋找解決問題的方向。JAVA虛擬機的內存模型,能夠從下面兩個維度來描述:

A.類空間/對象空間維度

B.堆/棧維度

  • 從JAVA虛擬機內存模型能夠看出,getMethod()須要從不連續的堆中檢索代碼段、定位函數入口,得到了函數入口、invoke()以後就和傳統的函數調用差很少了,因此性能瓶頸在getMethod();
  • 代碼段屬於類空間(也有資料將其描述爲「函數空間」/「代碼空間」),類被加載後,除非虛擬機關閉,函數入口不會變化。那麼,只要把setter函數的入口緩存起來,不就節約了getMethod()消耗的系統資源,進而提升了類反射代碼的執行效率嗎?

把接口類修改成這樣的結構(標紅的部分是新增或修改):

setterMap就是緩存字段setter的HashMap。爲何是兩層嵌套結構呢?由於這個Map是寫在基類裏面的靜態變量,每一個從基類派生出的接口類都用它緩存setter,因此第一層要區分不一樣的接口類,第二層要區分不一樣的字段。以下圖所示:

當ClassLoader加載基類時,建立setterMap(內容爲空):

 static {
        setterMap = new HashMap<String, Map<String, Method>>();
    }

 

這樣寫能夠保證setterMap只被初始化一次。

Initialize()函數做以下改進:

public void initialize() {
        // 先檢查子類的setter是否被緩存
        String className = this.getClass().getName();
        if (setterMap.get(className) == null) setterMap.put(className, new HashMap<String, Method>());
        Map<String, Method> setters = setterMap.get(className);
        // 遍歷報文節點
        for (MessageNode node: nodes) {
            try {
                // 檢查對應的setter是否被緩存了
                Method method = setters.get(node.getName());
                if (method == null) {
                    // 沒有緩存,先獲取、再緩存
                    method = this.getSetter(node.getName());
                    setters.put(node.getName(), method);
                }
                // 用類反射方式調用setter
                method.invoke(this, node.getValue());
            } catch (Exception e) {
                log.debug("It's failed to initialize field: {}, reason: {}", node.getName(), e);
            };
        }
    }

 

基本思路就是把setter緩存起來,經過MessageNode的name(字段的名字)找setter的入口地址,而後調用。

由於只在初始化第一個對象實例的時候調用getMethod(),極大地節約了系統資源、提升了效率,測試結果也證明了這一點。

驗證:測試方法和標準

1)先寫一個測試類,結構以下:

2)在構造函數中,用UUID初始化存儲鍵值對的列表nodes:

   this.createNode("test001", String.valueOf(UUID.randomUUID().toString().hashCode()));
        this.createNode("test002", String.valueOf(UUID.randomUUID().toString().hashCode()));
        // ……

 

之因此用UUID,是保證每一個實例、每一個字段的值都不同,避免JAVA編譯器自動優化代碼而破壞測試結果的原始性。

3)Initialize_ori()函數是用傳統的硬編碼方式直接調用setter的方法初始化實例字段,代碼以下:

  for (MessageNode node: this.nodes) {
            if (node.getName().equalsIgnoreCase("test001")) this.setTest001(node.getValue());
            else if (node.getName().equalsIgnoreCase("test002")) this.setTest002(node.getValue());
// ……
        }

 

優化效果就以它做爲對照標準1,對照標準2就是沒有優化的類反射代碼。

4)checkUnifomity()函數用來驗證:代碼是否用name-value鍵值對正確地初始化了各字段。

for (MessageNode node: nodes) {
            if (node.getName().equalsIgnoreCase("test001") && !node.getValue().equals(this.test001)) return false;
            else if (node.getName().equalsIgnoreCase("test002") && !node.getValue().equals(this.test002)) return false;
            // ……
        }
        return true;

 

每一種優化方案,咱們都會用它驗證明例的字段是否正確,只要出現一次錯誤,該方案就會被否認。

5)建立100萬個TestInvoke類的實例,而後循環調用每個實例的initialize_ori()函數(傳統的硬編碼,非類反射方法),記錄執行耗時(只記錄初始化耗時,建立實例的耗時不記錄);再建立100萬個實例,循環調用每個實例的類反射初始化函數(未優化),記錄執行耗時;再建立100萬個實例,改爲調用優化後的類反射初始化函數,記錄執行耗時。

6)以上是一個測試循環,獲得三種方法的耗時數據,重複作10次,獲得三組耗時數據,把記錄下的數據去掉最大、最小值,剩下的求平均值,就是該方法的平均耗時。某一種方法的平均耗時越短則認爲該方法的效率越高。

7)爲了進一步驗證三種方法在不一樣負載下的效率變化規律,改爲建立10萬個實例,重複5/6兩步,獲得另外一組測試數據。

測試結果顯示:在確保測試環境穩定、一致的前提下,8個字段的測試實例、初始化100萬個對象,傳統方法(硬編碼)耗時850~1000毫秒;沒有優化的類反射方法耗時23000~25000毫秒;優化後的類反射代碼耗時600~800毫秒。10萬個測試對象的狀況,三種方法的耗時也大體是這樣的比例關係。這個數據取決於測試環境的資源情況,不一樣的機器、不一樣時刻的測試,結果都有出入,但總的規律是穩定的。

基於測試結果,能夠得出這樣的結論:緩存優化的類反射代碼比沒有優化的代碼效率提升30倍左右,比傳統的硬編碼方法提升了10~20%。有必要強調的是,這個結論偏向保守。和ReflecASM相比,性能大幅度提升也是毋庸置疑的。

第一次迭代:忽略字段

緩存優化的效果很是好,可是,這個方案真的天衣無縫了麼?

通過分析,咱們發現:若是數據更復雜一些,這個方案的缺陷就暴露了。好比鍵值對列表裏的值在接口類裏面並無定義對應的字段,或者是沒有對應的、能夠訪問的setter,性能就會明顯降低。

這種狀況在實際業務中是很常見的,好比對接銀行核心接口,每每並不須要解析報文的所有字段,不少字段是能夠忽略的,因此接口類裏面不用定義這些字段,但解析代碼依然會把這些鍵值對所有解析出來,這時就會給優化代碼形成麻煩了。

分析過程以下:

1)舉例而言,若是鍵值對裏有兩個值在接口類(Interface01)並未定義,假定名字是fieldX、filedY,第一次執行initialize()函數:

初始狀態下,setterMap檢索不到Interface01類的setter緩存,initialize()函數會在第一次執行的時候,根據鍵值對的名字(field01/field02/……/fieldN/fieldX/fieldY)調用getMethod()函數、初始化sertter引用的緩存。由於fieldX和fieldY字段不存在,找不到它們對應的setter,緩存裏也沒有它們的引用。

2)第二次執行initialize()函數(也就是初始化第二個對象實例),field01/field02/……/fieldN鍵值對都能在緩存中找到setter的引用,調用速度很快;但緩存裏找不到fieldX/fieldY的setter的引用,因而再次調用getMethod()函數,而由於它們的setter根本不存在(連這兩個字段都不存在),作的是無用功,setterMap的狀態沒有變化。

3)第三次、第四次……第N次,都是如此,白白消耗系統資源,運行效率必然降低。

測試結果印證了這個推斷:在TestInvoke的構造函數增長了兩個不存在對應字段和setter的鍵值對(姑且稱之爲「無效鍵值對」),進行100萬個實例的初始化測試,通過優化的類反射代碼,耗時從原來的600~800毫秒,增長到7000~8000毫秒,性能降低10倍左右。若是增長更多的鍵值對(不存在對應字段),性能降低更嚴重。

因此必須進一步完善優化代碼。爲了加以區分,咱們把以前的優化代碼稱爲V1版;進一步完善的代碼稱爲V2版。

怎麼完善?從上面的分析不難找到思路:增長忽略字段(ignore field)緩存。

基類BaseModel做以下修改(標紅部分是新增或者修改),增長了ignoreMap:

ignoreMap的數據結構相似於setterMap,但第二層不是HashMap,而是Set,緩存每一個子類須要忽略的鍵值對的名字,使用Set更節約系統資源,以下圖所示:

一樣的,當ClassLoader加載基類的時候,建立ignoreMap(內容爲空):

  static {
        setterMap = new HashMap<String, Map<String, Method>>();
        ignoreMap = new HashMap<String, Set<String>>();
    }

 

Initialize()函數做以下改進:

public void initialize() {
        // 先檢查子類的setter是否被緩存
        String className = this.getClass().getName();
        if (setterMap.get(className) == null) setterMap.put(className, new HashMap<String, Method>());
        if (ignoreMap.get(className) == null) ignoreMap.put(className, new HashSet<String>());
        Map<String, Method> setters = setterMap.get(className);
        Set<String> ignores = ignoreMap.get(className);
        // 遍歷報文節點
        for (MessageNode node: nodes) {
            String sName = node.getName();
            try {
                // 檢查該字段是否被忽略
                if (ignores.contains(sName)) continue;
                // 檢查對應的setter是否被緩存了
                Method method = setters.get(sName);
                if (method == null) {
                    // 沒有緩存,先獲取、再緩存
                    method = this.getSetter(sName);
                    setters.put(sName, method);
                }
                // 用類反射方式調用setter
                method.invoke(this, node.getValue());
            } catch (NoSuchMethodException | SecurityException e) {
                log.debug("It's failed to initialize field: {}, reason: {}", sName, e);
                // 找不到對應的setter,放到忽略字段集合,之後再也不嘗試
                ignores.add(sName);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                log.error("It's failed to initialize field: {}, reason: {}", sName, e);
                try {
                    // 不能調用setter,多是虛擬機回收了該子類的所有實例、入口地址變化,更新地址、再試一次
                    Method method = this.getSetter(sName);
                    setters.put(sName, method);
                    method.invoke(this, node.getValue());
                } catch (Exception e1) {
                    log.debug("It's failed to initialize field: {}, reason: {}", sName, e1);
                }
            } catch (Exception e) {
                log.error("It's failed to initialize field: {}, reason: {}", sName, e);
            }
        }
    }

 

雖然代碼複雜了一些,但思路很簡單:用鍵值對的名字尋找對應的setter時,若是找不到,就把它放進ignoreMap,下次再也不找了。另外還增長了對setter引用失效的處理。雖然理論上說「只要虛擬機不重啓,setter的入口引用永遠不會變」,在測試中也歷來沒有遇到過這種狀況,但爲了覆蓋各類異常狀況,仍是增長了這段代碼。

繼續沿用前面的例子,分析改進後的代碼的工做流程:

1)第一次執行initialize()函數,實例的狀態是這樣變化的:

由於fieldX和fieldY字段不存在,找不到它們對應的setter,它們被放到ignoreMap中。

2)再次調用initialize()函數的時候,由於檢查到ignoreMap中存在fieldX和fieldY,這兩個鍵值對被跳過,再也不徒勞無功地調用getMethod();其它邏輯和V1版相同,沒有變化。

仍是用上面提到的TestInvoke類做驗證(8個字段+2個無效鍵值對),V2版本雖然代碼更復雜了,但100萬條紀錄的初始化耗時爲600~800毫秒,V1版代碼這個時候的耗時猛增到7000~8000毫秒。哪怕增長更多的無效鍵值對,V2版代碼耗時增長也不明顯,而這種狀況下V1版代碼的效率還會進一步降低。

至此,對JAVA類反射代碼的優化已經比較完善,覆蓋了各類異常狀況,如前所述,咱們把這個版本稱爲V2版。

第二次迭代:逆向思惟

這樣就表明優化工做已經作到最好了嗎?不是這樣的。

仔細觀察V一、V2版的優化代碼,都是循環遍歷鍵值對,用鍵值對的name(和字段的名字相同)推算setter的函數名,而後去尋找setter的入口引用。第一次是調用類反射的getMethod()函數,之後是從緩存裏面檢索,若是存在無效鍵值對,那就必然出現空轉循環,哪怕是V2版代碼,ignoreMap也不能避免這種空轉循環。雖然單次空轉循環耗時很是短,但在無效鍵值對比較多、負載很大的狀況下,依然有無效的資源開銷。

若是採用逆向思惟,用setter去反推、檢索鍵值對,又會如何?

先分析業務場景以及由業務場景所決定的數據結構特色:

  • 接口類的字段數量可能大於setter函數的數量,由於可能須要一些內部使用的功能性字段,並非從xml報文裏解析出來的;
  • xml報文裏解析出的鍵值對和字段是交集關係,多數狀況下,鍵值對的數量包含了接口類的字段,而且大機率存在一些不須要的鍵值對;
  • 相比較字段,setter函數和須要解析的鍵值對最接近於一一對應關係,出現空轉循環的機率最小;
  • 由於接口類編寫要遵照JAVA編程規範,從setter函數的名字反推字段的名字,進而檢索鍵值對,是可行、可靠的。

綜上所述,逆向思惟用setter函數反推、檢索鍵值對,初始化接口類,就是第二次迭代的具體方向。

須要把接口類修改爲這樣的結構(標紅的部分是新增或者修改):

1)爲了便於逆向檢索鍵值對,nodes字段改爲HashMap,key是鍵值對的名字、value是鍵值對的值。

2)爲了提升循環遍歷的速度,setterMap的第二層改爲鏈表,鏈表的成員是內部類FieldSetter,結構以下:

private class FieldSetter {
        private String name;
        private Method method;
        public String getName() {
            return name;
        }
        public Method getMethod() {
            return method;
        }
        public void setMethod(Method method) {
            this.method = method;
        }
        public FieldSetter(String name, Method method) {
            super();
            this.name = name;
            this.method = method;
        }
    }

 

setterMap的第二層繼續使用HashMap也能實現功能,但循環遍歷的效率,HashMap不如鏈表,因此咱們改用鏈表。

3)一樣的,setterMap在基類被加載的時候建立(內容爲空):

static {
        setterMap = new HashMap<String, List<FieldSetter>>();
    }

 

4)第一次初始化某個接口類的實例時,調用initSetters()函數,初始化setterMap:

protected List<FieldSetter> initSetters() {
        String className = this.getClass().getName();
        List<FieldSetter> setters = new ArrayList<FieldSetter>();
        // 遍歷類的可調用函數
        for (Method method: this.getClass().getMethods()) {
            String methodName = method.getName();
            // 若是從名字推斷是setter函數,添加到setter函數列表
            if (methodName.startsWith("set")) {
                // 反推field的名字
                String fieldName = StringUtils.lowerFirstChar(methodName.substring(3));
                setters.add(new FieldSetter(fieldName, method));
            }
        }
        // 緩存類的setter函數列表
        setterMap.put(className, setters);
        // 返回可調用的setter函數列表
        return setters;
    }

 

5)Initialize()函數修改成以下邏輯:

public void initialize() {
        // 從緩存獲取接口類的setter列表
        List<FieldSetter> setters = setterMap.get(this.getClass().getName());
        // 若是尚未緩存、初始化接口類的setter列表
        if (setters == null) setters = this.initSetters();
        // 遍歷接口類的setter 
        for (FieldSetter setter: setters) {
            // 用setter的名字(也就是字段的名字)檢索鍵值對
            String fieldName = setter.getName();
            String fieldValue = nodes.get(fieldName);
            // 沒有檢索到鍵值對、或者鍵值對沒有賦值,跳過
            if (StringUtils.isEmpty(fieldValue)) continue;
            try {
                Method method = setter.getMethod();
                // 用類反射方式調用setter
                method.invoke(this, fieldValue);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                log.error("It's failed to initialize field: {}, reason: {}", fieldName, e);
                // 不能調用setter,多是虛擬機回收了該子類的所有實例、入口地址變化,更新地址、再試一次
                try {
                    Method method = this.getSetter(fieldName);
                    setter.setMethod(method);
                    method.invoke(this, fieldValue);
                } catch (Exception e1) {
                    log.debug("It's failed to initialize field: {}, reason: {}", fieldName, e1);
                }
            } catch (Exception e) {
                log.error("It's failed to initialize field: {}, reason: {}", fieldName, e);
            }
        }
    }

 

不妨把這版代碼稱爲V3……繼續沿用前面TestInvoke的例子,分析改進後代碼的工做流程:

1)第一次執行initialize()函數,實例的狀態是這樣變化的:

經過setterMap反向檢索鍵值對的值,fieldX、fieldY由於不存在對應的setter,不會被檢索,避免了空轉。

2)以後每一次初始化對象實例,都不須要再初始化setterMap,也不會消耗任何資源去檢索fieldX、fieldY,最大限度地節省資源開銷。

3)由於取消了ignoreMap,取消了V2版判斷字段是否應該被忽略的邏輯,代碼更簡潔,也能節約一部分資源。

結果數據顯示:用TestInvoke測試類、8個setter+2個無效鍵值對的狀況下,進行100萬/10萬個實例兩個量級的對比測試,V3版比V2版性能最多提升10%左右,100萬實例初始化耗時550~720毫秒。若是增長無效鍵值對的數量,性能提升更爲明顯;沒有無效鍵值對的最理想狀況下,V一、V二、V3版本的代碼效率沒有明顯差異。

至此,用緩存機制優化類反射代碼的嘗試,已經比較接近最優解了,V3版本的代碼能夠視爲到目前爲止最好的版本。

總結和思考:方法論

總結過去兩年圍繞着JAVA類反射性能優化這個課題,咱們所進行的探索和研究,提升到方法論層面,能夠提煉出一個分析問題、解決問題的思路和流程,供你們參考:

1)從實踐中來

多數狀況下,探索和研究的課題並非坐在書齋裏憑空想出來的,而是在實際工做中遇到具體的技術難點,在現實需求的驅動下發現須要研究的問題。

以本文爲例,若是不是在對接銀行核心系統的時候遇到了大量的、格式奇特的xml報文,不會促使咱們嘗試用類反射技術去優雅地解析報文,也就不會面對類反射代碼執行效率低的問題,天然不會有後續的研究成果。

2)拿出手術刀,解剖一隻麻雀

在實踐中遇到了困難,首先要分析和研究面對的問題,不能着急,要有解剖一隻麻雀的精神,抽絲剝繭,把問題的根源找出來。

這個過程當中,邏輯分析和實操驗證都是必不可少的。沒有高屋建瓴的分析,就容易迷失大方向;沒有實操驗證,大機率會陷入坐而論道、腦補的怪圈。仍是那句話:實踐是最寶貴的財富,也是驗證一切構想的終極考官,是咱們認識世界改造世界的力量源泉。但咱們也不能陷入庸俗的經驗主義,無論怎麼說,這個世界的基石是有邏輯的。

回到本文的案例,咱們一方面研究JAVA內存模型,從理論上探尋類反射代碼效率低下的緣由;另外一方面也在實務層面,用實實在在的時間戳驗證了JAVA類反射代碼的耗時分佈。理論和實踐的結合,才能讓咱們找到解決問題的正確方向,兩者不可偏廢。

3)頭腦風暴,敢於創新

分析問題,找到關鍵點,接下來就是尋找解決方案。JAVA程序員有一個很大的優點,同時也是很大的劣勢:第三方解決方案很是豐富。JAVA生態比較完善,咱們面臨的麻煩和問題幾乎都有成熟的第三方解決方案,「吃現成的」是優點也是劣勢,不少時候,咱們的創造力也所以被扼殺。因此,當面臨高價值需求的時候,應該拿出大無畏的勇氣,啃硬骨頭,作底層和原創的工做。

就本文案例而言,ReflexASM就是看起來很不錯的方案,比傳統的類反射代碼性能提高了至少三分之一。可是,它真的就是最優解麼?咱們的實踐否認了這一點。JAVA程序員要有吃苦耐勞、以底層技術爲原點解決問題的精神,不然你就會被別人所綁架,失去尋求技術自由空間的機會。中國的軟件行業已經發展到了這個階段,提出了這樣的需求,咱們應該順應歷史潮流。

4)螺旋式發展,波浪式前進

研究問題和解決問題,迭代是很是有效的工做方法。首先,要有精益求精的態度,不斷改進,逼近最優方案,迭代必不可少。其次,對於比較複雜的問題,不要追求畢其功於一役,把一個大的目標拆分紅不一樣階段,分步實施、逐漸推動,這種狀況下,迭代更是解決問題的必由之路。

咱們解決JAVA類反射代碼的優化問題,就是通過兩次迭代、寫了三個版本,才獲得最終的結果,逼近了最優解。在迭代的過程當中會逐漸發現一些以前忽略的問題,這就是寶貴的經驗,這些經驗在解決其餘技術問題時也能發揮做用。好比HashMap的數據結構很是合理、經典,平時使用的時候效率是很高的,若是不是迭代開發、逼近極限的過程,咱們又怎麼可能發如今循環遍歷狀態下、它的性能不如鏈表呢?

行文至此,文章也快要寫完了,細心的讀者必定會有一個疑問:自始至終,舉的例子、類的字段都是String類型,類反射代碼根本沒有考慮setter的參數類型不一樣的狀況。確實是這樣的,由於咱們解決的是銀行核心接口報文解析的問題,接口字段所有是String,沒有其它數據類型。

其實,對類反射技術的研究深刻到這個程度,解決這個問題、而且維持代碼的高效率,易如反掌。好比,給FieldSetter類增長一個數據類型的字段,初始化setterMap的時候把接口類對應的字段的數據類型解析出來,和setter函數的入口一塊兒緩存,類反射調用setter時,把參數格式轉換一下,就能夠了。限於篇幅、這個問題就不展開了,感興趣的讀者能夠本身嘗試一下。

相關文章
相關標籤/搜索