硬核系列 | 深入剖析字節碼增強

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Java從誕生之日起就努力朝着跨平臺解決方案演進並延續至今,其中的支撐技術就是中間代碼(即:字節碼指令)。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"所謂字節碼增強,實質就是在編譯期或運行期對字節碼進行插樁,以便在運行期影響程序的執行行爲"},{"type":"text","text":"。在實際的開發過程中,大部分開發人員都曾直接或間接與其打過交道,比如:典型的AOP技術,或是使用JVM-Sanbox、Arthas、Skywalking等效能工具,甚至是在實現一個編譯器時的中間代碼轉儲。在此大家需要注意,通常字節碼與上層語言的語法指令無關,只要符合JVM規範,目標代碼就允許被裝載至JVM的世界中運行,由此我們可以得出一個結論,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"那些Java語法層面暫不支持的功能特性,並不代表JVM不支持"},{"type":"text","text":"(比如:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/cef6d2931a54f85142d863db7","title":""},"content":[{"type":"text","text":"Coroutine"}]},{"type":"text","text":"),總之,這完全取決於你的腦洞有多大。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常,這類技術基石類型的文章一直受衆較小,大部分開發人員的聚焦點仍停留在語法層面或功能層面上,然而,恰恰正因如此,註定了這將會是普通Java研發人員永遠的天花板,如果不想被定格,就請努力翻越這一座座的大山。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"AOP增強的本質"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在正式討論字節碼增強之前,我首先講一下AOP所涉及到的一些相關概念,有助於大家對後續內容有更深刻的理解(儘管早在7年前這類文章我曾在Iteye上講解過無數遍)。AOP(Aspect Oriented Programming,面向切面編程)的核心概念是"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"以不改動源碼爲前提,通過前後“橫切”的方式,動態爲程序添加新功能"},{"type":"text","text":",它的出現,最初是爲了解決開發人員所面臨的諸多耦合性問題,如圖1所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/02fa649d1948b64a29b2aaa6ab770709.png","alt":null,"title":"圖1 使用AOP進行解耦","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們都知道,OOP針對的是業務處理過程的實體及其屬性和行爲進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分,那麼程序中各個組件、模塊之間必然會存在着依賴性,也就是通常我們所說的緊耦合,在追求高內聚、低耦合的當下,想要完全消除程序中的耦合似乎是不現實的,但過分的、不必要的耦合又往往容易導致我們的代碼很難被複用,甚至還需爲此付出高昂的維護成本。一般來說,一個成熟的系統中往往都會包含但不限於如下6點通用邏輯:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"日誌記錄;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"異常處理;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"事物處理;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"權限檢查;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"性能統計;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"流量管控。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"AOP術語中我們把上述這些通用邏輯稱之爲切面(Aspect)。試想一下,如果在系統的各個業務模塊中都充斥着上述這些與自身邏輯無毫瓜葛的共享代碼會產生什麼問題?很明顯,當被依賴方發生改變時,避免不了需要修改程序中所有依賴方的邏輯代碼,着實不利於維護。想要解決這個痛點,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"就必須將這些共享代碼從邏輯代碼中剝離出來,讓其形成一個獨立的模塊,以一種聲明式的、可插拔式的方式來應用到具體的邏輯代碼中去,以此消除程序中那些不必要的依賴、提升開發效率和代碼的可重用性"},{"type":"text","text":",這就是我們使用AOP初衷。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,AOP和具體的實現技術無關,只要是符合AOP的思想,我們都可以將其稱之爲AOP的實現。目前市面上AOP框架的實現方案通常都是基於如下2種形式:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"靜態編織;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"動態編織。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"靜態編織選擇在編譯期就將AOP增強邏輯插入到目標類的方法中,以AspectJ爲例,當成功定義好目標類和代理類後,通過命令"},{"type":"codeinline","content":[{"type":"text","text":"“ajc -d .”"}]},{"type":"text","text":"進行編譯後調用執行時即會觸發增強邏輯;對字節碼文件進行反編譯後,大家會發現目標類中多出來了一些代碼,這些多出來的代碼實際上就是AspectJ在編譯期就往目標類中插入的AOP字節碼。而動態編織選擇在運行期以動態代理的形式對目標類的方法進行AOP增強,諸如Cglib、Javassist,以及ASM等字節碼生成工具都可用於支撐這一方案的實現。當然,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"無論是選擇靜態編織還是動態編織方案來實現AOP增強,都會面臨着侵入性和固化性等2個問題"},{"type":"text","text":",關於這2個問題,我暫時把它們放在下個小節進行討論。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"非侵入式運行期AOP增強"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於Spring的AOP增強方案,儘管對業務代碼而言不具備任何侵入性,但這並非是絕對的,因爲開發人員仍然需要手動修改Spring的Bean定義信息;除此之外,如果是使用Dubbo的Filter技術,還需要額外在項目的/resources目錄下新建/META-INF/Dubbo/com.alibaba.Dubbo.rpc.Filter文件,因此,從項目維度來看,常規的AOP增強方案似乎並不能滿足我們對零侵入性的要求。另外,固化性問題也對動態增強產生了一定程度上的限制,因爲增強邏輯只會對提前約定好的方法生效,無法在運行期重新對一個已有的方法進行增強。大家思考下,由於JVM允許開發人員在運行期對存儲在PermGen空間內(Java8之後爲Metaspace空間)的字節碼內容進行某種意義上的修改操作,那麼是否可以對目標類重複加載來解決固化性問題?接下來,我就爲大家演示在程序中直接通過sun.misc.Unsafe類的defineClass()方法指定AppClassLoader對目標類進行多次加載,看看會發生什麼,實例1-1:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"try {\n var temp = Unsafe.class.getDeclaredField(\"theUnsafe\");\n temp.setAccessible(true);\n var unsafe = temp.get((Object) null);\n var byteCode = getByteCode();\n for (int i = 0; i < 2; i++) {\n unsafe.defineClass(name, byteCode, 0,\n byteCode.length, this.getClass().getClassLoader(), null);\n }\n} catch (Throwable e) {\n e.printStackTrace();\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行上述程序必然會導致觸發java.lang.LinkageError異常,從堆棧信息的描述來看,AppClassLoader不允許對相同的類進行重複加載。既然此路不通,那麼是否還有別的方式?值得慶幸的是,從JDK1.5開始,Java的設計者們在java.lang.instrument包下爲開發人員提供了基於JVMTI(Java Virtual Machine Tool Interface,Java虛擬機工具接口)規範的Instrumentation-API,使之能夠使用Instrumentation來構建一個獨立於應用的Agent程序,以便於監測和協助運行在JVM上的程序。當然最重要的是,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"使用Instrumentation可以在運行期對類定義進行修改和替換,換句話來說,相當於我們可以動態對目標類的方法進行AOP增強"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Instrumentation-API提供有2種使用方式,如下所示:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"Agent on load;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"Agent on attach。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前者的使用方式要求開發人員在啓動腳本中加入命令行參數"},{"type":"codeinline","content":[{"type":"text","text":"“-javaagent”"}]},{"type":"text","text":"來指定目標jar文件,由應用的啓動來同步帶動Agent程序的啓動。當然,首先需要定義好Agent-Class,示例1-2:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class AgentLauncher {\n public static void premain(String args, Instrumentation inst) throws Throwable {\n inst.addTransformer((a,b,c,d,e) -> 增強後的字節碼, true);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當Agent啓動時,首先會觸發對premain()函數的調用。在java.lang.instrument包下有2個非常重要的接口,分別是Instrumentation和ClassFileTransformer。前者作爲增強器,其addTransformer()函數用於註冊ClassFileTransformer實例;後者作爲類文件轉換器,需自行重寫其transform()函數,用於返回增強內容。執行命令"},{"type":"codeinline","content":[{"type":"text","text":"“-XX:+TraceClassLoading”"}]},{"type":"text","text":"後不難發現,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"當目標類被加載進方法區之前,會由Instrumentation的實現負責回調transform()函數執行增強("},{"type":"text","text":"已加載的類則需要手動觸發Instrumentation.retransformClasses()函數顯式重定義)"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"。"},{"type":"text","text":"爲了演示方便,我直接在premain()函數中實現了增強邏輯,但實際的開發過程中,增強邏輯往往非常複雜,並且在某些場景下還需要處理類隔離等問題,因此,通常情況下,Agent-Class所扮演的角色僅僅只是一個Launcher。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後再對Agent進行打包之前,還需要在pom文件中定義"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"等標籤指定Agent-Class和允許對類定義做修改,示例1-3:"}]},{"type":"codeblock","attrs":{"lang":"html"},"content":[{"type":"text","text":"\n xx.xx.xx\n true\n true\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了load方式外,我們還可以通過attach實現運行時的Instrument,也就是說,可以在JVM啓動後,且所有類型均已全部完成加載之後再對目標類進行重定義。兩種Instrument的使用方式基本大同小異,只是在定義Agent-Class時,入口函數爲agentmain(),示例1-4:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class AgentLauncher {\n public static void agentmain(String agentArgs, Instrumentation inst) throws Throwable {\n inst.addTransformer((a,b,c,d,e) -> 增強後的字節碼, true);\n inst.retransformClasses(Class.forName(agentArgs));//對目標類進行重定義\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,pom文件中所定義的"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"標籤需要更改爲"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"。當對Agent進行打包後,我們只需要根據目標進程的PID便能實現動態附着,示例1-5:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"var vm = VirtualMachine.attach(pid);\nif (Objects.nonNull(vm)) {\n try {\n vm.loadAgent(path, name);\n } finally {\n vm.detach();\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"示例1-2至1-5中,我爲大家簡要介紹了Instrumentation-API的基本使用,但在transform()函數中卻並未實現具體的AOP增強邏輯,那麼接下來,我就爲大家演示如何使用字節碼增強工具Javassist對目標類進行重定義,實現真正意義上的非侵入式運行期AOP增強。"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"介於Javassist簡單易用,並且很好的屏蔽了諸多底層技術細節,使得開發人員在即使不懂JVM指令的情況下也能夠正確的操作字節碼"},{"type":"text","text":"(Dubbo的動態代理生成使用的就是Javassist技術)。使用Javassist創建動態代理有2種方式,一種是基於ProxyFactory的方式,而另一種則是基於動態代碼的實現方式,一般來說,選擇後者可以獲得更好的執行性能,示例1-6:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class Transformer implements ClassFileTransformer {\n @Override\n public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,\n ProtectionDomain protectionDomain, byte[] classfileBuffer)\n throws IllegalClassFormatException {\n return enhancement(className);\n }\n\n private byte[] enhancement(String className) {\n if ((className = className.replaceAll(\"/\", \".\")).equals(\"java.lang.String\")) {\n try {\n var cls = ClassPool.getDefault().get(className);//加載.class文件\n var method = cls.getDeclaredMethod(\"toString\");//獲取增強方法\n \t//增強邏輯\n method.insertBefore(\"System.out.println(\\\"before\\\");\");\n method.insertAfter(\"System.out.println(\\\"after\\\");\");\n return cls.toBytecode();//轉字節碼\n } catch (Throwable e) {\n e.printStackTrace();\n }\n }\n return null;\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述程序示例中,我基於load的方式來實現Instrument,介於ClassLoader每加載一個目標類就會調用transform()函數,所以這裏需要在程序中使用equals()函數來判斷目標類型。上述程序示例中,我對java.lang.String.toString()函數進行了增強,當觸發toString()函數時,方法前後都會執行一段特定的增強代碼,如圖2所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a44c3439ddce753711223acf9d3c23fa.png","alt":null,"title":"圖2 字節碼增強前後對比","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在此大家需要注意,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"由於Javassist的抽象層次較高,儘管簡單易用,但靈活性差,當面對一些複雜場景時(比如:需要根據特定的條件來進行插樁),則顯得無能爲力"},{"type":"text","text":"。因此,在接下來的小節中,我會重點爲大家講解關於字節碼的一些基礎知識,以及常用的JVM指令,以便大家快速上手ASM工具。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"字節碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ASM是一種偏向於指令層面的專用於生成、轉換,以及分析字節碼的底層工具,儘管它的學習門檻和使用成本非常高,但與生俱來的靈活性和高性能卻是它引以爲傲的資本,因此,在掌握ASM的使用之前,大家首先需要對字節碼結構以及JVM指令有所瞭解,否則將會無從下手。我們都知道,Java程序如果想要運行,首先需要由前端編譯器(即:Javac)將符合Java語法規範的源代碼編譯爲符合JVM規範的JVM指令,也就是說,語法指令與JVM指令本質上並不對等,編譯器的作用就像是一個翻譯官,將你原本聽不懂的語言轉換爲你能夠聽懂並深刻理解的語言。接下來,我們先來看一段簡單的Java代碼,示例1-7:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class ByteCode {\n public String id;\n public String getId() {return id;}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"成功編譯爲Class文件後,我們使用文本工具將其打開,如圖3所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2c/2c5b086f31c46186be474a45a7b9c925.png","alt":null,"title":"圖3 字節碼內容","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這一堆密密麻麻的16進制數究竟代表什麼意思?首先,大家需要明確,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"Class文件是一組由8bit字節單位構成的二進制流,各個數據項之間會嚴格按照固定且緊湊的排列順序組合在一起"},{"type":"text","text":"。或許大家存在一個疑問,既然是以8bit爲單位,那麼如果數據項所需佔用的存儲空間超過8bit時應該如何處理?簡單來說,如果是多bit數據項,則會按照big-endian的順序來進行存儲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然Class文件中各個數據項之間是按照固定的順序進行排列的,那麼這些數據項究竟代表着什麼?簡單來說,構成Class文件結構的數據項大致包含10種,如圖4所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6c0b8564a12a6dc92afa8bbce816a767.png","alt":null,"title":"圖4 Class文件結構","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"magic(魔術)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它作爲一個文件標識,當JVM加載目標Class文件時用於判斷其是否是一個標準的Class文件,其16進制的固定值爲0xCAFEBABE,佔32bit。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"version(版本號)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"magic之後是minor_version和major_version數據項,它們用於構成Class文件的格式版本號,分別都佔16bit,在實際的開發過程中,我相信大家都遇見過如下異常,示例1-8:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"Exception in thread \"main\" java.lang.UnsupportedClassVersionError: \nByteCode has been compiled by a more recent version of the Java Runtime (class file version 59.0), \nthis version of the Java Runtime only recognizes class file versions up to 52.0"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從異常堆棧中可以明確,目標Class文件是基於高版本JDK編譯的,超出了當前JVM能夠支持的最大版本範圍,由此可見,通過版本號約束可以在某種程度上避免一些嚴重的運行時錯誤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"constant_pool(常量池)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"constant_pool一個表類型的數據項,其入口處還包含一個constant_pool_count(常量池容量計數器),主要用於存放數值、字符串、final常量值等數據,以及類和接口的全限定名、字段及方法的名稱和描述符等信息。相對於其它數據項而言,constant_pool是其中最複雜和繁瑣的一種,因爲constant_pool中的各項常量類型自身都具有專有的結構,但值得慶幸的是,在實際的開發過程中,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"我們幾乎不必與constant_pool打交道,因爲ASM很好的屏蔽了與常量池相關的所有細節"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"access_flags(訪問標誌)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"constant_pool之後是佔16bit的access_flags"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":",在ASM的使用過程中,我們會經常與其打交道,因爲在定義類、接口,以及聲明各種修飾符時,均會使用到它"},{"type":"text","text":"。ASM操作碼中所定義的access_flags,示例1-9:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"int ACC_PUBLIC = 0x0001; // class, field, method\nint ACC_PRIVATE = 0x0002; // class, field, method\nint ACC_PROTECTED = 0x0004; // class, field, method\nint ACC_STATIC = 0x0008; // field, method\nint ACC_FINAL = 0x0010; // class, field, method, parameter\nint ACC_SUPER = 0x0020; // class\nint ACC_SYNCHRONIZED = 0x0020; // method\nint ACC_VOLATILE = 0x0040; // field\nint ACC_BRIDGE = 0x0040; // method\nint ACC_VARARGS = 0x0080; // method\nint ACC_TRANSIENT = 0x0080; // field\nint ACC_NATIVE = 0x0100; // method\nint ACC_INTERFACE = 0x0200; // class\nint ACC_ABSTRACT = 0x0400; // class, method\nint ACC_STRICT = 0x0800; // method\nint ACC_SYNTHETIC = 0x1000; // class, field, method, parameter\nint ACC_ANNOTATION = 0x2000; // class\nint ACC_ENUM = 0x4000; // class(?) field inner\nint ACC_MANDATED = 0x8000; // parameter"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"this"},{"type":"text","marks":[{"type":"italic"},{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"class(類索引)、super"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"class(超類索引),以及interfaces(接口索引)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"this_class和super_class佔16bit,而interfaces數據項則是一組16bit數據的集合,訪問時通過索引值指向constant_pool來獲取自身、超類,以及相關接口的全限定名,以便於確定一個類的繼承關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"fields(字段表)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"interfaces之後是fields數據項,其入口處還包含一個fields_count(字段計數器),用於表述當前類、接口中包括的所有類字段和實例字段,但不包括從超類繼承的相關字段。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"methods(方法表)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同fields和constant_pool等數據項類似,其入口處同樣也包含一個計數器(methods_count),用於表述當前類或接口中所定義的所有方法的完整描述,但不包括從超類繼承的相關方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"attributes(屬性表)"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"排列在最後是attributes數據項,主要用於存放Class文件中類和接口所定義屬性的基本信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想要對Class文件中的數據項有更深入的瞭解,我建議大家閱讀《Java虛擬機規範》一書,而關於Class文件結構本文則不再過多進行闡述。接下來,我們執行命令"},{"type":"codeinline","content":[{"type":"text","text":"“java -v”"}]},{"type":"text","text":",將示例1-7的編譯結果進行展開,對比下源代碼和部分中間代碼之間的差異,示例1-10:"}]},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"{\n public java.lang.String id;\n descriptor: Ljava/lang/String;\n flags: ACC_PUBLIC\n \n public ByteCode();\n descriptor: ()V\n flags: ACC_PUBLIC\n Code:\n stack=1, locals=1, args_size=1\n 0: aload_0\n 1: invokespecial #1 // Method java/lang/Object.\"\":()V\n 4: return\n LineNumberTable:\n line 22: 0\n LocalVariableTable:\n Start Length Slot Name Signature\n 0 5 0 this LByteCode;\n\n public java.lang.String getId();\n descriptor: ()Ljava/lang/String;\n flags: ACC_PUBLIC\n Code:\n stack=1, locals=1, args_size=1\n 0: aload_0\n 1: getfield #2 // Field id:Ljava/lang/String;\n 4: areturn\n LineNumberTable:\n line 26: 0\n LocalVariableTable:\n Start Length Slot Name Signature\n 0 5 0 this LByteCode;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有幾點大家需要注意,源代碼中的註釋信息(非Annotation)並不會包含在Class文件中,畢竟註釋是給人看的,它是一種增強代碼可讀性的輔助手段,但計算機卻並不需要。其次,package和import部分也不會包含在Class文件中,先前講解索引部分數據項時我曾經提及過,通常這類描述信息都是以全限定名的形式存放於constant_pool中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"細心的同學或許發現了一些端倪,示例1-10中,爲什麼類型全限定名中的符號由\".\"變成了“/”,並且有些前綴還包含有字母(“L”)和符號(“()”)?實際上,這是屬於Class文件的一種內部表示形式,比如語法層面Object類的全限定名格式爲“java.lang.Object”,但是在Class文件中,符號“.”會被“/”所替換,這樣的表述形式我們稱之爲內部名。"}]},{"type":"katexblock","attrs":{"mathString":"java.lang.Object->java/lang/Object"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果是引用類型的字段,那麼爲什麼內部名之前需要再加上字母“L”呢?這是因爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"內部名通常僅用於描述類和接口"},{"type":"text","text":",而諸如字段等類型,Class文件中則提供有另一種表述形式,即類型描述符。其中數組的類型描述符在內部名之前還需要加上符號\"[\",其維度決定了符號“[”的個數,也就是說,如果是二維數組,那麼類型描述符就是“[[Ljava/lang/Object;”,以此類推。除引用類型外,原始數據類型也有自己的類型描述符,Class文件中完整的類型描述符,如圖5所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/eb/eb3c4ed4062dcda7acbf2dce25cd7f1f.png","alt":null,"title":"圖5 類型描述符","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然在描述字段時需要使用到字段描述符,那麼在Class文件中,方法同樣也具備有類似的描述符,叫做方法描述符,也就是在示例1-10中全限定名以符號(“()”)作爲前綴的那部分描述符。方法描述符以符號“(”作爲開頭,其中包含着>=0個類型描述符,即入參類型,並以符號“)”結束,最後緊跟着方法的返回值類型,同樣也是使用類型描述符表述,比如:方法“boolean register(String str1, String str2)”的方法描述符就寫作“(Ljava/lang/String;Ljava/lang/String;)Z”形式。Class文件中完整的方法描述符,如圖6所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/37/377e53bae9b8f07b131b0c3411b772d4.png","alt":null,"title":"圖6 方法描述符","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"基於ASM實現字節碼增強"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管在使用ASM之前,我們需要了解和掌握一些前置知識,並且相對晦澀,但是,這並不代表ASM難以駕馭,就好比Class文件中的複雜數據項constant_pool,難道我們真的需要把其中各項常量類型的結構都弄得一清二楚嗎?答案是不用的,你僅需知道Class文件中有一個被稱之爲constant_pool的數據項,大致瞭解它的作用即可,所有的底層技術細節,ASM在語法層面上均已屏蔽,開發人員除API用法外,唯一需要掌握的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"就是在基於棧型架構的執行模式下,如何將上層語法轉換爲相對應的底層指令集"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們首先來學習下ASM API的基本用法。之前我曾經以及過,ASM除了能作用於字節碼增強外,逆向分析,以及編譯器的中間代碼生成等任務都能很好的勝任,簡而言之,ASM是一個專用於字節碼分析、增強,以及生成的底層工具包,其API提供如下2種使用形式:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"基於事件模型的API;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"基於對象模型的API。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"相對於後者而言,前者擁有絕對的性能優勢,但從使用效率上來說,卻不如擁有更高封裝層次的後者來的方便"},{"type":"text","text":"。本文我會重點討論基於事件模型的API,而關於對象模型API的使用,大家可以參考其它的文獻資料。在基於事件模型API模式下,ASM的整體架構主要是圍繞着分析、轉換,以及生成3個方面進行的,如圖7所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e3/e3779e202535a17af70b748e88e55326.png","alt":null,"title":"圖7 ASM整體架構","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"見名知意,ClassReader用於加載任意Class文件中的內容,並將其轉發給ClassVisitor實現,也就是說,ClassReader在一個完整的事件轉換鏈中是作爲入口程序存在的。而ClassVisitor作爲轉換類及生成類的超類,其中每一個visit()方法都對應着同名類文件的結構部分,比如:方法(visitMethod)、註解(visitAnnotation)、字段(visitField)等,這是一個典型的Visitor模式。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我首先爲大家演示如何基於一個自定義的ClassVisitor實現一個簡單的反編譯程序,示例1-11:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ClassReader reader = new ClassReader(\"java.lang.Object\");//讀取目標類,也可以從二進制流中讀取\n//調用accept()方法啓動\nreader.accept(new ClassVisitor(Opcodes.ASM5) {\n @Override\n public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {\n System.out.printf(\"className:%s extends:%s\\n\", name, superName);\n }\n\n @Override\n public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {\n System.out.printf(\"fieldName:%s desc:%s\\n\", name, desc);\n return super.visitField(access, name, desc, signature, value);\n }\n\n @Override\n public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {\n System.out.printf(\"methodName:%s desc:%s \", name, desc);\n System.out.print(\"exceptions:\");\n if (Objects.nonNull(exceptions)) {\n Arrays.stream(exceptions).forEach(System.out::print);\n } else {\n System.out.print(\"null\");\n }\n System.out.println();\n return super.visitMethod(access, name, desc, signature, exceptions);\n }\n}, 0);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ClassVisitor在上述程序示例中作爲匿名內部類的形式來處理反編譯任務,但在實際的開發過程中,我們卻並沒有太大必要這麼做,因爲在org.objectweb.asm.util包下,ASM爲開發人員提供有TraceClassVisitor類型專用於處理此類任務,示例1-12:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ClassReader reader = new ClassReader(\"java.lang.Object\");\nreader.accept(new TraceClassVisitor(new PrintWriter(System.out)), 0);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ClassVisitor實現可以選擇將相關事件派發給下一個ClassVisitor實現,也可以選擇將其轉發給ClassWriter轉儲,如果選擇後者,那麼一個完整的轉換鏈就構成了。在爲大家講解如何使用ClassVisitor實現修改字節碼之前,我會首先爲大家演示如何使用ClassWriter生成基於棧的指令集,示例1-13:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ClassWriter writer = new ClassWriter(0);\n//創建類標頭\nwriter.visit(V1_8, ACC_PUBLIC, \"UserService\", null, \"java/lang/Object\", null);\n//創建目標類構造函數\nMethodVisitor mv = writer.visitMethod(0, \"\", \"()V\", null, null);\nmv.visitCode();\nmv.visitVarInsn(ALOAD, 0);//將this壓入操作數棧棧頂\n//調用超類構造函數\nmv.visitMethodInsn(INVOKESPECIAL, \"java/lang/Object\", \"\", \"()V\", false);\nmv.visitTypeInsn(NEW, \"java/lang/Object\");//創建Object實例壓入棧頂\nmv.visitInsn(DUP);//拷貝棧頂元素並壓入棧頂\n//調用java/lang/Object構造函數\nmv.visitMethodInsn(INVOKESPECIAL, \"java/lang/Object\", \"\", \"()V\", false);\nmv.visitInsn(POP);//由於後續沒有任何指令操作,因此將棧頂元素彈出\nmv.visitInsn(RETURN);\nmv.visitMaxs(2, 1);//設置操作數棧和局部變量表大小\nmv.visitEnd();\nwriter.visitEnd();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述程序示例中,首先創建ClassWriter實例,然後調用visit()方法創建類標頭,並指定字節碼版本號、訪問修飾符、類名,以及超類等基本信息,這裏我們並不需要指定magic,並且也不需要單獨指定minor_version和major_version,ASM會自行進行處理。當成功創建好類標頭後,接下來就是調用ClassWriter.visitMethod()方法返回一個MethodVisitor實現爲其目標類創建一個缺省的構造函數。其中"},{"type":"codeinline","content":[{"type":"text","text":"mv.visitVarInsn(ALOAD, 0)"}]},{"type":"text","text":"方法用於將this引用壓入操作數棧的棧頂,通過指令"},{"type":"codeinline","content":[{"type":"text","text":"INVOKESPECIAL"}]},{"type":"text","text":"調用它的超類構造函數,至此,缺省構造行爲結束,接下來就是一些具體的用戶指令行爲操作。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/37/37f2f213aec551584e2f5ae2143da6de.png","alt":null,"title":"圖8 執行DUP指令","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的用戶操作非常簡單,語法層面上僅僅只是一個"},{"type":"codeinline","content":[{"type":"text","text":"new Object();"}]},{"type":"text","text":"操作,但是轉換爲字節碼指令後則顯得相對繁冗。首先我們需要使用指令"},{"type":"codeinline","content":[{"type":"text","text":"NEW"}]},{"type":"text","text":"將java/lang/Object實例推入棧頂,然後使用"},{"type":"codeinline","content":[{"type":"text","text":"INVOKESPECIAL"}]},{"type":"text","text":"指令調用其構造函數,最後再使用指令"},{"type":"codeinline","content":[{"type":"text","text":"POP"}]},{"type":"text","text":"彈出棧頂元素,並返回即可。在此大家或許存在一個疑問,當使用"},{"type":"codeinline","content":[{"type":"text","text":"NEW"}]},{"type":"text","text":"指令將目標實例推入棧頂了,爲何還需要再使用指令"},{"type":"codeinline","content":[{"type":"text","text":"DUP"}]},{"type":"text","text":"拷貝棧頂元素再次推入棧頂?其實這很好理解,"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"棧頂元素在被用戶訪問之前,執行引擎首先需要彈出棧頂元素調用其“”方法,因此爲了避免元素出棧後無法爲後續操作提供訪問,所以需要單獨拷貝一份"},{"type":"text","text":",如圖8所示。接下來,我再爲大家演示一個稍微複雜一點的邏輯代碼,示例1-14:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"//創建方法login\nmv = writer.visitMethod(ACC_PUBLIC, \"login\", \"(Ljava/lang/String;Ljava/lang/String;)V\", null, null);\nmv.visitCode();\nmv.visitVarInsn(ALOAD, 1);//將局部變量表中的入參1壓入棧頂\nmv.visitLdcInsn(\"admin\");//將字面值壓入棧頂\n//調用Ljava/lang/Object.equals()比較入參和目標字面值是否相等\nmv.visitMethodInsn(INVOKEVIRTUAL, \"java/lang/String\", \"equals\", \"(Ljava/lang/Object;)Z\", false);\nLabel l0 = new Label();//定義標籤l0\n//IFEQ表示棧頂int類型數值等於0時跳轉到標籤l0處,也就是說,如果equals不成立就跳轉到標籤l0處\n//boolean類型在JVM中的表示形式爲1(表示true)和0(表示false)\nmv.visitJumpInsn(IFEQ, l0);\nmv.visitVarInsn(ALOAD, 2);//將局部變量表中的入參2壓入棧頂\nmv.visitLdcInsn(\"123456\");\nmv.visitMethodInsn(INVOKEVIRTUAL, \"java/lang/String\", \"equals\", \"(Ljava/lang/Object;)Z\", false);\nmv.visitJumpInsn(IFEQ, l0);\n//將System.out靜態字段壓入棧頂\nmv.visitFieldInsn(GETSTATIC, \"java/lang/System\", \"out\", \"Ljava/io/PrintStream;\");\nmv.visitLdcInsn(\"login success\");\n//將字面值和System.out出棧,並調用實例方法println()輸出字面值\nmv.visitMethodInsn(INVOKEVIRTUAL, \"java/io/PrintStream\", \"println\", \"(Ljava/lang/String;)V\", false);\nmv.visitLabel(l0);//標籤l0處邏輯\nmv.visitFrame(F_SAME, 0, null, 0, null);\nmv.visitFieldInsn(GETSTATIC, \"java/lang/System\", \"out\", \"Ljava/io/PrintStream;\");\nmv.visitLdcInsn(\"login fial\");\nmv.visitMethodInsn(INVOKEVIRTUAL, \"java/io/PrintStream\", \"println\", \"(Ljava/lang/String;)V\", false);\nmv.visitInsn(RETURN);\nmv.visitMaxs(2, 3);//設置操作數棧和局部變量表大小\nmv.visitEnd();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述程序示例相對1-13來說要複雜得多,整體來看就是定義了多個流程控制語句的邏輯代碼。首先,我們先將局部變量表中的入參1壓入棧頂,然後將一個String類型的值壓入棧頂,緊接着通過指令"},{"type":"codeinline","content":[{"type":"text","text":"INVOKEVIRTUAL"}]},{"type":"text","text":"調用Ljava/lang/Object.equals()方法驗證這2個值是否相等。在此大家需要注意,在JVM指令中,沒有if-else流程控制語句這樣的命令,都是通過定義Label的方式執行jump的。如果表達式不成立,就直接跳轉到標籤l0處,l0處的邏輯實際上就是輸出"},{"type":"codeinline","content":[{"type":"text","text":"System.out.println(\"login fail\");"}]},{"type":"text","text":",反之繼續向下執行,再定義相同的指令繼續驗證另外2個String類型的值是否相等,不匹配時跳轉到標籤l0處,反之輸出"},{"type":"codeinline","content":[{"type":"text","text":"System.out.println(\"login success\");"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或許有些同學會存在疑問,實現一個簡單的邏輯代碼時都需要編寫這麼複雜的指令集,並且在書寫指令的過程中,在所難免會出現一些錯誤,那麼有什麼好辦法可以在生成字節碼之前檢測出異常指令呢(比如:對方法的調用順序是否恰當,以及參數是否合理有效)?值得慶幸的是,ASM在org.objectweb.asm.util包下爲開發人員提供了CheckClassAdapter類型和TraceClassVisitor類型來協助減少指令編碼時的異常情況,並且它們可以出現在整個轉換鏈的任何地方,示例1-15:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"ClassWriter writer = new ClassWriter(0);\n//將事件傳遞給ClassWriter\nCheckClassAdapter check = new CheckClassAdapter(writer);\n//將事件傳遞給CheckClassAdapter\nTraceClassVisitor trace = new TraceClassVisitor(check, new PrintWriter(System.out));\nwriter.visitEnd();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本小節的最後,我再爲大家演示下如何通過自定義ClassVisitor實現來對目標字節碼進行增強,其增強邏輯的主要內容爲,對實現了java.lang.Runnable接口的任意類型的 run()方法前後插樁一段println()函數。首先我們需要在Transformer.transform()函數中進行相應的判斷(基於Agent on load),只有Runnable實現纔會執行相關的增強邏輯,示例1-16:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"//由transform()函數觸發調用\nprivate byte[] enhancement(byte[] classfileBuffer) {\n ClassReader reader = new ClassReader(classfileBuffer);\n String className = reader.getClassName();\n String[] interfaces = reader.getInterfaces();//獲取目標類接口\n if (Objects.nonNull(interfaces) && interfaces.length > 0) {\n boolean isEnhancement = false;\n for (String interfaceName : interfaces) {//只有標記有Runnable接口的類型才需要增強\n if (interfaceName.equals(\"java/lang/Runnable\")) {\n isEnhancement = true;\n break;\n }\n }\n try {\n //相關增強邏輯\n if (isEnhancement) {\n ClassWriter writer = new ClassWriter(0);//聲明生成類\n reader.accept(new ClassEnhancementAdapter(Opcodes.ASM5,\n new TraceClassVisitor(writer, new PrintWriter(System.out)), className), 0);//分析入口\n return writer.toByteArray();//返回增強後的字節碼\n }\n } catch (Throwable e) {\n log.error(\"method:{},Enhancement failed!!!\", className, e);\n }\n }\n return classfileBuffer;\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在ClassVisitor實現中,我們需要重寫其visitMethod()方法,判斷目標run()方法和實現增強邏輯,示例1-17:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);\n/**\n * 方法增強需要滿足幾個條件:\n * 1、非接口類型\n * 2、MethodVisitor非空\n * 3、非函數\n * 4、匹配run()函數\n */\nif (!isInterface && Objects.nonNull(methodVisitor)\n && !name.equals(\"\")\n && name.equals(\"run\")) {\n methodVisitor = new MethodEnhancementAdapter(methodVisitor, className);\n}\nreturn methodVisitor;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"增強邏輯具體由MethodEnhancementAdapter來負責,它是一個MethodVisitor的實現,before邏輯需要在其visitCode()方法中進行插樁,而after邏輯則需要在visitInsn()方法中進行插樁,示例1-18:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Override\npublic void visitCode() {\n mv.visitCode();\n mv.visitFieldInsn(GETSTATIC,\"java/lang/System\",\"out\",\"Ljava/io/PrintStream;\");\n mv.visitLdcInsn(\"before\");\n mv.visitMethodInsn(INVOKEVIRTUAL,\"Ljava/io/PrintStream;\",\"println\",\"(Ljava/lang/String;)V\",false);\n}\n\n@Override\npublic void visitInsn(int opcode) {\n if(opcode == RETURN){\n mv.visitFieldInsn(GETSTATIC,\"java/lang/System\",\"out\",\"Ljava/io/PrintStream;\");\n mv.visitLdcInsn(\"after\");\n mv.visitMethodInsn(INVOKEVIRTUAL,\"Ljava/io/PrintStream;\",\"println\",\"(Ljava/lang/String;)V\",false);\n }\n super.visitInsn(opcode);\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在visitInsn()方法中進行插樁時我們需要對當前指令進行判斷,也就是說,after的增強邏輯應該發生在"},{"type":"codeinline","content":[{"type":"text","text":"RETURN"}]},{"type":"text","text":"指令執行之前。至此,關於字節碼增強和ASM的整體使用就暫時介紹到這裏,而關於一些更復雜的增強用法,建議大家閱讀"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/cef6d2931a54f85142d863db7","title":""},"content":[{"type":"text","text":"硬核系列 | 深入剖析 Java 協程"}]},{"type":"text","text":"。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"類隔離策略"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家需要注意,在很多情況下,我們的Agent包中大概率會包含一些與目標程序相沖突的第三方構件依賴,在這種情況下,爲了避免產生類污染,衝突等問題,Advice則只能由自定義類加載器來負責裝載,那麼這時就會面臨一個問題,由子類加載器負責加載的類對父類加載器而言是不可見的,那麼業務代碼中應該如何調用Advice的代碼呢?"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000080","name":"user"}}],"text":"出於對效率等多方面因素的考慮,我們可以在最頂層的類加載器Bootstrap ClassLoader中註冊一個對虛擬機中所有類加載器都具備可見性的間諜類Spy"},{"type":"text","text":",如圖9所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0c/0c034c58e54d3276ba8aa8f3298eccd7.png","alt":null,"title":"圖9 類隔離策略","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"間諜類的作用是用來打通類隔離後的“通訊”操作,而對目標類進行增強時,並不會直接把增強邏輯固化到目標類上,而是持有一個對間諜類的引用,由間諜類負責持有對隔離類的方法引用(java.lang.reflect.Method),通過反射的方式來調用增強邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至此,本文內容全部結束。如果在閱讀過程中有任何疑問,歡迎加入微信羣聊和小夥伴們一起參與討論。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1f/1fb2df1b828c32e511ddbaca641ba207.png","alt":null,"title":"","style":[{"key":"width","value":"25%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"推薦文章:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/cef6d2931a54f85142d863db7","title":""},"content":[{"type":"text","text":"硬核系列 | 深入剖析 Java 協程"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/61868b3f66de36d32a5f1434f","title":""},"content":[{"type":"text","text":"白玉試毒 | 灰度架構設計方案"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/655943e5f85e6f79ffbd03047","title":""},"content":[{"type":"text","text":"新時代背景下的 Java 語法特性"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/92ba88c7926b5f5c6fbc11830","title":""},"content":[{"type":"text","text":"剖析 Java15 新語法特性"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/4d571787a3280ef3094338f9b","title":""},"content":[{"type":"text","text":"看門狗 | 分佈式鎖架構設計方案 -01"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/545a3accd173d6517ebd0ad59","title":""},"content":[{"type":"text","text":"看門狗 | 分佈式鎖架構設計方案 -02"}]}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#006400","name":"user"}}],"text":"碼字不易,歡迎轉發"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章