Java?Agent?(代理)探針技術(shù)詳情
前言:
Java 中的 Agent 技術(shù)可以讓我們無侵入性的去進行代理,最常用于程序調(diào)試、熱部署、性能診斷分析等場景,現(xiàn)如今比較火熱的分布式鏈路追蹤項目Skywalking,就是通過探針技術(shù)去捕獲日志,將數(shù)據(jù)上報OAP觀察分析平臺。
Java Agent 技術(shù)簡介
Java Agent 直譯為 Java 代理,也常常被稱為 Java 探針技術(shù)。
Java Agent 是在 JDK1.5 引入的,是一種可以動態(tài)修改 Java 字節(jié)碼的技術(shù)。Java 中的類編譯后形成字節(jié)碼被 JVM 執(zhí)行,在 JVM 在執(zhí)行這些字節(jié)碼之前獲取這些字節(jié)碼的信息,并且通過字節(jié)碼轉(zhuǎn)換器對這些字節(jié)碼進行修改,以此來完成一些額外的功能。
Java Agent 是一個不能獨立運行 jar 包,它通過依附于目標程序的 JVM 進程,進行工作。啟動時只需要在目標程序的啟動參數(shù)中添加-javaagent 參數(shù)添加 ClassFileTransformer 字節(jié)碼轉(zhuǎn)換器,相當于在main方法前加了一個攔截器。
Java Agent 功能介紹
Java Agent 主要有以下功能:
Java Agent能夠在加載 Java 字節(jié)碼之前攔截并對字節(jié)碼進行修改;- Java Agent 能夠在 Jvm 運行期間修改已經(jīng)加載的字節(jié)碼;
Java Agent 的應(yīng)用場景:
- IDE 的調(diào)試功能,例如 Eclipse、IntelliJ IDEA ;
- 熱部署功能,例如 JRebel、XRebel、spring-loaded;
- 各種線上診斷工具,例如 Btrace、Greys,還有阿里的 Arthas;
- 各種性能分析工具,例如 Visual VM、JConsole 等;
- 全鏈路性能檢測工具,例如 Skywalking、Pinpoint等;
Java Agent 實現(xiàn)原理
在了解Java Agent的實現(xiàn)原理之前,需要對Java類加載機制有一個較為清晰的認知。一種是在man方法執(zhí)行之前,通過premain來執(zhí)行,另一種是程序運行中修改,需通過JVM中的Attach實現(xiàn),Attach的實現(xiàn)原理是基于JVMTI。
主要是在類加載之前,進行攔截,對字節(jié)碼修改
下面我們分別介紹一下這些關(guān)鍵術(shù)語:
- JVMTI 就是
JVM Tool Interface,是 JVM 暴露出來給用戶擴展使用的接口集合,JVMTI 是基于事件驅(qū)動的,JVM每執(zhí)行一定的邏輯就會觸發(fā)一些事件的回調(diào)接口,通過這些回調(diào)接口,用戶可以自行擴展
JVMTI是實現(xiàn) Debugger、Profiler、Monitor、Thread Analyser 等工具的統(tǒng)一基礎(chǔ),在主流 Java 虛擬機中都有實現(xiàn)
- JVMTIAgent是一個動態(tài)庫,利用JVMTI暴露出來的一些接口來干一些我們想做、但是正常情況下又做不到的事情,不過為了和普通的動態(tài)庫進行區(qū)分,它一般會實現(xiàn)如下的一個或者多個函數(shù):
Agent_OnLoad函數(shù),如果agent是在啟動時加載的,通過JVM參數(shù)設(shè)置Agent_OnAttach函數(shù),如果agent不是在啟動時加載的,而是我們先attach到目標進程上,然后給對應(yīng)的目標進程發(fā)送load命令來加載,則在加載過程中會調(diào)用Agent_OnAttach函數(shù)Agent_OnUnload函數(shù),在agent卸載時調(diào)用
- javaagent 依賴于instrument的JVMTIAgent(Linux下對應(yīng)的動態(tài)庫是libinstrument.so),還有個別名叫
JPLISAgent(Java Programming Language Instrumentation Services Agent),專門為Java語言編寫的插樁服務(wù)提供支持的 - instrument 實現(xiàn)了Agent_OnLoad和Agent_OnAttach兩方法,也就是說在使用時,agent既可以在啟動時加載,也可以在運行時動態(tài)加載。其中啟動時加載還可以通過類似-javaagent:jar包路徑的方式來間接加載
instrument agent,運行時動態(tài)加載依賴的是JVM的attach機制,通過發(fā)送load命令來加載agent - JVM Attach 是指 JVM 提供的一種進程間通信的功能,能讓一個進程傳命令給另一個進程,并進行一些內(nèi)部的操作,比如進行線程 dump,那么就需要執(zhí)行 jstack 進行,然后把 pid 等參數(shù)傳遞給需要 dump 的線程來執(zhí)行
Java Agent 案例
我們就以打印方法的執(zhí)行時間為例,通過Java Agent 來實現(xiàn)。
首先我們需要構(gòu)建一個精簡的Maven項目,在其中構(gòu)建兩個Maven的子項目,一個用于實現(xiàn)外掛的Agent,一個用于實現(xiàn)測試目標程序。

