前面有兩篇鋪墊博文,在博文《200303-如何優雅的在 java 中統計代碼塊耗時》,其最後提到了根據利用 java agent 來統計方法耗時git
博文《200316-IDEA + maven 零基礎構建 java agent 項目》中則詳細描述了搭建一個 java agent 開發測試項目的全過程github
本篇博文將進入 java agent 的實戰,手把手教你如何是實現一個統計方法耗時的 java agentbootstrap
上面兩節雖然手把手教你實現了一個 hello world 版 agent,然而實際上對 java agent 依然是一臉茫然,因此咱們得先補齊一下基礎知識app
首先來看 agent 的兩個方法中的參數 Instrumentation
,咱們先看一下它的接口定義jvm
/** * 註冊一個Transformer,今後以後的類加載都會被Transformer攔截。 * Transformer能夠直接對類的字節碼byte[]進行修改 */
void addTransformer(ClassFileTransformer transformer);
/** * 對JVM已經加載的類從新觸發類加載。使用的就是上面註冊的Transformer。 * retransformation能夠修改方法體,可是不能變動方法簽名、增長和刪除方法/類的成員屬性 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/** * 獲取一個對象的大小 */
long getObjectSize(Object objectToSize);
/** * 將一個jar加入到bootstrap classloader的 classpath裏 */
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/** * 獲取當前被JVM加載的全部類對象 */
Class[] getAllLoadedClasses();
複製代碼
前面兩個方法比較重要,addTransformer 方法配置以後,後續的類加載都會被 Transformer 攔截。對於已經加載過的類,能夠執行 retransformClasses 來從新觸發這個 Transformer 的攔截。類加載的字節碼被修改後,除非再次被 retransform,不然不會恢復。maven
經過上面的描述,可知ide
Transformer
修改類咱們須要統計方法耗時,因此想到的就是在方法的執行前,記錄一個時間,執行完以後統計一下時間差,即爲耗時學習
直接修改字節碼有點麻煩,所以咱們藉助神器javaassist
來修改字節碼測試
實現自定義的ClassFileTransformer
,代碼以下
public class CostTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
// 這裏咱們限制下,只針對目標包下進行耗時統計
if (!className.startsWith("com/git/hui/java/")) {
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
for (CtMethod method : cl.getDeclaredMethods()) {
// 全部方法,統計耗時;請注意,須要經過`addLocalVariable`來聲明局部變量
method.addLocalVariable("start", CtClass.longType);
method.insertBefore("start = System.currentTimeMillis();");
String methodName = method.getLongName();
method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +
".currentTimeMillis() - start));");
}
byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
複製代碼
而後稍微改一下 agent
/** * Created by @author yihui in 16:39 20/3/15. */
public class SimpleAgent {
/** * jvm 參數形式啓動,運行此方法 * * manifest須要配置屬性Premain-Class * * @param agentArgs * @param inst */
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premain");
customLogic(inst);
}
/** * 動態 attach 方式啓動,運行此方法 * * manifest須要配置屬性Agent-Class * * @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 CostTransformer(), true);
}
}
複製代碼
到此 agent 完畢,打包和上面的過程同樣,接下來進入測試環節
建立一個 DemoClz, 裏面兩個方法
public class DemoClz {
public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}
public int count(int i) {
System.out.println("cnt: " + i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 1;
}
}
複製代碼
而後對應的 main 方法以下
public class BaseMain {
public static void main(String[] args) throws InterruptedException {
DemoClz demoClz = new DemoClz();
int cnt = 0;
for (int i = 0; i < 20; i++) {
if (++cnt % 2 == 0) {
i = demoClz.print(i);
} else {
i = demoClz.count(i);
}
}
}
}
複製代碼
選擇 jvm 參數指定 agent 方式運行(具體操做和上面同樣),輸出以下
雖然咱們的應用程序中並無方法的耗時統計,可是最終的輸出卻完美的打印了每一個方法的調用耗時,實現了無侵入的耗時統計功能
到這裏本文的 java agent 的掃盲 + 實戰(開發一個方法耗時統計)都已經完成了,是否就宣告着能夠小結了,並非,下面介紹一下在實現上面的 demo 過程當中遇到的一個問題
在演示方法耗時的 agent 的示例中,並無藉助最開始的測試用例,而是新建了一個DemoClz
來作的,那麼爲何這樣選擇呢,若是直接用第二節的測試用例會怎樣呢?
public class BaseMain {
public int print(int i) {
System.out.println("i: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i + 2;
}
public void run() {
int i = 1;
while (true) {
i = print(i);
}
}
public static void main(String[] args) {
BaseMain main = new BaseMain();
main.run();
}
複製代碼
依然經過 jvm 參數指定 agent 的方式,運行上面的代碼,會發現拋異常,沒法正常運行了
指出了在 run 方法這裏,存在字節碼的錯誤,咱們統計耗時的 Agent,主要就是在方法開始前和結束後各自新增了一行代碼,咱們直接補充在 run 方法中,則至關於下面的代碼
上面的提示很明顯的告訴了,最後一行語句永遠不可能達到,編譯就存在異常了;那麼問題來了,做爲一個 java agent 的提供者,我哪知道使用者有沒有寫這種死循環的方法,若是應用中有這麼個死循環的任務存在,把個人 agent 一掛載上去,致使應用都起不來,這個鍋算誰的????
下面提供解決方案,也很簡單,在 jvm 參數中,添加一個-noverify
(請注意不一樣的 jdk 版本,參數可能不同,個人本地是 jdk8,用這個參數;若是是 jdk7 能夠試一下-XX:-UseSplitVerifier
)
在 IDEA 開發環境下,以下配置便可
再次運行,正常了
本篇爲實戰項目,首先明確方法參數Instrumentation
它的接口定義,經過它來實現 java 字節碼的修改
咱們經過實現自定義的ClassFileTransformer
,藉助 javassist 來修改字節碼,爲每一個方法的第一行和最後一行注入耗時統計的代碼,從而實現方法耗時統計
最後留一個小問題,上面的實現中,當方法內部拋出異常時,咱們注入的最後一行統計耗時會不會如期輸出,若是不會,應該怎麼修改,歡迎各位大佬留言指出解決方案
(具體解決方案能夠在源碼中獲取哦,還有配套的測試 case,求支持,求贊,求關注 ❀)
相關博文
相關源碼
一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛
盡信書則不如,已上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
一灰灰 blog