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