B站一面:手撕一個 Java Agent!
最近,有小伙伴反饋:B站 1面要手撕一個 Java Agent,直接把他搞懵逼了。這篇文章,我們將針對這么小伙伴遇到的問題,深入分析什么 Java Agent及其工作原理,最后帶領(lǐng)大家手撕一個 Java Agent。
一、什么是 Java Agent?
Java Agent 是一種特殊的 Java程序,從 Java5 開始支持,它可以在 Java虛擬機(jī)(JVM)啟動時或運(yùn)行時加載,并且能夠在不修改原始源代碼的情況下對字節(jié)碼進(jìn)行操作。
二、Java Agent原理
Java Agent 的核心原理是通過 Java Instrumentation API提供的機(jī)制,在類加載時或運(yùn)行時動態(tài)修改字節(jié)碼。這里涉及到主要的幾個技術(shù)點(diǎn):
- Instrumentation 接口
- Premain() 和 Agentmain()方法
1.Instrumentation
Instrumentation是 Java SE 5 在java.lang.instrument包下引入的一個接口,主要用于字節(jié)碼操作。它提供了以下幾個關(guān)鍵功能:
- 類轉(zhuǎn)換:允許在類加載時對字節(jié)碼進(jìn)行修改。
- 代理類生成:可以在運(yùn)行時生成新的類。
- 對象監(jiān)控:可以獲取JVM中的對象信息,如內(nèi)存使用情況。
Instrumentation 接口提供了一組用于操作類和對象的方法,以下是一些主要的方法及其說明:
(1) addTransformer
作用:添加一個 ClassFileTransformer,用于在類加載時對字節(jié)碼進(jìn)行修改。源碼如下:
/**
* @param transformer:要移除的字節(jié)碼轉(zhuǎn)換器
*/
void addTransformer(ClassFileTransformer transformer);
/**
* @param transformer:要移除的字節(jié)碼轉(zhuǎn)換器
* @param canRetransform:指示是否允許重新轉(zhuǎn)換已經(jīng)加載的類
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
(2) removeTransformer
作用:移除一個之前添加的ClassFileTransformer。源碼如下:
/**
* @param transformer:要移除的字節(jié)碼轉(zhuǎn)換器
* @return 如果轉(zhuǎn)換器被成功移除,則返回true,否則返回false
*/
boolean removeTransformer(ClassFileTransformer transformer);
(3) retransformClasses
作用:重新轉(zhuǎn)換已經(jīng)加載的類。源碼如下:
/**
* @param classes:要重新轉(zhuǎn)換的類
* @throws 如果某個類不能被修改,則拋出UnmodifiableClassException
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
(4) redefineClasses
作用:重新定義已經(jīng)加載的類。源碼如下:
/**
* @param definitions:包含類的定義及其新的字節(jié)碼
* @throws 如果類不能被修改或未找到,則拋出相應(yīng)的異常
*/
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
(5) isModifiableClass
作用:檢查一個類是否可以被修改。源碼如下:
/**
* @param theClass:要檢查的類
* @return 如果類可以被修改,則返回true,否則返回false
*/
boolean isModifiableClass(Class<?> theClass);
(6) isRetransformClassesSupported
作用:檢查當(dāng)前JVM是否支持重新轉(zhuǎn)換已經(jīng)加載的類。
/**
* @return 如果支持,則返回true,否則返回false
*/
boolean isRetransformClassesSupported();
(7) isRedefineClassesSupported
作用:檢查當(dāng)前JVM是否支持重新定義已經(jīng)加載的類。源碼如下:
/**
* @return 如果支持,則返回true,否則返回false
*/
boolean isRedefineClassesSupported();
(8) getAllLoadedClasses
作用:獲取當(dāng)前JVM中所有已經(jīng)加載的類。源碼如下:
/**
* @return 一個包含所有已加載類的數(shù)組
*/
Class<?>[] getAllLoadedClasses();
(9) getInitiatedClasses
作用:獲取由指定類加載器加載的所有類。源碼如下:
/**
* @param loader:類加載器
* @return 一個包含所有由指定類加載器加載的類的數(shù)組
*/
Class<?>[] getInitiatedClasses(ClassLoader loader);
(10) getObjectSize
作用:獲取指定對象的內(nèi)存大小。源碼如下:
/**
* @param objectToSize:要獲取大小的對象
* @return 對象的內(nèi)存大小(以字節(jié)為單位)
*/
long getObjectSize(Object objectToSize);
2.Premain 和 Agentmain
Java Agent 的入口是兩個特殊的方法:premain() 和 agentmain(),這兩個方法分別用于在 JVM啟動時和運(yùn)行時加載 Agent。
- premain:在JVM啟動時執(zhí)行。類似于C語言中的main函數(shù)。
- agentmain:在JVM運(yùn)行時通過Attach機(jī)制加載Agent。
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// 在JVM啟動時執(zhí)行的代碼
}
public static void agentmain(String agentArgs, Instrumentation inst) {
// 在JVM運(yùn)行時加載Agent時執(zhí)行的代碼
}
}
三、手撕 Java Agent
手撕一個 Java Agent 主要包括以下 4個步驟:
- 編寫 Agent類:包含 premain() 或 agentmain() 方法。
- 編寫 MANIFEST.MF 文件:指定 Agent 的入口類。
- 打包成 JAR 文件:包含 Agent 類和 MANIFEST 文件。
- 使用 Agent:通過指定 JVM 參數(shù)或 Attach 機(jī)制加載 Agent。
下面以在方法進(jìn)入和退出時打印日志為例,完整的演示如何手撕一個 Java Agent,開干!
1.編寫 Agent類
首先,我們需要編寫一個包含 premain()方法的 Agent類,示例代碼如下:
import java.lang.instrument.Instrumentation;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class LoggingAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new LoggingTransformer());
}
}
class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("com/example/JavaAgentTest")) {
// 使用 ASM或 Javassist進(jìn)行字節(jié)碼操作
return addLogging(classfileBuffer);
}
return classfileBuffer;
}
private byte[] addLogging(byte[] classfileBuffer) {
// 使用 ASM或 Javassist庫進(jìn)行字節(jié)碼修改
// 這里只是一個簡單的示例,實際操作會復(fù)雜得多
return classfileBuffer;
}
}
2.編寫 MANIFEST.MF文件
接著,我們需要在 MANIFEST.MF 文件中指定 Agent 的入口類,如下信息:
Manifest-Version: 1.0
Premain-Class: LoggingAgent
3.打包成 JAR文件
然后,將 Agent 類和 MANIFEST.MF 文件打包成一個 JAR 文件,指令如下:
jar cmf MANIFEST.MF loggingagent.jar LoggingAgent.class LoggingTransformer.class
4.使用 Agent
最后,通過指定 JVM 參數(shù)來加載 Agent,指令如下:
java -javaagent:loggingagent.jar -jar myapp.jar
或者通過 Attach 機(jī)制在運(yùn)行時加載 Agent,示例代碼如下:
import com.sun.tools.attach.VirtualMachine;
public class AttachAgent {
public static void main(String[] args) throws Exception {
String pid = args[0]; // 目標(biāo) JVM 的進(jìn)程ID
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("path/to/myagent.jar");
vm.detach();
}
}
最后,我們寫一個測試類來驗證上面的 Java Agent:
package com.example;
public class JavaAgentTest {
public void methodTest() {
System.out.println("Hello, World!");
}
public static void main(String[] args) {
JavaAgentTest test = new JavaAgentTest();
test.methodTest();
}
}
四、Java Agent使用場景
Java Agent 在實際應(yīng)用中有很多重要的使用場景,主要包括性能監(jiān)控、調(diào)試、日志增強(qiáng)、安全檢查、AOP等,以下是一些具體的應(yīng)用場景及其詳細(xì)說明。
1.性能監(jiān)控
通過Java Agent,可以在不修改應(yīng)用代碼的情況下,動態(tài)地收集性能指標(biāo),如方法執(zhí)行時間、內(nèi)存使用情況、線程狀態(tài)等。
比如,許多 Java Profiling工具,如 VisualVM、YourKit、JProfiler等,都使用 Java Agent 來收集性能數(shù)據(jù)。這些工具通過 Agent 動態(tài)注入代碼來記錄方法調(diào)用、CPU 使用率、內(nèi)存分配等信息。
2.調(diào)試
Java Agent 可以用于增強(qiáng)調(diào)試功能,在運(yùn)行時收集更多的調(diào)試信息。
在調(diào)試復(fù)雜問題時,可能需要額外的日志信息,通過Java Agent,可以在不修改原始代碼的情況下,動態(tài)地添加日志語句。
3.日志增強(qiáng)
日志是軟件開發(fā)中非常重要的一部分,通過Java Agent可以在不修改代碼的情況下,增強(qiáng)日志功能。
- 全局日志:通過Java Agent,可以在每個方法入口和出口處添加日志記錄,捕獲方法調(diào)用的參數(shù)和返回值,方便問題排查。
- 動態(tài)配置:Java Agent 可以根據(jù)配置文件動態(tài)調(diào)整日志級別和日志內(nèi)容,而不需要重啟應(yīng)用程序。
4.安全檢查
- 方法權(quán)限檢查:在方法調(diào)用前,Java Agent 可以動態(tài)檢查調(diào)用者的權(quán)限,防止未授權(quán)的操作
- 數(shù)據(jù)校驗:在數(shù)據(jù)處理前,Java Agent 可以動態(tài)添加數(shù)據(jù)校驗邏輯,確保輸入數(shù)據(jù)的合法性和完整性。
5.AOP
AOP(面向切面編程) 是一種編程范式,通過Java Agent可以實現(xiàn)動態(tài)AOP,增強(qiáng)代碼的靈活性和可維護(hù)性。
- 事務(wù)管理:通過Java Agent,可以在方法調(diào)用前后動態(tài)添加事務(wù)管理邏輯,確保數(shù)據(jù)的一致性。
- 緩存:在方法調(diào)用前,Java Agent 可以檢查緩存,如果有緩存數(shù)據(jù)則直接返回,避免重復(fù)計算。
6.其他應(yīng)用
- 熱部署:Java Agent 可以實現(xiàn)類的熱替換,支持應(yīng)用程序在不重啟的情況下更新代碼。
- 測試覆蓋率:通過Java Agent,可以動態(tài)收集測試覆蓋率信息,生成覆蓋率報告,幫助開發(fā)者了解測試的完整性。
五、Java Agent框架
通過上文我們可以看到 Java Agent 使用場景比較多,為了簡化和增強(qiáng)Java Agent的使用,許多開源和商業(yè)框架都提供了不同層次的支持和功能,下面介紹幾種比較流行的框架。
1.Javassist
Javassist 是一個高層次的Java字節(jié)碼操作庫,提供了簡單易用的API,允許開發(fā)者通過類似于操作Java源代碼的方式來操作字節(jié)碼。
Javassist 的特點(diǎn):
- 易于使用:提供了高層次的API,簡化了字節(jié)碼操作。
- 靈活:支持動態(tài)生成和修改類。
- 廣泛應(yīng)用:被許多Java框架和工具使用,如Hibernate、JBoss等。
2.AspectJ
AspectJ 是一個功能強(qiáng)大的AOP(面向切面編程)框架,允許開發(fā)者通過定義切面(Aspect)來增強(qiáng)Java代碼。AspectJ可以通過Java Agent來實現(xiàn)動態(tài)AOP。
AspectJ 的特點(diǎn):
- AOP支持:提供了強(qiáng)大的AOP支持,簡化了橫切關(guān)注點(diǎn)的處理。
- 靈活:支持靜態(tài)織入和動態(tài)織入。
- 廣泛應(yīng)用:被許多企業(yè)級應(yīng)用和框架使用,如Spring AOP。
3.Spring Instrument
Spring Instrument 是Spring框架提供的一個工具,用于在運(yùn)行時增強(qiáng)Spring應(yīng)用的功能。它使用Java Agent來實現(xiàn)類加載時的字節(jié)碼操作,常用于Spring AOP和Spring Load-Time Weaving(LTW)。
Spring Instrument 的特點(diǎn):
- 與 Spring集成:無縫集成到 Spring框架中,簡化了 Spring應(yīng)用的增強(qiáng)。
- 支持 LTW:支持運(yùn)行時織入,增強(qiáng) Spring應(yīng)用的動態(tài)功能。
- 易于配置:通過 Spring配置文件或注解進(jìn)行配置。
4.ASM
ASM 是一個低級別的 Java字節(jié)碼操作庫,功能強(qiáng)大但API相對復(fù)雜。它允許開發(fā)者以最細(xì)粒度的方式操作字節(jié)碼。
ASM的特點(diǎn):
- 高效:直接操作字節(jié)碼,性能極高。
- 靈活:支持復(fù)雜的字節(jié)碼修改和生成。
- 廣泛應(yīng)用:被許多其他字節(jié)碼庫和框架所使用,如ByteBuddy、CGLIB等。
5.鏈路追蹤框架
鏈路追蹤(Distributed Tracing)是分布式系統(tǒng)中用于追蹤請求流經(jīng)不同服務(wù)的過程的技術(shù),為了實現(xiàn)這一點(diǎn),許多鏈路追蹤框架利用了 Java Agent 技術(shù)來動態(tài)地注入代碼,從而在不修改應(yīng)用程序代碼的情況下實現(xiàn)對請求的追蹤,這種方法通常被稱為“字節(jié)碼增強(qiáng)”或“字節(jié)碼注入”。
常見的鏈路追蹤框架有:Apache SkyWalking,Elastic APM,Pinpoint,Zipkin,Jaeger 等,它們內(nèi)部通過 Java Agent 技術(shù)實現(xiàn)了對應(yīng)用程序的無侵入式監(jiān)控。
總結(jié)
Java Agent是一種強(qiáng)大的工具,可以在運(yùn)行時對字節(jié)碼進(jìn)行動態(tài)修改,從而實現(xiàn)各種監(jiān)控、調(diào)試和增強(qiáng)等功能,其核心原理包括:
- Instrumentation 接口
- Premain() 和 Agentmain()方法
通過 Instrumentation API,我們可以在不修改原始源代碼的情況下對字節(jié)碼進(jìn)行操作,這為開發(fā)者提供了極大的靈活性,在很多優(yōu)秀的框架中都有使用 Java Agent,因此,作為 Java程序員,建議掌握這個知識點(diǎn)。