自己動手實現(xiàn)Agent統(tǒng)計API接口調(diào)用耗時
環(huán)境:Java17
本篇文章介紹java agent技術(shù),然后通過一個示例講解如何使用agent,該示例的功能是通過agent技術(shù)實現(xiàn)api接口調(diào)用耗時情況。
1. 簡介
什么是agent?Java Agent也稱為Java探針,它 是一個獨立的 JAR 包,是在JDK1.5中引入的一種技術(shù),允許動態(tài)修改Java字節(jié)碼。這種技術(shù)使得Java應(yīng)用程序的Instrumentation API能夠與虛擬機進行交互。Java類在編譯之后形成字節(jié)碼,這些字節(jié)碼隨后被JVM執(zhí)行。在JVM執(zhí)行這些字節(jié)碼之前,Java Agent可以獲取這些字節(jié)碼信息,并且通過字節(jié)碼轉(zhuǎn)換器對這些字節(jié)碼進行修改,以實現(xiàn)一些額外的功能。
核心API
Instrumentation
public interface Instrumentation {
/**
* 注冊提供的轉(zhuǎn)換器。以后的所有類定義都可以通過轉(zhuǎn)換器看到,但所有已注冊的轉(zhuǎn)換器所依賴的類定義除外。
* 如果注冊了多個轉(zhuǎn)換器,那么會按添加的順序調(diào)用它們。
* 如果轉(zhuǎn)換器在執(zhí)行期間拋出異常,則 JVM 仍將按順序調(diào)用其他已注冊的轉(zhuǎn)換器。
* 可以多次添加同一轉(zhuǎn)換器。在任何外部 JVMTI ClassFileLoadHookAll 事件監(jiān)聽器看到類文件之前,用 addTransformer 注冊的所有轉(zhuǎn)換器始終可以看到類文件。
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 注銷提供的轉(zhuǎn)換器。以后的類定義將不顯示給該轉(zhuǎn)換器。
* 移除最近添加的轉(zhuǎn)換器的匹配實例。由于類加載的多線程特性,在調(diào)用被移除后,轉(zhuǎn)換器還可能接收調(diào)用。
* 所以編寫的轉(zhuǎn)換器應(yīng)防止出現(xiàn)這種情況。
*/
boolean removeTransformer(ClassFileTransformer transformer);
/**
* 返回當前 JVM 配置是否支持類的重定義。
* 重定義已加載類的能力是 JVM 的一個可選功能。
* 在執(zhí)行單個 JVM 的單實例化過程中,對此方法的多個調(diào)用將始終返回同一應(yīng)答。
*/
boolean isRedefineClassesSupported();
/**
* 使用提供的類文件重新定義提供的類集。
* 此方法用于在不引用現(xiàn)有類文件字節(jié)的情況下替換類的定義,就像從源代碼重新編譯以修復(fù)并繼續(xù)調(diào)試時可能會做的那樣。
* 如果要轉(zhuǎn)換現(xiàn)有的類文件字節(jié)(例如字節(jié)碼插入),則應(yīng)使用retransformClassess。
* 此方法對一個集合進行操作,以便允許同時對多個類進行相互依存的更改(對類a的重新定義可能需要對類B的重新定義)。
*/
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
/**
* 返回JVM當前加載的所有類的數(shù)組。
* 返回的數(shù)組包括所有類和接口,包括隱藏類或接口,以及所有類型的數(shù)組類。
*/
Class[] getAllLoadedClasses();
/**
* 返回指定對象所消耗的存儲量的特定于實現(xiàn)的近似值。
* 該結(jié)果可以包括對象的一些或全部開銷,因此對于實現(xiàn)內(nèi)部的比較是有用的,但對于實現(xiàn)之間的比較則不有用。
* 在JVM的一次調(diào)用過程中,估計值可能會發(fā)生變化。
*/
long getObjectSize(Object objectToSize);
}
ClassFileTransformer/**
* 類文件的轉(zhuǎn)換器。代理使用addTransformer方法注冊該接口的實現(xiàn),以便在加載、重新定義或重新轉(zhuǎn)換類時調(diào)用轉(zhuǎn)換器的轉(zhuǎn)換方法。
*/
public interface ClassFileTransformer {
/**
* 轉(zhuǎn)換給定的類文件并返回新的替換類文件
* loader: 要轉(zhuǎn)換的類的定義加載程序,如果引導(dǎo)加載程序,則可以為null。
* className: 類的名稱,內(nèi)部形式為完全限定的類和接口名稱,如Java虛擬機規(guī)范中定義的那樣。例如,“java/util/List”。
* classBeingRedefined: 如果這是由重新定義或重傳觸發(fā)的,則類被重新定義或重新轉(zhuǎn)換;如果這是類裝入,則為null。
* protectionDomain: 正在定義或重新定義的類的保護域。
* classfileBuffer: 類文件格式的輸入字節(jié)緩沖區(qū)-不能修改
*/
default byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
return null;
}
}
編寫規(guī)范
要編寫一個agent 入口程序有2個核心的方法(二選其一)
Java 虛擬機 (JVM) 初始化后,premain 方法將被調(diào)用,然后才是真正的應(yīng)用程序 main 方法。premain 方法必須返回才能繼續(xù)啟動。
JVM 首先嘗試在代理類上調(diào)用以下方法:
public static void premain(String agentArgs, Instrumentation instrumentation)
如果代理類未實現(xiàn)此方法,則 JVM 將嘗試調(diào)用:
public static void premain(String agentArgs)
有個上面定義的類之后還需要一個MANIFEST.MF文件
Manifest-Version: 1.0
Premain-Class: com.pack.agent.MonitorAgent
Can-Redefine-Classes: true
Premain-Class:指定了當在 JVM 啟動時指定代理時,此屬性指定代理類。即,包含 premain 方法的類。當在 JVM 啟動時指定代理程序時,此屬性是必需的。如果該屬性不存在,JVM 將中止。
Can-Redefine-Classes:是否能夠重新定義此代理所需的類。
有了上面的基礎(chǔ)知識后接下來我們就通過一個實例來更加清晰的認識Agent。
2. 實戰(zhàn)案例
添加依賴
<!--將通過javassit來修改類信息-->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
編寫Transformer類用來轉(zhuǎn)換修改要加載的類
public class MonitorTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 在上面的API說明中已經(jīng)說了,這里的className不是. 而是'/'
className = className.replace("/", ".");
// 這里只攔截com.pack及子包下的類
if (className.startsWith("com.pack")) {
try {
// 以下的相關(guān)API就是javassit;可自行查看javassit相關(guān)的文章
CtClass ctClass = ClassPool.getDefault().get(className) ;
CtMethod[] ctMethods = ctClass.getDeclaredMethods() ;
for (CtMethod ctMethod : ctMethods) {
// 獲取執(zhí)行的方法名稱
String methodName = ctMethod.getName() ;
// 打印方法執(zhí)行耗時時間
String executeTime = "\nSystem.out.println(\"" + methodName + " 耗時:\" + (end - start) + " + "\" ms\");\n" ;
// 添加2個局部變量
ctMethod.addLocalVariable("start", CtClass.longType) ;
ctMethod.addLocalVariable("end", CtClass.longType) ;
// 為上面2個局部變量賦值
ctMethod.insertBefore("start = System.currentTimeMillis() ;\n") ;
ctMethod.insertAfter("end = System.currentTimeMillis();\n") ;
// 將打印時間的語句插入到方法體的最后一行
ctMethod.insertAfter(executeTime) ;
}
// 返回修改后的字節(jié)碼(這里就是重寫字節(jié)碼文件)
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace() ;
}
}
return null;
}
}
編寫Agent入口
public class MonitorAgent {
// 這里的premain是我們Agent的入口,首先執(zhí)行的就是該premain,然后才是main
// agentArgs是agent運行時添加的參數(shù),我們可以在下面看到如何定義參數(shù)
public static void premain(String agentArgs, Instrumentation instrumentation) {
// 添加轉(zhuǎn)換器
instrumentation.addTransformer(new MonitorTransformer());
}
// 這里完全沒必要main,只是為了在eclipse中生成jar包方便
public static void main(String[] args) {
}
}
編寫MANIFEST.MF文件
Manifest-Version: 1.0
Premain-Class: com.pack.agent.MonitorAgent
Can-Redefine-Classes: true
以上步驟完成后,我們就可以打包了。我是通過Eclipse直接導(dǎo)出的jar,這種方式導(dǎo)出的jar會自動生成MANIFEST.MF文件,所以最后通過壓縮軟件將上面的MANIFEST.MF文件手動添加進去。最后看下生成的jar結(jié)構(gòu)
圖片
編寫SpringBoot程序
這里隨便寫一個API接口即可。
@RestController
@RequestMapping("/demos")
public class DemoController {
@GetMapping("/index")
public Object index() throws Exception {
TimeUnit.SECONDS.sleep(new Random().nextInt(5)) ;
return "success" ;
}
}
非常簡單的一個測試接口。我們會通過上面寫的agent來輸出當前接口執(zhí)行時間。
將該測試程序打包成jar,當前目錄。
圖片
MANIFEST.MF不是必須在這里,我這里是為了替換CosAgent.jar中的文件。
接下來是運行,運行需要指定agent jar包。
java -javaagent:CostAgent.jar -jar test.jar
通關(guān)-javaagent:CostAgent.jar指定了agent的jar包,我們可以在后面跟上參數(shù),這樣在premain方法中的第一個參數(shù)就可以接收到參數(shù)信息。
啟動后訪問測試接口/demos/index
index 耗時:0 ms
index 耗時:0 ms
index 耗時:0 ms
index 耗時:1008 ms
index 耗時:2012 ms
我們的接口訪問,成功的輸出了接口調(diào)用耗時時間。
我們可以通過arthas進行查看DemoController接口類
jad com.pack.DemoController
輸出結(jié)果
@GetMapping(value={"/index"})
publicObject index() throws Exception {
long start = System.currentTimeMillis();
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
String string= "success";
long l = System.currentTimeMillis();
String string2 = string;
System.out.println(new StringBuffer().append("index 耗時:").append(l - var1_1).append(" ms").toString());
return string2;
}
線上的類已經(jīng)通過Agent修改了。