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