Java如何使用Agent和ASM在字節(jié)碼層面實現方法攔截
Java Agent
Java Agent 是一種運行在 Java 虛擬機 (JVM) 上的特殊程序,可以在程序運行期間對字節(jié)碼進行修改和增強,從而達到在不修改源碼的情況下實現各種功能的目的。
Java Agent 的主要作用包括但不限于以下幾點:
字節(jié)碼增強:通過修改字節(jié)碼,實現一些功能增強,比如方法攔截、性能監(jiān)控等。
類加載控制:可以在類加載前對類進行修改或者替換,實現一些定制化需求。
內存分析:通過 Java Agent 可以獲取到 JVM 的內存信息,對內存進行分析,幫助排查內存相關問題。
代碼檢查:通過 Java Agent 可以在類加載前對代碼進行檢查,實現一些代碼質量相關的需求。
ASM
ASM(全稱:ASMifier Class Visitor),是一個輕量級的 Java 字節(jié)碼編輯和分析框架,可以直接以二進制形式讀取和修改類文件。ASM 提供了許多 API 和工具,可以方便地進行字節(jié)碼修改和生成。
ASM 的主要作用包括但不限于以下幾點:
字節(jié)碼生成:可以通過 ASM 生成 Java 類的字節(jié)碼,可以用于生成代理類、動態(tài)生成類等場景。
字節(jié)碼修改:可以通過 ASM 對已有的類字節(jié)碼進行修改,實現一些類增強、方法攔截等功能。
字節(jié)碼分析:可以通過 ASM 對已有的類字節(jié)碼進行分析,實現一些類結構的分析和轉換。
實踐
使用 Java Agent 和 ASM 實現方法攔截
需求背景
在一個項目中,統(tǒng)一對catch異常進行處理,例如日志輸出(含堆棧),由于研發(fā)人員水平不一,有很多時候打印日志格式不同意,也不利于做一些埋點工作。
應用層代碼
package com.example.demo.agent; import lombok.extern.slf4j.Slf4j; @Slf4j public class Test { public static void main(String[] args) { try { int i = 1 / 0; } catch (Exception e) { // 由字節(jié)碼增強來輸出 } } }
構建探針jar包
package com.example.demo.agent; import java.lang.instrument.Instrumentation; import java.util.Set; public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { // 獲取需要掃描的包名 Set<String> basePackages = ConfigService.getBasePackages(); // 構造 MyClassTransformer MyClassTransformer transformer = new MyClassTransformer(basePackages); inst.addTransformer(transformer); } }
在 Java Agent 中,premain 方法是 Java 虛擬機啟動時調用的入口方法。它允許我們在應用程序啟動之前對字節(jié)碼進行修改或者進行一些預處理操作。
premain 方法是 Java Agent 的必要組成部分,用于指定 Java Agent 的初始化邏輯。當我們將 Java Agent JAR 文件通過 -javaagent 參數傳遞給 Java 虛擬機時,虛擬機會加載并初始化 Java Agent,并在應用程序啟動之前調用 premain 方法。
在 premain 方法中,我們可以通過獲取 Instrumentation 實例來注冊自定義的轉換器(Transformer),并對加載的類進行字節(jié)碼轉換。通過在 premain 方法中注冊轉換器,我們可以在類加載過程中對類的字節(jié)碼進行修改,實現類似方法攔截、性能統(tǒng)計、日志記錄等功能。
因此,實現 premain 方法是 Java Agent 的一項必要要求,它是 Java Agent 啟動和初始化的入口方法,用于配置和注冊自定義的轉換器。
Maven 插件配置
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestEntries> <Premain-Class>com.example.demo.agent.MyAgent</Premain-Class> </manifestEntries> </archive> </configuration> </plugin>
這是 Maven 的插件配置,用于配置生成的 JAR 文件的元數據信息,其中 是設置 Java Agent 的入口類。
在 Java Agent 中,需要在 JAR 文件的 MANIFEST.MF 文件中指定 Java Agent 的入口類,以便 Java 虛擬機可以正確地加載和啟動 Java Agent。通過 Maven 的 maven-jar-plugin 插件配置,我們可以方便地指定 Java Agent 的入口類。
在上述配置中, 元素指定了 com.example.demo.agent.MyAgent 類作為 Java Agent 的入口類。當我們使用 Maven 構建項目并生成 JAR 文件時,插件會自動生成包含這個元數據信息的 MANIFEST.MF 文件,并將其包含在生成的 JAR 文件中。
這樣,當我們將生成的 JAR 文件作為 Java Agent 使用時,Java 虛擬機會讀取 JAR 文件中的 MANIFEST.MF 文件,并根據其中指定的入口類啟動 Java Agent。這樣就能確保 Java Agent 正確加載和執(zhí)行,完成相應的字節(jié)碼轉換或其他操作。
ASM處理字節(jié)碼
package com.example.demo.agent; import aj.org.objectweb.asm.Opcodes; import org.objectweb.asm.*; import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; import java.util.HashSet; import java.util.Set; import static org.objectweb.asm.Opcodes.*; public class MyClassTransformer implements ClassFileTransformer { // basePackages是需要增強的類所在的包的集合 private final Set<String> basePackages; // 獲取該ClassTransformer類的全限定名,將包名中的點號替換為文件路徑中的分隔符 private static final String OWNER = MyClassTransformer.class.getCanonicalName().replace(".", File.separator); public MyClassTransformer(Set<String> basePackages) { this.basePackages = basePackages; } public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 判斷是否需要對該類進行增強,如果不需要直接返回原字節(jié)碼數據 if (!needEnhance(className)) { return classfileBuffer; } System.out.println("Transforming class: " + className); // 利用ASM對字節(jié)碼進行增強 try { // 創(chuàng)建ClassReader對象 ClassReader cr = new ClassReader(className); // 創(chuàng)建ClassWriter對象 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 創(chuàng)建ClassVisitor對象,對字節(jié)碼進行訪問 ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { // 對每個方法進行訪問,返回MethodVisitor對象進行訪問 MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // 創(chuàng)建MethodVisitor對象,對方法字節(jié)碼進行訪問 return new MethodVisitor(Opcodes.ASM5, mv) { // 存儲try-catch處理器的標簽 private final Set<Label> tryCatchBlockHandlers = new HashSet<>(); @Override public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { // 對visitTryCatchBlock方法進行訪問,在訪問方法中存儲try-catch處理器的標簽 tryCatchBlockHandlers.add(handler); super.visitTryCatchBlock(start, end, handler, type); } @Override public void visitLineNumber(int line, Label start) { // 對visitLineNumber方法進行訪問,在訪問方法中插入方法調用指令 if (tryCatchBlockHandlers.contains(start)) { // 當該行代碼處于try-catch塊中時,在該行代碼前插入方法調用指令 mv.visitMethodInsn(INVOKESTATIC, OWNER, "logStackTrace", "(Ljava/lang/Throwable;)V", false); } super.visitLineNumber(line, start); } }; } }; // 開始訪問ClassReader中的字節(jié)碼 cr.accept(cv, ClassReader.EXPAND_FRAMES); // 返回增強后的字節(jié)碼數據 return cw.toByteArray(); } catch (Exception e) { System.out.println("MyClassTransformer e=" + e); } // 出現異常時返回原字節(jié)碼數據 return classfileBuffer; } // 定義方法 public static void logStackTrace(Throwable throwable) { System.out.println("統(tǒng)一打印堆棧:"); throwable.printStackTrace(); } private boolean needEnhance(String className) { for (String basePackage : basePackages) { if (className.startsWith(basePackage)) { return true; } } return false; } }
這段代碼實現了一個 ClassFileTransformer 接口的類 MyClassTransformer,它用于對指定的類進行字節(jié)碼增強。
主要做了以下事情:
- 在構造方法中接收需要增強的類所在的包的集合 basePackages。
- 實現了 transform 方法,該方法是 ClassFileTransformer 接口的核心方法,用于對類的字節(jié)碼進行轉換和增強。
- 在 transform 方法中,首先判斷當前類是否需要進行增強,如果不需要則直接返回原字節(jié)碼數據。
- 使用 ASM 庫進行字節(jié)碼的讀取和修改。通過創(chuàng)建 ClassReader 對象讀取原始字節(jié)碼,創(chuàng)建 ClassWriter 對象進行修改,創(chuàng)建 ClassVisitor 對象對字節(jié)碼進行訪問和修改。
- 在 ClassVisitor 的 visitMethod 方法中,對每個方法進行訪問,并創(chuàng)建 MethodVisitor 對象對方法的字節(jié)碼進行訪問和修改。
- 在 MethodVisitor 的 visitLineNumber 方法中,當該行代碼處于 try-catch 塊中時,在該行代碼前插入方法調用指令,調用名為 logStackTrace 的靜態(tài)方法,用于打印堆棧信息。
- 最后,在 needEnhance 方法中判斷是否需要對類進行增強,如果類的包名在 basePackages 中,則返回 true,否則返回 false。
總體而言,這段代碼的作用是在指定的類中的每個方法中插入一段代碼,在方法調用處打印堆棧信息,用于統(tǒng)一處理異常的情況。這樣可以方便地進行日志記錄或其他異常處理操作。
指定類路徑
application.properties:
package com.example.demo.agent; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Properties; import java.util.Set; public class ConfigService { private static final String CONFIG_FILE_PATH = "demo/src/main/resources/application.properties"; private static final String BASE_PACKAGES_PROPERTY = "basePackages"; private static Set<String> basePackages; static { Properties props = new Properties(); try (InputStream is = new FileInputStream(CONFIG_FILE_PATH)) { props.load(is); String basePackagesStr = props.getProperty(BASE_PACKAGES_PROPERTY); basePackages = new HashSet<>(Arrays.asList(basePackagesStr.split(","))); System.out.println("basePackages=" + basePackages); } catch (IOException e) { // 處理異常 } } public static Set<String> getBasePackages() { return basePackages; } }
這段代碼是一個配置服務類 ConfigService,主要用于讀取配置文件并提供基礎包名的集合。
具體功能如下:
定義了配置文件的路徑 CONFIG_FILE_PATH,這里假設配置文件為 application.properties,位于 demo/src/main/resources/ 目錄下。
定義了配置文件中基礎包名的屬性名稱 BASE_PACKAGES_PROPERTY,用于讀取配置文件中的基礎包名。
聲明了一個靜態(tài)的 Set 類型的變量 basePackages,用于存儲從配置文件中讀取到的基礎包名集合。
在靜態(tài)代碼塊中,通過 Properties 對象讀取配置文件,并將配置文件中的基礎包名字符串拆分為數組,然后轉換為集合存儲在 basePackages 變量中。
最后,提供了一個靜態(tài)方法 getBasePackages(),用于獲取讀取到的基礎包名集合。
總體而言,這段代碼的作用是從配置文件中讀取基礎包名集合,并提供訪問該集合的方法。這樣可以將需要進行方法攔截的類所在的包名配置到配置文件中,以便在 MyClassTransformer 類中使用。
Idea執(zhí)行
Run/Debug Configurations:
-noverify是Java虛擬機的一個啟動選項,用于禁用類驗證器(Class Verifier)。類驗證器是Java虛擬機的一部分,負責驗證字節(jié)碼的結構和語義是否符合Java語言規(guī)范。它檢查類文件中的字節(jié)碼指令,確保它們不會違反虛擬機的安全性和完整性。
運行效果
在本文中,我們深入探索了如何在字節(jié)碼層面實現方法攔截,并發(fā)現了 Java Agent 和 ASM 的魅力。Java Agent 是一種強大的工具,允許我們在應用程序啟動時通過字節(jié)碼轉換來修改類的行為。而 ASM 是一個強大而靈活的字節(jié)碼操作庫,提供了豐富的API來讀取、修改和生成字節(jié)碼。
通過結合 Java Agent 和 ASM,我們可以實現方法攔截的功能。我們首先編寫了一個 Java Agent,并使用 Premain-Class 來指定其入口點。在 Java Agent 中,我們使用 Instrumentation API 注冊了一個 ClassFileTransformer,該轉換器負責對加載的類進行轉換。然后,我們定義了一個實現 ClassFileTransformer 接口的類,使用 ASM 對字節(jié)碼進行操作。
具體來說,我們使用 ASM 創(chuàng)建了一個 ClassVisitor,用于訪問和修改類的字節(jié)碼。在 ClassVisitor 中,我們重寫了 visitMethod 方法,用于訪問和修改類中的方法字節(jié)碼。我們利用 MethodVisitor 對方法字節(jié)碼進行訪問和修改,實現了方法攔截的功能。在示例中,我們演示了如何在方法的異常處理器(try-catch 塊)中插入代碼,以實現異常拋出時的統(tǒng)一堆棧打印。
通過本文的探索和實踐,我們深刻體會到了 Java Agent 和 ASM 的魅力,它們?yōu)槲覀兲峁┝藷o限的可能性,讓我們能夠更加靈活和精確地控制和改變程序的行為。無論是在調試和分析應用程序,還是在實現特定的需求和功能方面,掌握字節(jié)碼級別的方法攔截技術都是非常有價值的。希望本文能為讀者提供有關 Java Agent 和 ASM 的深入理解,并啟發(fā)讀者在實際項目中嘗試和應用這些強大的技術。
到此這篇關于Java如何使用Agent和ASM在字節(jié)碼層面實現方法攔截的文章就介紹到這了,更多相關Java方法攔截內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
logback中顯示mybatis查詢日志文件并寫入的方法示例
這篇文章主要為大家介紹了logback中顯示mybatis查詢日志文件并寫入的方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-03-03Jackson將json string轉為Object,org.json讀取json數組的實例
下面小編就為大家?guī)硪黄狫ackson將json string轉為Object,org.json讀取json數組的實例,具有很好的參考價值,希望對大家有所幫助2017-12-12Spring-AOP自動創(chuàng)建代理之BeanNameAutoProxyCreator實例
這篇文章主要介紹了Spring-AOP自動創(chuàng)建代理之BeanNameAutoProxyCreator實例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Java文件字符輸入流FileReader讀取txt文件亂碼的解決
這篇文章主要介紹了Java文件字符輸入流FileReader讀取txt文件亂碼的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09SpringBoot注解@EnableScheduling定時任務詳細解析
這篇文章主要介紹了SpringBoot注解@EnableScheduling定時任務詳細解析,@EnableScheduling 開啟對定時任務的支持,啟動類里面使用@EnableScheduling 注解開啟功能,自動掃描,需要的朋友可以參考下2024-01-01