Java Agent 動態修改字節碼詳情
假設您有一個在生產環境中運行的應用程序。每隔一段時間,它就會進入中斷狀態,錯誤很難重現,您需要從應用程序中獲得更多信息。
那麼你想知道解決方案嗎?
您可以做的是動態地將一些代碼集附加到應用程序中,並仔細地重寫它,以便代碼轉儲您可以記錄的其他信息,或者您可以將應用程序階段轉儲到文本文件中。Java
為我們提供瞭使用Java Agent
實現這一點的工具。
你有沒有想過我們的Java代碼是如何在IDE
中進行熱交換的?這是因為特工。關於Java Agent
的另一個有趣的事實是,應用程序探查器在後端使用相同的技術來收集內存使用情況、內存泄漏和方法執行時間的信息。
1、什麼是Java Agent
Java Agent是一種特殊類型的類,通過使用Java Instrumentation API,它可以攔截JVM上運行的應用程序,修改它們的字節碼。Java Agent非常強大,也非常危險。
在開始之前,我將解釋Java Agent如何使用簡單的HelloWorld示例攔截類。
public class Hello { public static void main(String[] args){ System.out.println("hello world"); } }
如下圖所示:
類加載器負責將類從二進制加載到內存中。運行編譯後的HelloWorld應用程序(HelloWorld.class
)時,可以將Agent視為在運行時攔截類加載器行為的一種方式。您可能會想,java字節代碼是如何被重新構造的,以便Agent可以在正確的位置添加相關代碼的。有趣的是,對於Java程序來說,字節碼的結構非常接近原始Java程序源代碼。因此,雖然我們不為Java程序本身添加工具,但我們使用瞭它的一個非常接近的表示形式。需要註意的是,有一些非Java語言可以編譯成Java字節碼(如Scala、Clojure和Kotlin),這意味著程序字節碼的結構和形狀可能會非常不同。
2、實現Java Agent
JavaAgent基於來自Java平臺的facility,它的入口點是Java.lang instrument
包,它提供瞭允許Agent為JVM上運行的程序提供工具的服務。該包非常簡單且自包含,因為它包含兩個異常類、一個數據類、類定義和兩個接口。在這兩種方法中,如果我們想編寫Java Agent,我們隻需要實現classFileTransformer
接口。
定義代理有兩種方法。
第一個是靜態代理,這意味著我們構建Agent代理,將其打包為jar文件,當我們啟動Java應用程序時,我們傳入一個名為java agent的特殊JVM參數。然後我們給它代理jar在磁盤上的位置,然後JVM發揮它的魔力。
$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar file you want to intecept>
我們需要添加一個特殊的清單條目,稱為pre-main類,當然,這是一個完全限定的名稱類定義。
Premain-Class : org.example.JavaAgent
這個類看起來像這樣
public class JavaAgent { /** * As soon as the JVM initializes, This method will be called. * * @param agentArgs The list of agent arguments * @param instrumentation The instrumentation object * @throws InstantiationException */ public static void premain(String agentArgs, Instrumentation instrumentation) throws InstantiationException { InterceptingClassTransformer interceptingClassTransformer = new InterceptingClassTransformer(); interceptingClassTransformer.init(); instrumentation.addTransformer(interceptingClassTransformer); } }
premain
方法接受兩個參數:
agentArgs
—字符串參數,用戶選擇作為參數傳遞給Java Agent調用的任何參數。instrumentation
來自java.lang instrument
包,我們可以添加一個新的ClassFileTransformer
對象,它包含Agent的實際邏輯。
第二個選項稱為動態代理。
我們可以做的不是檢測啟動應用程序的方式,而是編寫一小段代碼,接收並連接到現有JVM,並告訴它加載某個代理。
VirtualMachine vm = VirtualMachine.attach(vmPid); vm.load(agentFilePath); vm.detach();
此參數agentFilePath與靜態代理方法中的參數完全相同。它必須是agent jar的文件名,因此沒有輸入流也沒有字節。這種方法有兩個警告。第一個是,這是生活在com.sun空間下的私有API,它通常適用於熱點實現。第二個問題是,使用Java9進行排序時,您不能再使用此代碼連接到它正在運行的JVM。
2.1 類轉換
這是我們需要為agent實現的接口,以便轉換類。
public interface ClassFileTransformer { byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException; }
這有點多,但我將解釋方法簽名中的必要參數。第一個重要的是類名。此參數的主要目的是幫助查找並區分要攔截的正確類和其他類。顯然,您可能不想截取應用程序中的每個類,最簡單的方法是使用條件語句進行檢查。
然後是類加載器,它主要用於基本應用程序沒有平坦類空間的環境中,您可能不需要查看它,但一旦遇到更復雜或模塊化的平臺,您就需要查看類加載器。classfileBuffer
是檢測前類的當前定義。要截取它,您需要使用庫讀取這個字節數組並截取代碼,然後必須再次轉換回字節碼才能返回。
有幾個字節碼生成庫。您需要進行研究,並根據它是高級API還是低級API、社區大小和許可證自行決定。我放在下面的演示是Javassist,因為我認為它在高級和低級API之間有一個很好的平衡,並且是一個三重許可證,所以幾乎任何人都可以使用它。這
就是ClassFileTransformer
實現的主體。
@Override public byte[] transform(ClassLoader loader, ..) throws .. { byte[] byteCode = classfileBuffer; // If you wanted to intercept all the classs then you can remove this conditional check. if (className.equals("Example")) { try { ClassPool classPool = scopedClassPoolFactory.create(loader, rootPool, ScopedClassPoolRepositoryImpl.getInstance()); CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod method : methods) { if (method.equals("main")) { method.insertAfter("System.out.println(\"Logging using Agent\");"); } } byteCode = ctClass.toBytecode(); ctClass.detach(); } catch (Throwable ex) { log.log(Level.SEVERE, "Error in transforming the class: " + className, ex); } } return byteCode; } ctClass.detach(); } catch (Throwable ex) { log.log(Level.SEVERE, "Error in transforming the class: " + className, ex); } } return byteCode; }
在這裡,從類池中,我們可以直接繞過classfileBuffer
獲取類,因為我想使用main
方法。我們循環遍歷類定義中的所有方法,得到我們想要的類。我們根本不需要使用字節碼。我們可以簡單地向它傳遞一些合法的Java代碼,然後Javassist將編譯它,生成新的字節碼,並給出定義。
有三種方法可以向方法中插入一些Java代碼。insertAfter(..)
在正文末尾插入字節碼。它在正文的末尾插入字節碼。insertAt(..)
在正文的指定行插入字節碼,insertBefore(..)
在正文的開頭插入字節碼。
2.2 使用Java代理進行實際操作
從指定的鏈接下載示例應用程序和Java Agent。
使用進入路徑並執行命令mvn clean install
來構建這兩個repo
現在,您將獲得目標中的jar文件。復制示例應用程序中.jar文件的路徑,並復制Java Agent中-dependencies.jar
文件的路徑。
首先,使用命令$java-jar<path of the packaged jar>
僅與示例應用程序一起運行應用程序,並觀察輸出。Hi I am main。將在控制臺中打印。
然後,使用命令$ java -javaagent:<path of agent jar file> -jar <path of the packaged jar
並觀察輸出。將在控制臺中另外打印使用代理的日志記錄。這確保
file you want to intercept>java agent
被攔截並添加到main
方法的主體中。
總之,如果要實現Java Agent,請執行以下操作:
1. 您需要創建兩個Java類。一個是premain
方法(JavaAgent),另一個是擴展ClassFileTransformer的類(CustomTransformer
)
2. 在premain
方法的主體內,需要添加擴展ClassFileTransformer
的類的對象
3. 然後,您需要在CustomTransformer中的重寫方法transform中添加邏輯。
4. 在轉換方法內轉換字節碼時,可能需要根據用途使用字節碼生成庫。
5. 您需要在清單中指定premain
類並構建jar。
6. 使用javaagent
標記將代理加載到要攔截的應用程序中。
到此這篇關於Java Agent動態修改字節碼詳情的文章就介紹到這瞭,更多相關Java Agent動態修改字節碼內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java Agents代理是什麼
- 通過使用Byte Buddy便捷創建Java Agent
- 深度分析java dump文件
- 詳解JVM基礎之字節碼的增強技術
- maven為MANIFEST.MF文件添加內容的方法