戰(zhàn)損版JavaAgent方法耗時統(tǒng)計工具實現(xiàn)

新來的實習(xí)生妹妹故意刁難我,
說想讓我實現(xiàn)一個方法耗時統(tǒng)計工具,
不能用切面,
這能難倒我嘛,Java Agent安排上。
前言
本篇文章將實現(xiàn)一個超絕戰(zhàn)損版的基于Java Agent的方法耗時統(tǒng)計工具。
整體內(nèi)容分為:
Java Agent原理簡析;
方法耗時統(tǒng)計工具實現(xiàn);
方法耗時工具的Springboot的starter包實現(xiàn)。
正文
一. Java Agent原理簡析
理解啥是Java Agent前,需要先介紹一下JVM TI(JVM Tool Interface)。
JVM TI是JVM提供的用于訪問JVM各種狀態(tài)的一套編程接口?;?strong>JVM TI可以注冊各種JVM事件鉤子函數(shù),當(dāng)JVM事件發(fā)生時,觸發(fā)鉤子函數(shù)以對相應(yīng)的JVM事件進行處理。關(guān)于JVM TI的詳細文檔,可以參考JVMTM Tool Interface。
那么Java Agent可以理解為就是JVM TI的一種具體實現(xiàn)。關(guān)于Java Agent,可以概括其特性如下。
是一個jar包;
無法獨立運行;
(JDK1.5)可以在程序運行前被加載,加載后會調(diào)用到Java Agent提供的入口函數(shù)premain(String agentArgs, Instrumentation inst);
(JDK1.6開始)可以在程序運行中被加載,加載后會調(diào)用到Java Agent提供的入口函數(shù)agentmain(String agentArgs, Instrumentation inst)。
如果想要agentmain()?方法被調(diào)用,則需要將Agent程序attach到主進程的JVM上,這時就需要使用到com.sun.tools.attach包里提供的Attach API,Agent被attach到JVM后,agent的agentmain()?方法就會被調(diào)用。
最后說明一下Java Agent的入口函數(shù)中的類型為Instrumentation的參數(shù)。Instrument是JVM提供的一套能夠?qū)?strong>Java代碼進行插樁操作的服務(wù)能力,JDK1.5的Instrument支持在JVM啟動并加載類時修改類,Instrument從JDK1.6開始支持在程序運行時修改類。Instrument提供的重要方法如下所示。
也就是可以向Instrumentation注冊ClassFileTransformer。
JDK1.5時只能通過addTransformer(ClassFileTransformer)?方法注冊ClassFileTransformer,此時每個類被加載到JVM中之前會調(diào)用到注冊的ClassFileTransformer的transform()?方法,并可以在其中先改變類定義后再將類加載到JVM中。
JDK1.6開始提供了addTransformer(ClassFileTransformer, boolean)?方法,當(dāng)?shù)诙€參數(shù)傳入false時,效果與addTransformer(ClassFileTransformer)?方法一樣,當(dāng)?shù)诙€參數(shù)傳入true時,那么此時注冊的ClassFileTransformer除了在類被加載到JVM中之前會調(diào)用到,還會在retransformClasses(Class<?>... classes) 方法調(diào)用時被調(diào)用到,也就是此時注冊的ClassFileTransformer支持對通過retransformClasses(Class<?>... classes) 方法傳入的類進行重定義然后再重加載到JVM中。
二. 整體構(gòu)思
首先,因為是超絕戰(zhàn)損版,所以我們的方法耗時統(tǒng)計,偽代碼可以表示如下。
其次,我們需要編寫一個Java Agent,且希望能夠在程序運行時加載這個Java Agent,所以編寫的Java Agent需要提供入口函數(shù)agentmain(String agentArgs, Instrumentation inst),此時Java Agent需要通過com.sun.tools.attach包里提供的Attach API來加載并附加到主進程JVM上。
然后,在Java Agent中,我們需要初始化ClassFileTransformer,然后將ClassFileTransformer注冊到Instrumentation,再然后獲取到需要重定義的類并通過Instrumentation的retransformClasses(Class<?>... classes) 方法將這些類傳遞到注冊的ClassFileTransformer中。
接著,在我們自定義的ClassFileTransformer中,需要借助Javassist的能力,為相應(yīng)的類添加方法耗時統(tǒng)計的代碼片段,并完成重加載。
最后,還需要編寫一個測試程序來驗證我們的超絕戰(zhàn)損版方法耗時打印工具的功能。
整體的一個流程示意圖如下。

