如下文章來源於古時的風箏 ,做者古時的風箏java
咱們平時寫 Java Agent 的機會確實很少,也能夠說幾乎用不着。但其實咱們一直在用它,並且接觸的機會很是多。下面這些技術都使用了 Java Agent 技術,看一下你就知道爲何了。spring
Java Agent 直譯過來叫作 Java 代理,還有另外一種稱呼叫作 Java 探針。首先說 Java Agent 是一個 jar 包,只不過這個 jar 包不能獨立運行,它須要依附到咱們的目標 JVM 進程中。咱們來理解一下這兩種叫法。數據庫
代理:比方說咱們須要瞭解目標 JVM 的一些運行指標,咱們能夠經過 Java Agent 來實現,這樣看來它就是一個代理的效果,咱們最後拿到的指標是目標 JVM ,可是咱們是經過 Java Agent 來獲取的,對於目標 JVM 來講,它就像是一個代理;apache
探針:這個說法我感受很是形象,JVM 一旦跑起來,對於外界來講,它就是一個黑盒。而 Java Agent 能夠像一支針同樣插到 JVM 內部,探到咱們想要的東西,而且能夠注入東西進去。api
拿上面的幾個咱們平時會用到的技術舉例子。拿 IDEA 調試器來講吧,當開啓調試功能後,在 debugger 面板中能夠看到當前上下文變量的結構和內容,還能夠在 watches 面板中運行一些簡單的代碼,好比取值賦值等操做。還有 Btrace、Arthas 這些線上排查問題的工具,比方說有接口沒有按預期的返回結果,但日誌又沒有錯誤,這時,咱們只要清楚方法的所在包名、類名、方法名等,不用修改部署服務,就能查到調用的參數、返回值、異常等信息。eclipse
上面只是說到了探測的功能,而熱部署功能那就不只僅是探測這麼簡單了。熱部署的意思就是說再不重啓服務的狀況下,保證最新的代碼邏輯在服務生效。當咱們修改某個類後,經過 Java Agent 的 instrument 機制,把以前的字節碼替換爲新代碼所對應的字節碼。jvm
1. Java Agent 結構maven
Java Agent 最終以 jar 包的形式存在。主要包含兩個部分,一部分是實現代碼,一部分是配置文件。ide
配置文件放在 META-INF 目錄下,文件名爲 MANIFEST.MF 。包括如下配置項:工具
Manifest-Version: 版本號 Created-By: 創做者 Agent-Class: agentmain 方法所在類 Can-Redefine-Classes: 是否能夠實現類的重定義 Can-Retransform-Classes: 是否能夠實現字節碼替換 Premain-Class: premain 方法所在類
入口類實現 agentmain 和 premain 兩個方法便可,方法要實現什麼功能就由你的需求決定了。
2. 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>
3. 實現入口類和功能邏輯
入口類上面也說了,要實現 agentmain 和 premain 兩個方法。這兩個方法的運行時機不同。這要從 Java Agent 的使用方式來講了,Java Agent 有兩種啓動方式,一種是以 JVM 啓動參數 -javaagent:xxx.jar 的形式隨着 JVM 一塊兒啓動,這種狀況下,會調用 premain方法,而且是在主進程的 main方法以前執行。另一種是以 loadAgent 方法動態 attach 到目標 JVM 上,這種狀況下,會執行 agentmain方法。
代碼實現以下:
package kite.lab.custom.agent; 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 裏特定的標示符。
4. 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
5. 配置打包所需的 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 命令便可。
mvn assembly:assembly
最後打出來的 jar 包默認是以「項目名稱-版本號-jar-with-dependencies.jar」這樣的格式生成到 target 目錄下。
6. 運行打包好的 Java Agent
首先寫一個簡單的測試項目,用來做爲目標 JVM,稍後會以兩種方式將 Java Agent 掛到這個測試項目上。
package kite.attachapi; 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 類
package kite.attachapi; public class Person { public String test(){ System.out.println("執行測試方法"); return "I'm ok"; } }
以命令行的方式運行
由於項目是在 IDEA 裏建立的,爲了省事兒,我就直接在 IDEA 的 「Run/Debug Configurations」里加參數了。
-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 要複雜的多,但原理是同樣的。
7. 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 監控須要開啓的那些參數定義。