java agent: JVM的層面的"AOP"

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

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

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

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

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

Spring AOP

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

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);
    }
}

可以注意到,我們?cè)趖est()方法中調(diào)用了testInside()這個(gè)private的方法。有深入了解Spring AOP的實(shí)現(xiàn)原理就知道,Spring在實(shí)現(xiàn)AOP的時(shí)候是在bean放入容器前生成的代理類。而test()調(diào)用this.testInside()的情況,是沒有對(duì)testInside()進(jìn)行AOP增強(qiáng)的。怎么辦?這個(gè)時(shí)候,就進(jìn)入我們本文的重點(diǎn)。

Java agent 在JVM層面實(shí)現(xiàn)"AOP增強(qiáng)"

回顧一下,類是怎么被加載到JVM里面的?加載->驗(yàn)證->準(zhǔn)備->解析->初始化。在加載階段,一定會(huì)有一個(gè)步驟是把類的二進(jìn)制信息讀入JVM(可能是.class文件、可能是網(wǎng)絡(luò)等)。那么我們可不可以在都進(jìn)來后,開始下一個(gè)步驟前,改一改類的二進(jìn)制信息呢?(比如往里面添加統(tǒng)計(jì)時(shí)間)

答案是當(dāng)然可以。朋友,Java agent和instrumentation了解一下?

什么是java agent

看字面意思,agent就是代理的意思啊。從外部視角來看,它相當(dāng)于對(duì)我的java程序的一個(gè)代理。既然提到代理,那我不是可以在運(yùn)行我的java程序前做些什么小動(dòng)作?

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

在命令行中敲入上面的命令,是說以the-agent-demo.jar為java agent,運(yùn)行我的HelloWorld程序。

這個(gè)時(shí)候,在運(yùn)行HelloWorld的main()方法前,會(huì)先運(yùn)行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這個(gè)是自定義的參數(shù),比如我可以java -javaagent:the-agent-demo.jar=theAgentArumentDemo HelloWorld其中theAgentArgumentDemo就作為這個(gè)參數(shù)傳進(jìn)來了。而第二個(gè)參數(shù)則是下一節(jié)要描述的。再次之前,還要注意,我們需要在the-agent-demo.jar里面打包進(jìn)去包含Pre-Main參數(shù)的MENIFEST.MF文件。

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

什么是instrumentation

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

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

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

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

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

什么是javassist

上面說到我們要修改byte[]來達(dá)到修改類的目的。可是,直接改二進(jìn)制文件這種騷操作可能只有上古達(dá)人才能做到吧。于是,javassist的作用來了。javassist是jboss提供的一個(gè)方便我們修改這個(gè)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)計(jì),當(dāng)然這個(gè)也判斷也可以刪了
                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 也就是編譯時(shí)候的類相關(guān)信息。我們通過CtClass ctClass = ClassPool.getDefault().makeClass(new ByteArrayInputStream(classfileBuffer));創(chuàng)建了一個(gè)ctClass對(duì)象,然后就可以對(duì)其注入我們需要的代碼了。上面代碼的例子只是輸出了調(diào)用的方法名和方法參數(shù),至于具體執(zhí)行時(shí)間,采用類似的方法也就不難實(shí)現(xiàn)了。

注意在打包agent.jar的時(shí)候,不要忘了將javassist.jar也一起打包進(jìn)去

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í)行控制臺(tái)輸出:

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

參考資料

  1. 如何指導(dǎo)編寫一個(gè)javaagent
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容