我們在父應(yīng)用中導(dǎo)入兩個項目公共依賴的包
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
</dependencies>首先我們?nèi)?gòu)建測試的目標程序
// 啟動類
public class APPMain {
public static void main(String[] args) {
System.out.println("APP 啟動?。?!");
AppInit.init();
}
}
// 模擬的應(yīng)用初始化的類
public class AppInit {
public static void init() {
try {
System.out.println("APP初始化中...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}然后我們啟動程序,測試是否能正常執(zhí)行,程序正常執(zhí)行之后,我們開始構(gòu)建探針程序
探針程序中我們需要編寫,改變原有class的Transformer,通過自定義的Transformer類完成輸出方法執(zhí)行時間的功能,

首先構(gòu)檢Agent程序的入口
public class RunTimeAgent {
public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("探針啟動?。?!");
System.out.println("探針傳入?yún)?shù):" + arg);
instrumentation.addTransformer(new RunTimeTransformer());
}
}這里每個類加載的時候都會走這個方法,我們可以通過className進行指定類的攔截,然后借助javassist這個工具,進行對Class的處理,這里的思想和反射類似,但是要比反射功能更加強大,可以動態(tài)修改字節(jié)碼。
javassist是一個開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class RunTimeTransformer implements ClassFileTransformer {
private static final String INJECTED_CLASS = "com.zhj.test.init.AppInit";
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
if (realClassName.equals(INJECTED_CLASS)) {
System.out.println("攔截到的類名:" + realClassName);
CtClass ctClass;
try {
// 使用javassist,獲取字節(jié)碼類
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);
// 得到該類所有的方法實例,也可選擇方法,進行增強
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod method : declaredMethods) {
System.out.println(method.getName() + "方法被攔截");
method.addLocalVariable("time", CtClass.longType);
method.insertBefore("System.out.println(\"---開始執(zhí)行---\");");
method.insertBefore("time = System.currentTimeMillis();");
method.insertAfter("System.out.println(\"---結(jié)束執(zhí)行---\");");
method.insertAfter("System.out.println(\"運行耗時: \" + (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();
} catch (Throwable e) { //這里要用Throwable,不要用Exception
System.out.println(e.getMessage());
e.printStackTrace();
}
}
return classfileBuffer;
}
}我們需要在Maven中配置,編譯打包的插件,這樣我們就可以很輕松的借助Maven生成Agent的jar包
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven編譯的jdk版本。若不指定,maven3默認用jdk 1.5 maven2默認用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自動添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>com.zhj.agent.RunTimeAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>否則我們需要在resources下創(chuàng)建META-INF/MANIFEST.MF文件,文件內(nèi)容如下,我們可以看出這個與Maven中的配置是一致的,然后通過配置編譯器,借助編譯器打包成jar包,需指定該文件
Manifest-Version: 1.0 Premain-Class: com.zhj.agent.RunTimeAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
告示文件MANIFEST.MF參數(shù)說明:
Manifest-Version
文件版本
Premain-Class
包含 premain 方法的類(類的全路徑名)main方法運行前代理
Agent-Class
包含 agentmain 方法的類(類的全路徑名)main開始后可以修改類結(jié)構(gòu)
Boot-Class-Path
設(shè)置引導(dǎo)類加載器搜索的路徑列表。查找類的特定于平臺的機制失敗后,引導(dǎo)類加載器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。(可選)
Can-Redefine-Classes true
表示能重定義此代理所需的類,默認值為 false(可選)
Can-Retransform-Classes true
表示能重轉(zhuǎn)換此代理所需的類,默認值為 false (可選)
Can-Set-Native-Method-Prefix true
表示能設(shè)置此代理所需的本機方法前綴,默認值為 false(可選)
最后通過Maven生成Agent的jar包,然后修改測試目標程序的啟動器,添加JVM參數(shù)即可
參數(shù)示例:-javaagent:F:\code\myCode\agent-test\runtime-agent\target\runtime-agent-1.0-SNAPSHOT.jar=hello

最終效果:

這樣就完成了無侵入的代理。
到此這篇關(guān)于Java Agent (代理)探針技術(shù)詳情的文章就介紹到這了,更多相關(guān)Java Agent 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot多項目結(jié)構(gòu)實現(xiàn)
本文主要介紹了springboot多項目結(jié)構(gòu)實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-01-01
SpringBoot集成圖片驗證碼框架easy-captcha的詳細過程
本文介紹了如何將Easy-Captcha框架集成到SpringBoot項目中,實現(xiàn)圖片驗證碼功能,Easy-Captcha是一款輕量級的開源驗證碼框架,通過簡潔的API和高度可定制性,可以快速實現(xiàn)驗證碼功能,文章詳細介紹了如何配置、生成和展示驗證碼,并提供了Easy-Captcha的GitHub地址2025-03-03
JAVA統(tǒng)計字符串中某個字符出現(xiàn)次數(shù)的方法實現(xiàn)
本文主要介紹了JAVA統(tǒng)計字符串中某個字符出現(xiàn)次數(shù)的方法實現(xiàn),可以循環(huán)使用String的charAt(int index)函數(shù),具有一定的參考價值,感興趣的可以了解一下2023-11-11
多個sheet Excel 數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫的實現(xiàn)方法
這篇文章主要介紹了多個sheet Excel 數(shù)據(jù)導(dǎo)入數(shù)據(jù)庫的實現(xiàn)方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03
Java實現(xiàn)Html轉(zhuǎn)Pdf的方法
這篇文章主要介紹了Java實現(xiàn)Html轉(zhuǎn)Pdf的方法,實例分析了java基于ITextRenderer類操作頁面及系統(tǒng)自帶字體生成pdf文件的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-07-07
詳解java8在Collection中新增加的方法removeIf
這篇文章主要介紹了詳解java8在Collection中新增加的方法removeIf的相關(guān)資料,需要的朋友可以參考下2018-01-01

