前提概要
-
Java調式、熱部署、JVM背後的支持者Java Agent:
-
各個 Java IDE 的除錯功能,例如 eclipse、IntelliJ ;
-
熱部署功能,例如 JRebel、XRebel、spring-loaded;
-
各種線上診斷工具,例如 Btrace、Greys,還有阿里的 Arthas;
-
各種效能分析工具,例如 Visual VM、JConsole 等;
-
Agent的介紹
Java Agent 直譯過來叫做 Java 代理,還有另一種稱呼叫做 Java 探針。首先說 Java Agent 是一個 jar 包,只不過這個 jar 包不能獨立執行,它需要依附到我們的目標 JVM 程式中。我們來理解一下這兩種叫法。
-
代理:比方說我們需要了解目標 JVM 的一些執行指標,我們可以通過 Java Agent 來實現,這樣看來它就是一個代理的效果,我們最後拿到的指標是目標 JVM ,但是我們是通過 Java Agent 來獲取的,對於目標 JVM 來說,它就像是一個代理;
-
探針:這個說法我感覺非常形象,JVM 一旦跑起來,對於外界來說,它就是一個黑盒。而 Java Agent 可以像一支針一樣插到 JVM 內部,探到我們想要的東西,並且可以注入東西進去。
-
拿IDEA偵錯程式來說吧,當開啟除錯功能後,在debugger皮膚中可以看到當前上下文變數的結構和內容,還可以在watches皮膚中執行一些簡單的程式碼,比如取值賦值等操作。
-
還有Btrace、Arthas這些線上排查問題的工具,比方說有介面沒有按預期的返回結果,但日誌又沒有錯誤。這時,我們只要清楚方法的所在包名、類名、方法名等,不用修改部署服務,就能查到呼叫的引數、返回值、異常等資訊。
-
上面只是說到了探測的功能,而熱部署功能那就不僅僅是探測這麼簡單了。熱部署的意思就是說再不重啟服務的情況下,保證最新的程式碼邏輯在服務生效。當我們修改某個類後,通過 Java Agent 的 instrument 機制,把之前的位元組碼替換為新程式碼所對應的位元組碼。
-
Java Agent 結構
Java Agent 最終以 jar 包的形式存在。主要包含兩個部分,一部分是實現程式碼,一部分是配置檔案。配置檔案放在 META-INF 目錄下,檔名為 MANIFEST.MF 。
包括以下配置項:
Manifest-Version: 版本號
Created-By: 創作者
Agent-Class: agentmain方法所在類
Can-Redefine-Classes: 是否可以實現類的重定義
Can-Retransform-Classes: 是否可以實現位元組碼替換
Premain-Class: premain 方法所在類
入口類實現 agentmain 和 premain 兩個方法即可,方法要實現什麼功能就由你的需求決定了。
Java Agent 實現和使用
接下來就來實現一個簡單的 Java Agent,基於 Java 1.8,主要實現兩點簡單的功能:
-
列印當前載入的所有類的名稱;
-
監控一個特定的方法,在方法中動態插入簡單的程式碼並獲取方法返回值;
-
在方法中插入程式碼主要是用到了位元組碼修改技術,位元組碼修改技術主要有 javassist、ASM,已經 ASM 的高階封裝可擴充套件 cglib,這個例子中用的是 javassist。所以需要引入相關的 maven 包。
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
實現入口類和功能邏輯
入口類上面也說了,要實現 agentmain 和 premain 兩個方法。
-
這兩個方法的執行時機不一樣。這要從 Java Agent 的使用方式來說了,Java Agent 有兩種啟動方式,一種是以 JVM 啟動引數 -javaagent:xxx.jar 的形式隨著 JVM 一起啟動,這種情況下,會呼叫 premain方法,並且是在主程式的 main方法之前執行。
-
另外一種是以 loadAgent 方法動態 attach 到目標 JVM 上,這種情況下,會執行 agentmain方法。
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
JVM 會優先載入 帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。Instrumentation是一個重要的引數。
在 Java SE 6 的 Instrumentation 當中,提供了一個新的代理操作方法:agentmain,可以在 main 函式開始執行之後再執行,跟premain函式一樣, 開發者可以編寫一個含有agentmain函式的 Java 類。
- 採用attach機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被載入完成,這個時候需要藉助Instrumentation#retransformClasses(Class<?>... classes)
讓對應的類可以重新轉換,從而啟用重新轉換的類執行ClassFileTransformer列表中的回撥
public static void agentmain (String agentArgs, Instrumentation inst)
public static void agentmain (String agentArgs)
agentMain 主要用於對java程式的監控,呼叫java程式,將自己編寫的agentMain 注入目標完成對程式的監控,修改。
程式碼實現如下:
import java.lang.instrument.Instrumentation;
public class MyCustomAgent {
/**
* jvm 引數形式啟動,執行此方法
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain");
customLogic(inst);
}
/**
* 動態 attach 方式啟動,執行此方法
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agentmain");
customLogic(inst);
}
/**
* 列印所有已載入的類名稱
* 修改位元組碼
* @param inst
*/
private static void customLogic(Instrumentation inst){
inst.addTransformer(new MyTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for(Class cls :classes){
System.out.println(cls.getName());
}
}
}
-
我們看到這兩個方法都有引數agentArgs和inst,其中 agentArgs 是我們啟動 Java Agent 時帶進來的引數,比如-javaagent:xxx.jar agentArgs。
-
Instrumentation Java開放出來的專門用於位元組碼修改和程式監控的實現。我們要實現的列印已載入類和修改位元組碼也就是基於它來實現的。其中 inst.getAllLoadedClasses()一個方法就實現了獲取所以已載入類的功能。
inst.addTransformer方法則是實現位元組碼修改的關鍵,後面的引數就是實現位元組碼修改的實現類,程式碼如下:
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("正在載入類:"+ className);
if (!"kite/attachapi/Person".equals(className)){
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = cl.getDeclaredMethod("test");
System.out.println("獲取方法名稱:"+ ctMethod.getName());
ctMethod.insertBefore("System.out.println(\" 動態插入的列印語句 \");");
ctMethod.insertAfter("System.out.println($_);");
byte[] transformed = cl.toBytecode();
return transformed;
}catch (Exception e){
e.printStackTrace();
}
return classfileBuffer;
}
}
以上程式碼的邏輯就是當碰到載入的類是 kite.attachapi.Person的時候,在其中的 test 方法開始時插入一條列印語句,列印內容是"動態插入的列印語句",在test方法結尾處,列印返回值,其中$_ 就是返回值,這是 javassist 裡特定的標示符。
MANIFEST.MF 配置檔案
在目錄 resources/META-INF/ 下建立檔名為 MANIFEST.MF 的檔案,在其中加入如下的配置內容:
Manifest-Version: 1.0
Created-By: fengzheng
Agent-Class: kite.lab.custom.agent.MyCustomAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: kite.lab.custom.agent.MyCustomAgent
配置打包所需的 pom 設定
最後 Java Agent 是以 jar 包的形式存在,所以最後一步就是將上面的內容打到一個 jar 包裡。
在 pom 檔案中加入以下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
-
用的是 maven 的 maven-assembly-plugin 外掛,注意其中要用 manifestFile 指定 MANIFEST.MF 所在路徑,然後指定 jar-with-dependencies ,將依賴包打進去。
-
上面這是一種打包方式,需要單獨的 MANIFEST.MF 配合,還有一種方式,不需要在專案中單獨的新增 MANIFEST.MF 配置檔案,完全在 pom 檔案中配置上即可。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Premain-Class>kite.agent.vmargsmethod.MyAgent</Premain-Class>
<Agent-Class>kite.agent.vmargsmethod.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
這種方式是將 MANIFEST.MF 的內容全部寫作 pom 配置中,打包的時候就會自動將配置資訊生成 MANIFEST.MF 配置檔案打進包裡。
新增maven外掛指定javaagent類,maven自動完成manifest配置,不用自己再去配置推薦
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動新增META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.rickiyang.learn.PreMainTraceAgent</Premain-Class>
<Agent-Class>com.rickiyang.learn.PreMainTraceAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
MANIFEST.MF引數說明
- Premain-Class :包含 premain 方法的類(類的全路徑名)main方法執行前代理
- Agent-Class :包含 agentmain 方法的類(類的全路徑名)另一種代理main開始後可以修改類結構
- Boot-Class-Path :設定引導類載入器搜尋的路徑列表。查詢類的特定於平臺的機制失敗後,引導類載入器會搜尋這些路徑。按列出的順序搜尋路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑元件語法。如果該路徑以斜槓字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 檔案的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之後某一時刻啟動的,則忽略不表示 JAR 檔案的路徑。(可選)說白就是agent依賴的類
- Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)
- Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)
- Can-Set-Native-Method-Prefix: true表示能設定此代理所需的本機方法字首,預設值為 false(可選)
執行打包命令
接下來就簡單了,執行一條 maven 命令即可。
mvn assembly:assembly
最後打出來的 jar 包預設是以「專案名稱-版本號-jar-with-dependencies.jar」這樣的格式生成到 target 目錄下。
執行打包好的 Java Agent
寫個的測試專案,用來作為目標 JVM,稍後會以兩種方式將 Java Agent 掛到這個測試專案上。
import java.util.Scanner;
public class RunJvm {
public static void main(String[] args){
System.out.println("按數字鍵 1 呼叫測試方法");
while (true) {
Scanner reader = new Scanner(System.in);
int number = reader.nextInt();
if(number==1){
Person person = new Person();
person.test();
}
}
}
}
以上只有一個簡單的 main 方法,用 while 的方式保證執行緒不退出,並且在輸入數字 1 的時候,呼叫 person.test()方法。
以下是 Person 類
public class Person {
public String test(){
System.out.println("執行測試方法");
return "I'm ok";
}
}
以命令列的方式執行
java -javaagent:agent1.jar -javaagent:agent2.jar -jar MyProgram.jar
-javaagent:/java-agent路徑/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar
然後直接執行就可以看到效果了,會看到載入的類名稱。然後輸入數字鍵 "1",會看到位元組碼修改後的內容。
以動態 attach 的方式執行
測試之前先要把這個測試專案跑起來,並把之前的引數去掉。執行後,找到這個它的程式id,一般利用jps -l即可。
動態 attach 的方式是需要程式碼實現的,實現程式碼如下:
public class AttachAgent {
public static void main(String[] args) throws Exception{
VirtualMachine vm = VirtualMachine.attach("pid(程式號)");
vm.loadAgent("java-agent路徑/lab-custom-agent-1.0-SNAPSHOT-jar-with-dependencies.jar");
}
}
執行上面的 main 方法 並在測試程式中輸入“1”,會得到上圖同樣的結果。
發現了沒,我們到這裡實現的簡單的功能是不是和 BTrace 和 Arthas 有點像呢。我們攔截了指定的一個方法,並在這個方法裡插入了程式碼而且拿到了返回結果。如果把方法名稱變成可配置項,並且把返回結果儲存到一個公共位置,例如一個記憶體資料庫,是不是我們就可以像 Arthas 那樣輕鬆的檢測線上問題了呢。當然了,Arthas 要複雜的多,但原理是一樣的。
sun.management.Agent 的實現
不知道你平時有沒有用過 visualVM 或者 JConsole 之類的工具,其實,它們就是用了 management-agent.jar 這個Java Agent 來實現的。如果我們希望 Java 服務允許遠端檢視 JVM 資訊,往往會配置上一下這些引數:
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=192.168.1.1
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
這些引數都是 management-agent.jar 定義的。
我們進到 management-agent.jar 包下,看到只有一個 MANIFEST.MF 配置檔案,配置內容為:
Manifest-Version: 1.0
Created-By: 1.7.0_07 (Oracle Corporation)
Agent-Class: sun.management.Agent
Premain-Class: sun.management.Agent
可以看到入口 class 為 sun.management.Agent,進到這個類裡面可以找到 agentmain 和 premain,並可以看到它們的邏輯。在這個類的開始,能看到我們前面對服務開啟遠端 JVM 監控需要開啟的那些引數定義。