小王是一個剛來不久的妹子,啊呸,是一個剛來不久的程序媛,經(jīng)常垂頭喪氣的~讓我很是不解,終于有一天我怕小王哪天想不開離職了豈不是會增加我的工作量(部門為數(shù)不多的妹子 - 1)?于是乎,我主動找小王進行了談心找到了問題所在,原來是小王編程經(jīng)驗不足,不知道如何巧妙的進行日志打印,那么因果關系就總結(jié)出來了:經(jīng)驗不足導致編碼經(jīng)常出錯,編碼出錯由于日志未打印導致排查困難,排查困難導致開發(fā)抑郁。查到問題的原因,那么進行對癥下藥即可~
其實以上問題我相信很多小伙伴都遇到過,開發(fā)過程中未出現(xiàn)的錯誤在上線后就頻頻出現(xiàn),那么只能不斷的進行添加日志打印然后再打包上傳進行問題跟蹤,一天的時間絕大部分都浪費在了打包上傳的上面。那么能不能直接進行bug跟蹤,然后查看到問題出錯的所在?這種需求不亞于給奔跑中的汽車更換輪胎,匪夷所思卻又無可奈何~其實有開發(fā)經(jīng)驗的小伙伴已經(jīng)想出來一個中間件,那就是 Arthas!但是這篇文章不是介紹如何使用 Archas,而是我們自己能不能實現(xiàn)這種動態(tài)調(diào)試的技能?那么就進入我們今天的整體 --- JAVA Agent 技術
Java Instrument
這個玩意并不是什么 Java 的新特性,早在 JDK 1.5 的時候就誕生了,位于
java.lang.instrument.Instrumentation 中,它的作用就是用來在運行的時候重新加載某個類的 calss 文件的 api。
這種類的實現(xiàn)方式其實是一種 Java Agent 技術,我們這里可以順帶了解一下什么是 Java Agent。
一、Java Agent
代理這個詞對于我們開發(fā)人員來說并不默認,我們經(jīng)常用到的 AOP 面向切面編程用到的就是代理方式。它可以動態(tài)切入某個面,進行代碼增強 。這種不用重復補充輪子的方式大大增加了我們開發(fā)效率,那么這里捕獲到了一個關鍵詞 動態(tài)。那么 Java Agent 如何實現(xiàn)?那就可以說到 JVMTI(JVM Tool Interface) ,這是Java 虛擬機對外提供的 Native 編程接口,通過它我們可以獲取運行時JVM的諸多信息,而 Agent 是一個運行在目標 JVM 的特定程序,它可以從目標 JVM 獲取數(shù)據(jù),然后將數(shù)據(jù)傳遞給外部進程,然后外部進程可以根據(jù)獲取到的數(shù)據(jù)進行動態(tài)Enhance。
那么 Java Agent 什么時候能夠加載?
- 目標 JVM 啟動時
- 目標 JVM 運行時
那么我們關注的是 運行時 ,這樣子就能滿足我們動態(tài)加載的需求。
而 Java Agent看上去這么高大上,我們要如何編寫?當然在 JDK 1.5 之前,實現(xiàn)起來是具有困難性的,我們需要編寫 Native 代碼來實現(xiàn),那么 JDK 1.5 之后我們就可以利用上面說到的 Java Instrument 來實現(xiàn)了!
首先我們先了解一下 Instrumentation 這個接口,其中有幾個方法:
- addTransformer(ClassFileTransformer transformer, boolean canRetransform)
加入一個轉(zhuǎn)換器 Transformer ,之后所有的目標類加載都會被 Transformer 攔截,可自定義實現(xiàn) ClassFileTransformer 接口,重寫該接口的唯一方法 transform() 方法,返回值是轉(zhuǎn)換后的類字節(jié)碼文件
- retransformClasses(Class<?>... classes)
對 JVM 已經(jīng)加載的類重新觸發(fā)類加載,使用上面自定義的轉(zhuǎn)換器進行處理。該方法可以修改方法體,常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
- redefineClasses(ClassDefinition... definitions)
此方法用于替換類的定義,而不引用現(xiàn)有類文件字節(jié)。
- getObjectSize(Object objectToSize)
獲取一個對象的大小
- AppendToBootstrapClassLoaderSearch(JarFile jarfile)
將一個 jar 文件添加到 bootstrap classload 的 classPath 中
- getAllLoadedClasses()
獲取當前被 JVM 加載的所有類對象
redefineClasses 和 retransformClasses 補充說明
兩者區(qū)別:
redefineClasses 是自己提供字節(jié)碼文件替換掉已存在的 class 文件
retransformClasses 是在已存在的字節(jié)碼文件上修改后再進行替換
替換后生效的時機
如果一個被修改的方法已經(jīng)在棧幀中存在,則棧幀中的方法會繼續(xù)使用舊字節(jié)碼運行,新字節(jié)碼會在新棧幀中運行
注意點
兩個方法都是只能改變類的方法體、常量池和屬性值,但不能新增、刪除、重命名屬性或方法,也不能修改方法的簽名
二、實現(xiàn) Agent
1、編寫方法
上面我們已經(jīng)說到了有兩處地方可以進行 Java Agent 的加載,分別是 目標JVM啟動時加載 和 目標JVM運行時加載,這兩種不同的加載模式使用不同的入口函數(shù):
1、JVM 啟動時加載
入口函數(shù)如下所示:
// 函數(shù)1
public static void premain(String agentArgs, Instrumentation inst);
// 函數(shù)2
public static void premain(String agentArgs);
JVM 首先尋找函數(shù)1,如果沒有發(fā)現(xiàn)函數(shù)1,則會尋找函數(shù)2
2、JVM 運行時加載
入口函數(shù)如下所示:
// 函數(shù)1
public static void agentmain(String agentArgs, Instrumentation inst);
// 函數(shù)2
public static void agentmain(String agentArgs);
與上述一致,JVM 首先尋找函數(shù)1,如果沒有發(fā)現(xiàn)函數(shù)1,則會尋找函數(shù)2
這兩組方法的第一個參數(shù) agentArgs 是隨同 “-javaagent” 一起傳入的程序參數(shù),如果這個字符串代表了多個參數(shù),就需要自己解析這參數(shù),inst 是 Instrumentation 類型的對象,是 JVM 自己傳入的,我們可以那這個參數(shù)進行參數(shù)的增強操作。
2、聲明方法
當定義完這兩組方法后,要使之生效還需要手動聲明,聲明方式有兩種:
1、使用 MANIFEST.MF 文件
我們需要創(chuàng)建
resources/META-INF.MANIFEST.MF 文件,當 jar包打包時將文件一并打包,文件內(nèi)容如下:
Manifest-Version: 1.0
Can-Redefine-Classes: true # true表示能重定義此代理所需的類,默認值為 false(可選)
Can-Retransform-Classes: true # true 表示能重轉(zhuǎn)換此代理所需的類,默認值為 false (可選)
Premain-Class: cbuc.life.agent.MainAgentDemo #premain方法所在類的位置
Agentmain-Class: cbuc.life.agent.MainAgentDemo #agentmain方法所在類的位置
2、如果是maven項目,在pom.xml加入
3、指定 agent
要讓目標JVM認你這個 Agent ,你就要給目標JVM介紹這個 Agent
1、JVM 啟動時加載
我們直接在 JVM 啟動參數(shù)中加入 -javaagent 參數(shù)并指定 jar 文件的位置
# 將該類編譯成 class 文件
javac TargetJvm.java
# 指定agent程序并運行該類
java -javaagent:./java-agent.jar TargetJvm
2、JVM 運行時加載
要實現(xiàn)動態(tài)調(diào)試,我們就不能將目標JVM停機后再重新啟動,這不符合我們的初衷,因此我們可以使用 JDK 的 Attach Api 來實現(xiàn)運行時掛載 Agent。
Attach Api 是 SUN 公司提供的一套擴展 API,用來向目標 JVM 附著(attach)在目標程序上,有了它我們可以很方便地監(jiān)控一個 JVM。Attach Api 對應的代碼位于 com.sun.tools.attach包下,提供的功能也非常簡單:
- 列出當前所有的 JVM 實例描述
- Attach 到其中一個 JVM 上,建立通信管道
- 讓目標JVM加載Agent
該包下有一個類 Virtualmachine,它提供了兩個重要的方法:
- VirtualMachine attach(String var0)
傳遞一個進程號,返回目標 JVM 進程的 vm 對象,該方法是 JVM進程之間指令傳遞的橋梁,底層是通過 socket 進行通信
- void loadAgent(String var1)
該方法允許我們將 agent 對應的 jar 文件地址作為參數(shù)傳遞給目標 JVM,目標 JVM 收到該命令后會加載這個 Agent
有了 Attach Api ,我們就可以創(chuàng)建一個java進程,用它attach到對應的jvm,并加載agent。
以下是簡單的 Attach 代碼實現(xiàn):
注意:在mac上安裝了的jdk是能直接找到 VirtualMachine 類的,但是在windows中安裝的jdk無法找到,如果你遇到這種情況,請手動將你jdk安裝目錄下:lib目錄中的tools.jar添加進當前工程的Libraries中。
上面代碼十分簡易的實現(xiàn)了 Attach 的方式,通過尋找當前系統(tǒng)中所有運行的 JVM 進程,然后通過比對 PID 來篩選出目標JVM,然后讓 Agent 附著在目標 JVM 上。當然這邊已經(jīng)簡易到直接在代碼中指定目標JVM的 PID,這種方式在實際生產(chǎn)中是十分不可取的,我們可以通過動態(tài)參數(shù)的方式傳入 PID~!而 Attach 的執(zhí)行原理也不復雜,簡單流程如下:
三、案例說明
我們上述簡單聊了下 Java Agent 的實現(xiàn)過程,那我們下面也簡單寫個案例來理解一下 Java Agent 的實現(xiàn)過程~
我們上面說到可以使用 Java Instrumentation 來完成動態(tài)類修改的功能,并且在 Instrumentation 接口中我們可以通過 addTransformer() 方法來增加一個類轉(zhuǎn)換器,類轉(zhuǎn)換器由類 ClassFileTransformer 接口實現(xiàn)。該接口中有一個唯一的方法 transform() 用于實現(xiàn)類的轉(zhuǎn)換,也就是我們可以增強類處理的地方!當類被加載的時候就會調(diào)用 transform()方法,實現(xiàn)對類加載的事件進行攔截并返回轉(zhuǎn)換后新的字節(jié)碼,通過 redefineClasses()或retransformClasses()都可以觸發(fā)類的重新加載事件。
實際操作
1)準備目標JVM
我們這里直接使用一個 SpringBoot 項目來試驗,方便大家增強改造~ 項目結(jié)構(gòu)如下:
target-jvm
├─src
├─main
├─java
└─cbuc
└─life
└─targetjvm
├─controller
| └─TestController.java
└─service
| └─SimpleService.java
└─TargetJvmApplication.java
其中 TestController 和 SimpleService 兩個類的內(nèi)容也很簡單,直接貼代碼
2)準備 Agent
1、編寫方法
然后編寫我們的Agent jar包。因為懶惰,所以我這邊將 premain 和 agentmain 兩個方法寫在同一個 jar 包中,然后分別以 啟動時 和 運行時 來模擬場景~
很簡單,一個類中包含了我們需要的所有功能~ 防止圖片內(nèi)容過于擁擠,小菜貼心地分別粘貼出核心代碼:
- premain
- agentmain
- ClassFileTransformer
2)聲明方法
然后將 Agent 打包,打包的時候需要在 pom.xml 文件中添加以下內(nèi)容
然后運行mvn assembly:assembly 既可
3)啟動 Agent
當我們已經(jīng)準備好了兩個 jar 包便可以開始測試了!
1、啟動時加載
nohup java -javaagent:./java-agent-jar-with-dependencies.jar -jar target-jvm.jar &
xxxxxxxxxxbr nohup java -javaagent:./java-agent-jar-with-dependencies.jar -jar target-jvm.jar &
我們直接啟動時添加參數(shù),帶上我們的 Agent jar包
結(jié)果并沒有讓小菜太尷尬,成功的實現(xiàn)我們想要的功能,但是這只是啟動時加載,明顯不是我們想要的~ 我們來試下運行時如何加載
2、運行時加載
正常運行下,方法并沒有做耗時統(tǒng)計,我們的需求就來了,我們想要統(tǒng)計該方法的耗時,首先獲取該進程ID
然后通過 Attach 方式(調(diào)用controller 的 active() 方法)附著 Agent,我們可以實時查看控制臺
已經(jīng)可以看到 Agent 似乎已經(jīng)成功附著了,然后我們繼續(xù)請求 test 接口
可以發(fā)現(xiàn) resolve 方法已經(jīng)被我們增強了!
四、題外話
上面我們已經(jīng)簡單的實現(xiàn)了動態(tài)操作目標類文件,文章開頭就說明了給奔跑中的汽車更換輪胎是一個匪夷所思卻又無可奈何的需求,但是這個需求能不能讓別人實現(xiàn),其實是可以的,而這個就是小菜的主要目的,我們了解了如何實現(xiàn)動態(tài)換輪胎的原理后,當我們運用其成熟的中間件也能更加應手而不會不知所措,知識不能讓我們只學會臥槽兩個字,而是當別人實現(xiàn)的時候我們能默默思考,思考后再說出牛逼~!感興趣的同學不妨拉取一下源碼演練一番:Arthas gitee,已經(jīng)使用過類似 Arthas 或 BTrace 的同學,看完相信會更加了解其工作運行原理,沒使用過的同學下次用到的時候也不會那么戰(zhàn)戰(zhàn)兢兢!