java agent: JVM的層面的"AOP"

考慮下這個問題:怎么知道我寫的方法執(zhí)行了多久锅移?

再考慮下:怎么知道所有方法分別執(zhí)行了多久在抛?

初學(xué)者都會的方法

void methodDemo(){
    Date start = new Date();
    
    //...業(yè)務(wù)代碼
    
    Date end = new Date();
    
    Long duration = end.getTime() - start.getTime();
}

上面的方法我的確可以知道methodDemo()的執(zhí)行時間蟹地,但是如果我有一百個這樣的方法怎么辦或悲?寫一百遍么?這時候用過Spring的站出來指著我鼻子說:”你傻啊堪唐,用AOP啊!“

Spring AOP

具體的樣例代碼我就不贅述了巡语,網(wǎng)上一搜一大把教你使用Spring AOP. 但是有一個問題淮菠,考慮下你的代碼是這樣的:

class Test1{
    public void test(){
        testInside(12345);
        System.out.println("the test method executed");
    }
    private  void testInside(int param){
        System.out.println("the testInside method executed,param:"+param);
    }
}

可以注意到,我們在test()方法中調(diào)用了testInside()這個private的方法合陵。有深入了解Spring AOP的實現(xiàn)原理就知道枢赔,Spring在實現(xiàn)AOP的時候是在bean放入容器前生成的代理類。而test()調(diào)用this.testInside()的情況拥知,是沒有對testInside()進行AOP增強的。怎么辦举庶?這個時候,就進入我們本文的重點。

Java agent 在JVM層面實現(xiàn)"AOP增強"

回顧一下峦嗤,類是怎么被加載到JVM里面的?加載->驗證->準備->解析->初始化替梨。在加載階段,一定會有一個步驟是把類的二進制信息讀入JVM(可能是.class文件副瀑、可能是網(wǎng)絡(luò)等)恋谭。那么我們可不可以在都進來后,開始下一個步驟前狈孔,改一改類的二進制信息呢?(比如往里面添加統(tǒng)計時間)

答案是當然可以均抽。朋友,Java agent和instrumentation了解一下潦蝇?

什么是java agent

看字面意思深寥,agent就是代理的意思啊。從外部視角來看翩迈,它相當于對我的java程序的一個代理。既然提到代理负饲,那我不是可以在運行我的java程序前做些什么小動作?

先來看用法: java -javaagent:the-agent-demo.jar HelloWorld

在命令行中敲入上面的命令妥泉,是說以the-agent-demo.jar為java agent洞坑,運行我的HelloWorld程序。

這個時候迟杂,在運行HelloWorld的main()方法前排拷,會先運行the-agent-demo.jar中的premain方法。

而premain方法的參數(shù)是什么呢监氢?

public static void premain(String agentArgument,
                               Instrumentation instrumentation){
        System.out.println("Java Agent Demo");
        SimpleClassTransformer simpleClassTransformer = new SimpleClassTransformer();
        instrumentation.addTransformer(simpleClassTransformer);
    }

agentArgument這個是自定義的參數(shù),比如我可以java -javaagent:the-agent-demo.jar=theAgentArumentDemo HelloWorld其中theAgentArgumentDemo就作為這個參數(shù)傳進來了纵揍。而第二個參數(shù)則是下一節(jié)要描述的议街。再次之前,還要注意隔盛,我們需要在the-agent-demo.jar里面打包進去包含Pre-Main參數(shù)的MENIFEST.MF文件。

Manifest-Version: 1.0
Premain-Class: cn.kobelee.test.TestJavaAgent

什么是instrumentation

Instrumentation是一個接口吮炕,定義了字節(jié)碼修改的規(guī)范龙亲,其位于java.lang.instrument包下面。

在這個包下面另外一個關(guān)鍵的接口類是ClassFileTransformer鳄炉,顧名思義類文件轉(zhuǎn)換器。它只有一個接口定義方法:

byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

注意到有一個byte[]數(shù)組的參數(shù)classFileBuffer佑女。這就是類的二進制文件buffer谈竿,其返回值也是byte[]數(shù)組,為修改后的類嚎花。那么這個方法的實現(xiàn)就是要transform(轉(zhuǎn)換/修改)類咯呀洲。

完整連起來就是: java -javaagent:xxx.jar HelloWorld指定代理的jar,里面有premain. 我們需要在premain里面對instrumentation添加ClassTransformer. 而這個classTransformer的實現(xiàn)就是你要怎么修改這個類兵罢。

什么是javassist

上面說到我們要修改byte[]來達到修改類的目的滓窍。可是贰您,直接改二進制文件這種騷操作可能只有上古達人才能做到吧拢操。于是,javassist的作用來了杠园。javassist是jboss提供的一個方便我們修改這個byte[]的工具包舔庶。直接上例子:


