java agent: JVM的层面的"AOP"

考虑下这个问题:怎么知道我写的方法执行了多久?

再考虑下:怎么知道所有方法分别执行了多久?

初学者都会的方法

void methodDemo(){
    Date start = new Date();
    
    //...业务代码
    
    Date end = new Date();
    
    Long duration = end.getTime() - start.getTime();
}

上面的方法我的确可以知道methodDemo()的执行时间,但是如果我有一百个这样的方法怎么办?写一百遍么?这时候用过Spring的站出来指着我鼻子说:”你傻啊,用AOP啊!“

Spring AOP

具体的样例代码我就不赘述了,网上一搜一大把教你使用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()方法中调用了testInside()这个private的方法。有深入了解Spring AOP的实现原理就知道,Spring在实现AOP的时候是在bean放入容器前生成的代理类。而test()调用this.testInside()的情况,是没有对testInside()进行AOP增强的。怎么办?这个时候,就进入我们本文的重点。

Java agent 在JVM层面实现"AOP增强"

回顾一下,类是怎么被加载到JVM里面的?加载->验证->准备->解析->初始化。在加载阶段,一定会有一个步骤是把类的二进制信息读入JVM(可能是.class文件、可能是网络等)。那么我们可不可以在都进来后,开始下一个步骤前,改一改类的二进制信息呢?(比如往里面添加统计时间)

答案是当然可以。朋友,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方法的参数是什么呢?

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

agentArgument这个是自定义的参数,比如我可以java -javaagent:the-agent-demo.jar=theAgentArumentDemo HelloWorld其中theAgentArgumentDemo就作为这个参数传进来了。而第二个参数则是下一节要描述的。再次之前,还要注意,我们需要在the-agent-demo.jar里面打包进去包含Pre-Main参数的MENIFEST.MF文件。

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

什么是instrumentation

Instrumentation是一个接口,定义了字节码修改的规范,其位于java.lang.instrument包下面。

在这个包下面另外一个关键的接口类是ClassFileTransformer,顾名思义类文件转换器。它只有一个接口定义方法:

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

注意到有一个byte[]数组的参数classFileBuffer。这就是类的二进制文件buffer,其返回值也是byte[]数组,为修改后的类。那么这个方法的实现就是要transform(转换/修改)类咯。

完整连起来就是: java -javaagent:xxx.jar HelloWorld指定代理的jar,里面有premain. 我们需要在premain里面对instrumentation添加ClassTransformer. 而这个classTransformer的实现就是你要怎么修改这个类。

什么是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")){//我只需要我定义的包路径下统计,当然这个也判断也可以删了
                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 也就是编译时候的类相关信息。我们通过CtClass ctClass = ClassPool.getDefault().makeClass(new ByteArrayInputStream(classfileBuffer));创建了一个ctClass对象,然后就可以对其注入我们需要的代码了。上面代码的例子只是输出了调用的方法名和方法参数,至于具体执行时间,采用类似的方法也就不难实现了。

注意在打包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执行控制台输出:

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

可以看到,我们的testInside方法也在调用前执行了我们添加的代码。

总结

通过使用java agent,代理我们的应用。同时对instrument的不同实现,达到我们可以在业务代码执行前后插入任何我们想要的逻辑;但是我们并没有修改任何一行业务代码。目前业界主要用来做分布式系统的链路跟踪日志输出。将业务日志在java agent中按照指定格式输出,同时输出分布式环境下的调用唯一标识,然后再将日志放入流处理引擎中进行链路生成。比如这篇博客讲的阿里鹰眼监控:阿里巴巴鹰眼技术解密。其中的第一步日志输出就需要用到本文讲的方法。

参考资料

  1. 如何指导编写一个javaagent
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容