前面有兩篇鋪墊博文,在博文《200303-如何優雅的在 java 中統計代碼塊耗時》,其最後提到了根據利用 java agent 來統計方法耗時git
博文《200316-IDEA + maven 零基礎構建 java agent 項目》中則詳細描述了搭建一個 java agent 開發測試項目的全過程github
本篇博文將進入 java agent 的實戰,手把手教你如何是實現一個統計方法耗時的 java agentbootstrap
<!-- more -->app
上面兩節雖然手把手教你實現了一個 hello world 版 agent,然而實際上對 java agent 依然是一臉茫然,因此咱們得先補齊一下基礎知識jvm
首先來看 agent 的兩個方法中的參數 Instrumentation
,咱們先看一下它的接口定義maven
/** * 註冊一個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,不然不會恢復。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