三. 方法耗時統(tǒng)計工具實現(xiàn)
現(xiàn)在開始代碼實現(xiàn)。首先創(chuàng)建一個Maven工程,命名為myagent-core,POM文件如下所示。
POM文件中主要就是引入必須的javassist的依賴,以及通過打包插件將依賴打入jar包。
然后需要創(chuàng)建
src/main/resources/META-INF目錄,然后在其中創(chuàng)建MANIFEST.MF文件,內(nèi)容如下所示。
Agent-Class: com.lee.learn.agent.core.MethodAgentCan-Redefine-Classes: trueCan-Retransform-Classes: true
特別注意最后有一個空行。
現(xiàn)在開始編寫Java Agent的代碼。首先是自定義一個轉(zhuǎn)換器,命名為MTransformer,并實現(xiàn)ClassFileTransformer接口,代碼現(xiàn)如下。
在MTransformer的構(gòu)造函數(shù)中,需要傳入目標(biāo)類的類對象的集合,目的就是做到動態(tài)的控制對哪些類添加方法耗時統(tǒng)計的邏輯。
最后定義Java Agent的主體類,命名為MethodAgent,代碼如下所示。
至此Java Agent就編寫完畢,整個的工程目錄如下所示。

可以先將Java Agent進行打包,并將得到的jar包放在磁盤的某個路徑下,這里就放在D盤的根路徑下(D:\
myagent-core-jar-with-dependencies.jar)。
下面編寫測試工程。首先創(chuàng)建Maven工程,命名為myagent-local-test,POM文件如下所示。
主要就是引入sun的工具包。
然后創(chuàng)建兩個測試目標(biāo)類,InnerTask位于
com.lee.learn.agent.test.inner包路徑下,OuterTask位于
com.lee.learn.agent.test.outter包路徑下,實現(xiàn)如下所示。
然后就是主測試方法,如下所示。
因為事先已經(jīng)將Java Agent的jar包放在了D盤根路徑下,所以在測試程序中attach到主進程中后,直接通過jar包加載Java Agent。
測試工程目錄結(jié)構(gòu)如下所示。

運行測試程序,打印如下所示。

四. 方法耗時統(tǒng)計工具的Springboot的starter包實現(xiàn)
第三節(jié)中的方法耗時統(tǒng)計工具,功能是實現(xiàn)了,并且主要就是依靠一個Java Agent的jar包,但是實在是太簡陋了,作為超絕戰(zhàn)損版也完全不能看,缺點如下。
Java Agent的jar包需要通過某種手段才能讓應(yīng)用程序找得到。例如容器中的一個應(yīng)用,要使用這個Java Agent,首先要做的事情就是下載jar包,然后拷貝到容器中的某個路徑下;
對用戶代碼產(chǎn)生了侵入。在測試程序中,編寫了代碼并調(diào)用了com.sun.tools.attach包的VirtualMachine的相關(guān)API才實現(xiàn)了attach主進程以及加載Java Agent,這在實際使用中,大家肯定都是不愿意做這個事情的。
鑒于第三節(jié)中的做法實在是不優(yōu)雅,所以本節(jié)會編寫一個方法耗時統(tǒng)計的starter包,只需要在Springboot工程中引用這個包,然后做少量配置,就能夠?qū)崿F(xiàn)和第三節(jié)一樣的方法耗時統(tǒng)計效果。
整體會創(chuàng)建三個工程,如下所示。
myagent-package工程。該工程僅需要做一件事情,就是存放Java Agent的jar包;
myagent-starter工程。starter包,主要完成的事情就是完成Java Agent的加載;
myagent-starter-test工程。測試工程。
主要的做法和部分代碼,參考了Arthas的Springboot的starter包
arthas-spring-boot-starter的實現(xiàn)。
1. myagent-package工程
創(chuàng)建一個Maven工程,命名為myagent-package,然后將Java Agent的jar包打成zip包并放在myagent-package工程的src/main/resources目錄下,如下所示。