public class SimpleClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            if(!className.contains("kobelee")){//我只需要我定義的包路徑下統(tǒng)計陈醒,當然這個也判斷也可以刪了
                return null;
            }
            CtClass ctClass = ClassPool.getDefault().makeClass(new ByteArrayInputStream(classfileBuffer));
            CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
            for (CtBehavior method : declaredMethods) {
                CtClass[] parameterTypes = method.getParameterTypes();
                StringBuilder sb = new StringBuilder("{");
                for (int i = 0; i< parameterTypes.length; i++) {
                    sb.append("StringBuilder code = new StringBuilder();");
                    sb.append("code.append(\""+method.getLongName()+" before.\");");
                    sb.append("code.append(\""+parameterTypes[i].getName()+"\");");
                    sb.append("code.append(\":\");");
                    sb.append("code.append($args["+i+"]);");
                    sb.append("System.out.println(code.toString());");
                }

                sb.append("}");
                method.insertBefore(sb.toString());
                method.insertAfter("System.out.println(\""+method.getLongName()+" end\");");
            }
            byte[] returnByte = ctClass.toBytecode();
            return returnByte;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

可以看到代碼中有CtClass, CtMethod等類钉跷。這些表示的是CompileTimeXXX 也就是編譯時候的類相關(guān)信息肚逸。我們通過CtClass ctClass = ClassPool.getDefault().makeClass(new ByteArrayInputStream(classfileBuffer));創(chuàng)建了一個ctClass對象朦促,然后就可以對其注入我們需要的代碼了。上面代碼的例子只是輸出了調(diào)用的方法名和方法參數(shù)务冕,至于具體執(zhí)行時間,采用類似的方法也就不難實現(xiàn)了臊旭。

注意在打包agent.jar的時候油湖,不要忘了將javassist.jar也一起打包進去

public class HelloWorld {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        Test1 one = new Test1();
        one.test();
        System.out.println("Hello World");
    }


}
class Test1{
    public void test(){
        testInside(12345);
        System.out.println("the test method executed");
    }
    private  void testInside(int param){
        System.out.println("the testInside method executed,param:"+param);
    }
}

java -javaagent:JavaAgentDemo-1.0-SNAPSHOT.jar HelloWorld執(zhí)行控制臺輸出:

Java Agent Demo
方法:cn.kobelee.test.HelloWorld.main(java.lang.String[])執(zhí)行前
方法:cn.kobelee.test.Test1.test()執(zhí)行前
方法:cn.kobelee.test.Test1.testInside(int)執(zhí)行前
the testInside method executed,param:12345
方法:cn.kobelee.test.Test1.testInside(int)執(zhí)行后
the test method executed
方法:cn.kobelee.test.Test1.test()執(zhí)行后
Hello World
方法:cn.kobelee.test.HelloWorld.main(java.lang.String[])執(zhí)行后

可以看到乏德,我們的testInside方法也在調(diào)用前執(zhí)行了我們添加的代碼。

總結(jié)

通過使用java agent喊括,代理我們的應(yīng)用。同時對instrument的不同實現(xiàn)府喳,達到我們可以在業(yè)務(wù)代碼執(zhí)行前后插入任何我們想要的邏輯蘑拯;但是我們并沒有修改任何一行業(yè)務(wù)代碼。目前業(yè)界主要用來做分布式系統(tǒng)的鏈路跟蹤日志輸出申窘。將業(yè)務(wù)日志在java agent中按照指定格式輸出,同時輸出分布式環(huán)境下的調(diào)用唯一標識碎捺,然后再將日志放入流處理引擎中進行鏈路生成。比如這篇博客講的阿里鷹眼監(jiān)控:阿里巴巴鷹眼技術(shù)解密晋柱。其中的第一步日志輸出就需要用到本文講的方法诵叁。

參考資料

  1. 如何指導(dǎo)編寫一個javaagent
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浓领,隨后出現(xiàn)的幾起案子势腮,更是在濱河造成了極大的恐慌,老刑警劉巖捎拯,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件署照,死亡現(xiàn)場離奇詭異,居然都是意外死亡建芙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門右蒲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瑰妄,“玉大人映砖,你說我怎么就攤上這事∫赝耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵逝撬,是天一觀的道長乓土。 經(jīng)常有香客問我,道長狡相,這世上最難降的妖魔是什么食磕? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮滔悉,結(jié)果婚禮上单绑,老公的妹妹穿的比我還像新娘。我一直安慰自己搂橙,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布苔巨。 她就那樣靜靜地躺著废离,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悼尾。 梳的紋絲不亂的頭發(fā)上湘捎,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音舷胜,去河邊找鬼活翩。 笑死,一個胖子當著我的面吹牛材泄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峦树,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼急灭!你這毒婦竟也來了谷遂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤畴嘶,失蹤者是張志新(化名)和其女友劉穎集晚,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甩恼,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡条摸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了切端。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顷啼。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖茵瀑,靈堂內(nèi)的尸體忽然破棺而出躬厌,到底是詐尸還是另有隱情,我是刑警寧澤扛施,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布疙渣,位于F島的核電站,受9級特大地震影響妄荔,放射性物質(zhì)發(fā)生泄漏谍肤。R本人自食惡果不足惜灶轰,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一笋颤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伴澄,春花似錦阱缓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至玖媚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間勺像,已是汗流浹背错森。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留涩维,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓辰狡,卻偏偏與公主長得像垄分,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子叫倍,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容