Javaagent技術初探
1.引子
近期遇到一個需求場景,微服務場景中無侵入的採集java程式的各種執行資訊(方法呼叫流量資訊、異常棧資訊等),spring系程式設計師的我自然想到了spring aop在方法前、中、拋異常時採集相關資訊。用Spring AOP的方法確實可以採集資訊,但是無法做到無侵入,必須預先寫好程式碼-編譯-釋出後才能正常執行與採集。
而且現有微服務們正在執行,不可能大批量的修改程式碼再進行重啟發布。我們的需求呼之欲出: 無侵入+執行時仍能生效 。好在Java語言強大而飽滿,早在JDK5和JDK6時已經提供瞭解決此問題的技術手段: Javaagent技術 。(相見恨晚啊)。本文初次窺探下Javaagent技術並使用 Javaagent(agentmain方式)、VirtualMachine、Javaassist 實現在某個方法前後進行列印輸出。
2.簡介
demo涉及Javaagent、VirtualMachine、Javaassist技術,簡單總結下用到的內容。(如有理解不到位的,望不吝賜教)
Javaagent :javaagent通常可理解為一個“外掛”,本質是一個精心提供的jar檔案,我們精心的編碼在其中描寫需要進行的操作,這些操作通過java.lang.Instrument包提供的API進行Java應用程式的Instrument(這裡我理解為用Java程式碼裝備,用於裝備的Java程式碼可以進行任何和規矩的操作)
java.lang.instrument :JDK1.5之後提供的用於裝備Java應用程式的工具API,允許JavaAgent程式Instrument(裝備)在JVM上執行的應用程式,通常的做法是提供方法用於在位元組碼中插入要執行的附加程式碼。JDK1.6後提供兩種實現:命令列(-javaagent)形式在應用程式啟動前處理(premain方式);在應用程式啟動後的某個時機處理(agentmain方式)。
Instrumentation :此類提供能夠Instrument(裝備)Java程式碼的服務方法。啟動Agent機制時,Instrumentation物件會被傳遞給premain或者agentmain方法。
ClassFileTransformer :JavaAgent的程式碼中需要提供一個它的實現類,以進行自定義的位元組碼轉換。
VirtualMachine :com.sun.tools.VirtualMachine代表一個已經附加(attach)到別的VM(目標JVM)上的VM。在本文章中我們使用attach(pid)的方法獲得VirtualMachine。此後呼叫loadagent方法將javaagent的jar載入到目標JVM中。此類是實現執行時仍能生效的關鍵。
Javaassist :它是一個處理Java位元組碼類庫。能允許在Java程式執行時定義類,並能在JVM載入類時修改類檔案。重要的是其提供兩種級別的API:原始碼級別和位元組碼級別。使用原始碼級別的API可以向編寫Java原始碼一樣修改類檔案,Javaassist會進行即時編譯。
3.demo編寫
這裡我採用agentmain方式編寫demo,即上文中說明的在應用程式啟動後的某個時機進行程式碼的instrument。demo的整體架構圖如下:

demo架構圖
我們需要準備一個應用程式執行在目標虛擬機器中(demo-spring)、需要一個javaagent.jar作為要instrument的程式碼、最後需要一個程序在demo-spring啟動後將javaagent.jar裝載到目標虛擬機器(JavaDetect)。
3.1 Javaagent
在程式碼啟動後進行agent的織入與instrument,我們需要進行如下步驟
(1) 編寫一個包含一個agentmain方法的類,javaagent.jar包被載入到目標JVM後,JVM會invoke這個agentmain方法傳入Instrumentation物件,Instrumentation提供位元組碼修改的機制。

agentmain方法
instrumentation.addTransformer()第一個引數要求傳入一個ClassFileTransformer的實現類。ClassFileTransformer的transform方法在類被載入、redefine或者指定canRetransform引數為true類被retransform時會被呼叫。第二個引數指定是否可以retransform類。
下面的處理,我要對指定的類進行程式碼的織入,因此呼叫getAllLoadedClasses()獲取所有載入的類,之後進行過濾
(2)接著實現ClassFileTransformer的實現類,在這個類中進行位元組碼的轉換織入程式碼

ClassFileTransformer實現類
在本段程式碼中引入了Javaassist技術,引入其Jar包即可使用。簡單介紹下其重要的類:
CtClass: compile-time class,一個例項可以用來操作一個class檔案
ClassPool:ClassPool是快取CtClass物件的容器,所有的CtClass物件都在ClassPool中。所以,CtClass物件很多時,ClassPool會消耗很大的記憶體,為了避免記憶體的消耗,建立ClassPool物件時可以使用單例模式,或者對於CtClass物件,呼叫detach方法將其從ClassPool中移除。
CtBehavior:代表一個方法、構造器或者一個靜態構造器
因此使用Javaassist進行位元組碼的改變與織入,可以遵循以下的步驟
A.建立ClassPool,ClassPool.getDefault()
B.獲取CtClass,ClassPool.makeClass()
C.獲取要操作的方法CtBehavior,CtClass.getDeclaredBehaviors()
D.進行方法位元組碼修改:CtBehavior.insertBefore()
E.從ClassPool中移除CtClass,防止記憶體溢位:CtClass.detach()
(3)上述兩步驟即可完成Javaagent程式碼的編寫,但是要進行能夠正常執行,需要在META-INF/MANIFREST.MF檔案中配置Agent-Class。採用Maven構建專案可通過Maven打包生成,如下:

maven構建MANIFEST.MF
或者手動新增MANIFEST.MF檔案

MANIFEST.MF
3.2 JavaDetect(AttachAgent)
JavaDetect程序通過PID找到要注入程式碼的程序,將Javaagent.jar織入目標程序的JVM中

AgentAttach程式碼
這裡會用到VirtualMachine,其使用方法如下
A.VirtualMachine.attach()方法,傳入目標程序的PID,得到一個代表目標程序的VM物件
B.VirtualMachine.loadAgent()方法,傳入jar包的路徑,將jar織入到目標程序的VM中
C.VirtualMachine.detach()方法,從目標Vm上移除,之後對於目標VM的操作將無效
上面兩步我們完成了一個javaagent以及一個織入javaagent到目標程序的方法AgentDetach,之後編寫了一個簡單的spring-boot專案用於看到效果。

執行效果圖
4.問題與優化
4.1 優化
本demo中存在著許多可以優化的點,在這裡列出,以後慢慢優化處理
(1) AgentAttach啟動是需要手動指定目標程序的PID,這裡可以考慮方案優化下
(2) 如何做到多次載入AgentAttach,仍然能保持正常的程式碼織入,目前AgentAttach啟動-停止-再啟動,會織入兩次程式碼,造成訪問出現如下結果

異常出現
4.2 問題
(1) java類的redefine和retransform的區別和什麼場景下觸發?
(2) Javaassist與VirtualMachine的詳細學習
5.參考資料
ofollow,noindex">Java.lang.Instrument官方文件
6.附錄