最后將myagent-package通過install安裝到本地倉庫。
2. myagent-starter工程
創(chuàng)建一個Maven工程,POM文件如下所示。
上述POM文件中引入的zt-zip是一個zip包工具,然后最關(guān)鍵的就是需要引入myagent-package的依賴,Java Agent的jar包的壓縮包就在這個依賴包中。
myagent-starter的核心思路就是基于Springboot的SPI機制注冊一個ApplicationListener監(jiān)聽器,監(jiān)聽的事件是
ApplicationEnvironmentPreparedEvent,也就是在外部配置加載完畢后就開始加載Java Agent。
現(xiàn)在先創(chuàng)建
src/main/resources/META-INF目錄,然后創(chuàng)建spring.factories文件,內(nèi)容如下所示。
自定義的事件監(jiān)聽器
MyAgentApplicationListener實現(xiàn)如下所示。
監(jiān)聽到
ApplicationEnvironmentPreparedEvent事件后,就會創(chuàng)建一個MyAgentLoader加載器并調(diào)用其load()?方法。Java Agent的加載器MyAgentLoader實現(xiàn)如下。
MyAgentLoader#load方法的主要思路如下。
創(chuàng)建用于存放Java Agent的jar包的臨時目錄;
從classpath下找到Java Agent的zip包;
將Java Agent的zip包解壓到剛創(chuàng)建出來的臨時目錄中;
拿到主進程Id;
從Environment中拿到配置的目標(biāo)包路徑;
基于VirtualMachine附加到主進程上;
加載Java Agent,并傳入目標(biāo)包路徑。
至此starter包就編寫完畢。myagent-starter工程的目錄結(jié)構(gòu)如下所示。

最后還需要將myagent-starter通過install安裝到本地倉庫。
3. myagent-starter-test工程
現(xiàn)在開始編寫測試工程并完成測試,測試工程的目錄結(jié)構(gòu)如下所示。

是一個簡單的三層架構(gòu),首先POM文件如下所示。
然后MyController,MyService和MyDao的實現(xiàn)如下。
也就是模擬每個方法會耗時2秒。最后編寫配置文件,如下所示。
配置僅對controller和dao包下的類進行方法耗時統(tǒng)計。
最后啟動Springboot程序,并調(diào)用MyController接口,打印如下。

方法耗時統(tǒng)計確實只針對controller和dao包生效了,至此測試完畢。
總結(jié)
Java Agent就是一個無法獨立運行的jar包,其加載時機可以是程序運行前和程序運行中,也就是基于Java Agent可以實現(xiàn)在程序運行前和程序運行中來動態(tài)的修改類。
方法耗時統(tǒng)計,簡單的思路就是使用切面去切,首先想到的就是使用Spring的AOP來切,但是Spring的AOP都知道是基于動態(tài)代理,但是無論是JDK動態(tài)代理,還是CGLIB動態(tài)代理,都有其局限性(貌似AspectJ可行,但這不是本文的重點),不是所有類都能切,所以本文采取的思路就是基于Java Agent再結(jié)合Javassist的能力,完成向目標(biāo)類的方法插入方法耗時統(tǒng)計的邏輯。
一個Java Agent的jar包,是一個很精致的jar包,但是有些時候想要這個jar包被加載,還真有點頭疼,主要是放哪里怎么解決,所以提供一個Springboot的starter包貌似是一個很好的解決思路,只需要在程序中引入提供的starter包,那么我們的程序最終無論是虛機部署,還是容器部署,我們都能拿到Java Agent并加載。
本文的方法耗時統(tǒng)計,之所以稱為戰(zhàn)損版,是因為僅僅做了耗時的一個打印,但是真正有用的是啥,那就是能夠通過鏈路Id將方法調(diào)用鏈路以及耗時串起來,但是這也不是本文的重點。