美文网首页JVM
JavaAgent技术

JavaAgent技术

作者: 知止9528 | 来源:发表于2019-01-23 19:38 被阅读5次

    java 提供了操作运行时字节码的机制,见包java.lang.instrument。可以用java开发一个jar包,以agent方式部署。比如jar包myAgent.jar

    部署指令

    -javaagent:/path/myAgent.jar
    

    jar包规范:
    在main方法之前执行
    1、有Premain-Class属性
    文件META-INF/MANIFEST.MF

    Manifest-Version: 1.0
    Implementation-Title: myagent
    Premain-Class: com.yjm.agent.AgentMain
    Implementation-Version: 1.0-SNAPSHOT
    Built-By: yjm628
    

    2、Premain-Class类有premain方法。

    // 第一个优先级高;参数只能是一个string,如果多个参数,需要agent自己定义规范并解析
    public static void premain(String agentArgs, Instrumentation inst);
    public static void premain(String agentArgs);
    

    在main方法之后执行

    同premain,只是属性和实现方法的区别

    有Agent-Class属性
    必须实现public的静态方法agentmain

    public static void agentmain(String agentArgs, Instrumentation inst);
    public static void agentmain(String agentArgs);
    

    Instrumentation接口

    Instrumentation是一个接口,提供了服务使得可以在运行时操作java程序,包括改变字节码,新增一个jar包,替换class等。 于是可以通过它实现各种功能的agent,比如监控,覆盖率分析,打印日志,动态部署等工具。
    主要方法:

    • addTransformer:可以在加载字节码时注册拦截器装换源代码
    • redefineClasses: 替换class
    • appendToBootstrapClassLoaderSearch: 运行时指定jar包给bootclassload加载
      目前提供10多个方法,详细见api。

    使用asm增强字节

    ASM库可以用来生成、转换和分析编译后的java类。asm提供了两套api,核心的API是基于事件的,而Tree API是基于对象的。基于对象模型的api构建在基于事件的模型之上。asm有如下特点:

    小,且设计良好模块化的API,且易于使用
    文档完善,有eclipse何idea插件帮助方便生产字节码操作api。
    社区完善、开发。
    两套api各有优缺点,基于事件的api速度更快,使用内存空间更新,但实现复杂转换较困难。下文简单介绍基于事件的api用法。

    byte[] b1 = ...;
    ClasssWriter cw = new ClassWriter();
    ClassAdapter ca = new ClassAdapter(cw); 
    ClassReader cr = new ClassReader(b1);
    cr.accept(ca, 0);
    byte[] b2 = cw.toByteArray();
    

    Reader相当于字节码生产者,把所有事件传递给writer,而adapter是一个中间适配器,形成一个转换链。下面写一个示例,往一个class的方法里面写一行代码;

    public static void main(String[] args) throws Exception {
            // 获取class byte
            String className = "com.yjm.agent.Test";
            InputStream resourceAsStream = asmTest.class
                    .getClassLoader()
                    .getResourceAsStream(className.replace('.', '/') + ".class");
            byte[] bytes = IOUtils.toByteArray(resourceAsStream);
            // 往方法里面添加代码System.out.println("hello");
            ClassWriter cw = new ClassWriter(8);
            TestVisitor ca = new TestVisitor(cw);
            ClassReader cr = new ClassReader(bytes);
            cr.accept(ca, 0);
            byte[] b2 = cw.toByteArray();
            FileOutputStream output = new FileOutputStream(new File("/tmp/target.class"));
            IOUtils.write(b2, output);
        }
        static class TestVisitor extends ClassVisitor {
            public TestVisitor(ClassVisitor classVisitor) {
                super(Opcodes.ASM7, classVisitor);
            }
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
                if (name.equals("<init>") || mv == null) {
                    // 构造方法不需要添加
                    return mv;
                }
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("hello");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                return mv;
            }
        }
    

    详细文档见asm官网

    简单示例
    下面写一个javaagent,统计应用方法执行时间。
    第一步: 构建agent包

    public class AgentMain {
        //代理程序入口函数
        public static void premain(String args, Instrumentation inst) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            System.out.println("agent premain begin");
            //添加字节码转换器
            inst.addTransformer(new Transformer(), true);
            System.out.println("agent premain end");
        }
    }
    

    把AgentMain添加到jar包属性中,可以借助maven-assembly-plugin,把agent依赖都打入包中并制定premain类。

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
            <execution>
                <goals>
                    <goal>single</goal>
                </goals>
                <phase>package</phase>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>com.yjm.agent.AgentMain</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </execution>
        </executions>
    </plugin>
    

    生成的jar包含了依赖的类,比如asm。

    第二步: 编写Transformer

    public class Transformer implements ClassFileTransformer {
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain, byte[] classfileBuffer) {
            if (className == null) {
                //返回null,将会使用原生class。
                return null;
            }
            if (className.startsWith("java") ||
                    className.startsWith("javax") ||
                    className.startsWith("jdk") ||
                    className.startsWith("sun") ||
                    className.startsWith("com/sun") ||
                    className.startsWith("com/intellij") ||
                    className.startsWith("org/jetbrains") ||
                    className.startsWith("com/thoreauz/agent")
            ) {
                // 不对JDK类以及agent类增强
                return null;
            }
            //读取类的字节码流
            ClassReader reader = new ClassReader(classfileBuffer);
            //创建操作字节流值对象,ClassWriter.COMPUTE_MAXS:表示自动计算栈大小
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            //接收一个ClassVisitor子类进行字节码修改
            reader.accept(new TimeClassVisitor(writer, className), 8);
            //返回修改后的字节码流
            return writer.toByteArray();
        }
    }
    
    public class TimeClassVisitor extends ClassVisitor {
        private String className = null;
        public TimeClassVisitor(ClassVisitor classVisitor, String className) {
            super(Opcodes.ASM7, classVisitor);
            this.className = className.replace('/','.');
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
            //过来待修改类的构造函数
            if (name.equals("<init>") || mv == null) {
                // 对象初始化方法就不增强了
                return mv;
            }
            mv = new AdviceAdapter(Opcodes.ASM7, mv, access, name, descriptor) { 
                @Override
                public void onMethodEnter() {
                    //TODO 1方法进入时计时
                }
                @Override
                public void onMethodExit(int opcode) {
                    //TODO 方法退出时获取结束时间并计算执行时间
                }
            };
            return mv;
        }
    }
    

    通过AdviceAdapter分别在方法进入和退出时修改方法字节码。这儿引入一个保存时间的类:

    public class TimeCache {
        public static Map<String, Long> startTimeMap = new HashMap<>();
        public static Map<String, Long> endTimeMap = new HashMap<>();
        public static void setStartTime(String methodName, long time) {
            startTimeMap.put(methodName, time);
        }
        public static void setEndTime(String methodName, long time) {
            endTimeMap.put(methodName, time);
        }
        public static String getCostTime(String methodName) {
            long start = startTimeMap.get(methodName);
            long end = endTimeMap.get(methodName);
            return methodName + "[" + (end - start) + " ms]";
        }
    }
    

    假设有一个test方法如下

    public class Test {
        public void test() {
             // do something
        }
    }
    

    实现计时功能,只需要把test改成如下即可:

    public class Test {
        public void test() {
            TimeCache.setStartTime("test",System.currentTimeMillis());
            // do something
            TimeCache.setEndTime("test",System.currentTimeMillis());
            System.out.println(TimeCache.getCostTime("test"));
        }
    }
    

    于是,怎么通过asm把test方法转换成如上代码便是关键
    通过javap命令,反编译class为字节码结构

    # javap -c Test 
    public class com.yjm.agent.Test {
      public com.yjm.agent.Test();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
      public void test();
        Code:
           0: ldc           #2                  // String test
           2: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
           5: invokestatic  #4                  // Method com/yjm/agent/TimeCache.setStartTime:(Ljava/lang/String;J)V
           8: ldc           #2                  // String test
          10: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
          13: invokestatic  #5                  // Method com/yjm/agent/TimeCache.setEndTime:(Ljava/lang/String;J)V
          16: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
          19: ldc           #2                  // String test
          21: invokestatic  #7                  // Method com/yjm/agent/TimeCache.getCostTime:(Ljava/lang/String;)Ljava/lang/String;
          24: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          27: return
    }
    

    对照操作指令转成asm的方法。

    最终得到TimeClassVisitor如下:

    public class TimeClassVisitor extends ClassVisitor {
        private String className = null;
        public TimeClassVisitor(ClassVisitor classVisitor, String className) {
            super(Opcodes.ASM7, classVisitor);
            this.className = className.replace('/','.');
        }
        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
            //过来待修改类的构造函数
            if (name.equals("<init>") || mv == null) {
                // 对象初始化方法就不增强了
                return mv;
            }
            String key = className + ":" + name;
            mv = new AdviceAdapter(Opcodes.ASM7, mv, access, name, descriptor) {
                //方法进入时获取开始时间
                @Override
                public void onMethodEnter() {
                    mv.visitLdcInsn(key);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                    mv.visitMethodInsn(INVOKESTATIC,
                            "com/yjm/agent/TimeCache",
                            "setStartTime",
                            "(Ljava/lang/String;J)V",
                            false);
                }
                //方法退出时获取结束时间并计算执行时间
                @Override
                public void onMethodExit(int opcode) {
                    mv.visitLdcInsn(key);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                    mv.visitMethodInsn(INVOKESTATIC,
                            "com/yjm/agent/TimeCache",
                            "setEndTime",
                            "(Ljava/lang/String;J)V",
                            false);
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn(key);
                    mv.visitMethodInsn(INVOKESTATIC,
                            "com/yjm/agent/TimeCache",
                            "getCostTime",
                            "(Ljava/lang/String;)Ljava/lang/String;",
                            false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                }
            };
            return mv;
        }
    }
    

    测试

    public class Application {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("hello world!");
            add(1, 2);
        }
        private static int add(int a, int b) throws InterruptedException {
            Thread.sleep(300);
            return a + b;
        }
    }
    

    打印出Application的main和add方法的运行时间:

    # java  -javaagent:/省略了path/myagent-1.0-SNAPSHOT-jar-with-dependencies.jar com.yjm.test.Application 
    agent premain begin
    agent premain end
    hello world!
    com.yjm.test.Application:add[186 ms]
    com.yjm.test.Application:main[186 ms]
    

    本文简单介绍了javaagent的使用规范,并写了premain的agent,通过asm增强代码统计方法运行时间。

    javaagent的使用场景很多,agentmain在main方法运行后操作java应用更是提供了无数可能。但是真正开发agent还有许多难点比如:

    agent本身依赖asm等其他二方包,为了不污染jdk自带类和应用类,应该自定义classLoader,隔离agent类。
    asm字节码增强,需要考虑排查一些类,避免造成死循环调用。比如PrintStream.println方法注入System.out.println。还有对已经注入代码的类的判断避免重复注入(同标志或者代码锁)。
    字节码增强已经改变了类,agent的问题代码可能影响应用的执行。

    最后推荐阿里开源的两个工具,基于本文提到的原理开发而来。

    1. TProfiler:性能分析工具,代码比较简单,可以作为初步学习参考。
    2. arthas:java问题诊断神器,功能强大丰富。

    相关文章

      网友评论

        本文标题:JavaAgent技术

        本文链接:https://www.haomeiwen.com/subject/lzmsjqtx.html