写点什么

轻松带你学习 java-agent

发布于: 2021 年 04 月 22 日

​​​​​​摘要:java-agent 是应用于 java 的 trace 工具,核心是对 JVMTI(JVM Tool Interface)的调用。


本文分享自华为云社区《Java动态trace技术:java-agent》,原文作者:技术火炬手 。


动态 trace 技术是在应用部署之后监控程序的调用,获取其中的变量内容,甚至可以插入或替换部分代码。业界的 trace 工具很多,ptrace,strace,eBPF,btrace,java-agent 等等。这次应用的目的是监控 kafka 服务中 publish 与 consume 的调用,获取依赖关系。鉴于 kafka 是通过 Scala 语言编写,所以采用了 java-agent 技术。


java-agent 是应用于 java 的 trace 工具,核心是对 JVMTI(JVM Tool Interface)的调用。JVMTI 是 java 虚拟机对外开放的一系列接口函数,通过 JVMTI 可以获取 java 虚拟机当前运行的状态。java-agent 程序运行时会在 java 虚拟机中挂载一个 agent 进程,通过 JVMTI 监控所挂载的 java 应用。通过 agent 程序可以完成 java 代码的热替换,类加载的过程监控等功能。


java-agent 的挂载方式有两种,一种是静态挂载,一种是动态挂载。静态挂载中,agent 与 java 应用一起启动,在 java 应用初始化前 agent 就已经挂载完成,并开始监控 java 应用。动态挂载则是在应用运行过程中,通过进程 ID 确定挂载对象,动态的将 agent 挂载在目标进程上。

静态挂载


首先编写 java-agent 的监控程序,静态挂载的入口函数为 premain。premain 函数有两种,区别是传入参数不同。通常选择带有 Instrumentation 参数,可以使用该变量完成代码的热替换。


public static void premain(String agentArgs, Instrumentation inst);public static void premain(String agentArgs);
复制代码


​下面是一个简单的例子。在 premain 函数中,使用 Instrumentation 增加一个 transformer。当监控的 java 应用每次加载 class 的时候都会调用 transformer。DefineTransformer 是一个 transformer,是 ClassFileTransformer 的实现。在它的 transform 函数的入参中会给出当前加载的类名,类加载器等信息。样例中我们只是打印了加载的类名。


import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import javassist.*;public class PreMain {     public static void premain(String agentArgs, Instrumentation inst) {        System.out.println("agentArgs : " + agentArgs);        inst.addTransformer(new DefineTransformer(), true);    }     static class DefineTransformer implements ClassFileTransformer{         @Override        public byte[] transform(ClassLoader loader,                                String className,                                Class<?> classBeingRedefined,                                ProtectionDomain protectionDomain,                                byte[] classfileBuffer){            System.out.println("premain load Class:" + className);            return classfileBuffer;        }    }}
复制代码


运行 java-agent 需要将上述程序打包成一个 jar 文件,在 jar 文件的 MANIFEST.MF 中需要包含以下几项


Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: com.huawei.PreMain
复制代码


Premain-Class 声明了这个 jar 的 premain 函数所在的类,java-agent 加载 jar 包时会在 PreMain 类中寻找 premain。Can-Redefine-Classes 与 Can-Retransform-Classes 声明为 true,表示允许这段程序修改 java 应用的代码。


如果你是使用 Maven 的项目,可以使用增加下面的插件来自动添加 MANIFEST.MF


<plugin>    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-assembly-plugin</artifactId>    <version>2.6</version>    <configuration>        <appendAssemblyId>false</appendAssemblyId>    <descriptorRefs>        <descriptorRef>jar-with-dependencies</descriptorRef>    </descriptorRefs>    <archive>        <manifest>            <addClasspath>true</addClasspath>        </manifest>        <manifestEntries>            <Premain-Class>com.huawei.PreMain</Premain-Class>            <Can-Redefine-Classes>true</Can-Redefine-Classes>            <Can-Retransform-Classes>true</Can-Retransform-Classes>        </manifestEntries>    </archive>    </configuration>    <executions>    <execution>        <id>assemble-all</id>        <phase>package</phase>        <goals>        <goal>single</goal>        </goals>    </execution>    </executions></plugin>
复制代码


​输出 jar 文件之后,编写一个 hello world 的 java 应用编译为 hello.class,在启动应用时使用如下命令


java -javaagent:/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar hello
复制代码


​在执行中就可以打印 java 虚拟机在运行 hello.class 所加载的所有类。


java-agent 的功能不仅限于输出类的加载过程,通过下面这个样例可以实现代码的热替换。首先编写一个测试类。


public class App {    public static void main( String[] args )    {        try{            System.out.println( "main start!" );             App test = new App();            int x1 = 1;            int x2 = 2;            while(true){                System.out.println(Integer.toString(test.add(x1, x2)));                Thread.sleep(2000);            }        } catch (InterruptedException e) {            e.printStackTrace();            System.out.println("main end");        }     }     private int add(int x1, int x2){        return x1+x2;    }}
复制代码


​然后我们修改 PreMain 类中 transformer,并通过 Instrumentation 添加这个 transformer。与 DefineTransformer 一样。


static class MyClassTransformer implements ClassFileTransformer {        @Override        public byte[] transform(final ClassLoader loader,                                final String className,                                final Class<?> classBeingRedefined,                                final ProtectionDomain protectionDomain,                                final byte[] classfileBuffer) {            // 如果当前加载的类是我们编写的测试类,进入修改。            if ("com/huawei/App".equals(className)) {                try {                    // 从ClassPool获得CtClass对象                    final ClassPool classPool = ClassPool.getDefault();                    final CtClass clazz = classPool.get("com.huawei.App");                     //打印App类中的所有成员函数                    CtMethod[] methodList = clazz.getDeclaredMethods();                    for(CtMethod method: methodList){                        System.out.println("premain method: "+ method.getName());                    }                     // 获取add函数并替换,$1表示函数的第一个入参                    CtMethod convertToAbbr = clazz.getDeclaredMethod("add");                    String methodBody = "{return $1 + $2 + 11;}";                    convertToAbbr.setBody(methodBody);                     // 在add函数体之前增加一段代码,同理也可以在函数尾部添加                    String methodBody = "System.out.println(Integer.toString($1));";                    convertToAbbr.insertBefore(methodBody);                     // 返回字节码,并且detachCtClass对象                    byte[] byteCode = clazz.toBytecode();                    //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载                    clazz.detach();                    return byteCode;                } catch (Exception ex) {                    ex.printStackTrace();                }            }            // 如果返回null则字节码不会被修改            return null;        }    }
复制代码


之后的步骤与之前相同,运行会发现 add 函数的逻辑已经被替换了。

动态挂载


动态挂载是在应用运行过程中动态的添加 agent。技术原理是通过 socket 与目标进程通讯,发送 load 指令在目标进程挂载指定 jar 文件。agent 执行过程中的功能与静态过载是完全相同的。在实施过程中,有几点不同。首先入口函数名不同,动态挂载的函数名是 agentmain。与 premain 类似,有两种格式。但通常采用带有 Instrumentation 的那种。如下例所示


public class AgentMain {     public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {        instrumentation.addTransformer(new MyClassTransformer(), true);        instrumentation.retransformClasses(com.huawei.Test.class);    }     static class MyClassTransformer implements ClassFileTransformer {        @Override        public byte[] transform(final ClassLoader loader,                                final String className,                                final Class<?> classBeingRedefined,                                final ProtectionDomain protectionDomain,                                final byte[] classfileBuffer) {            // 如果当前加载的类是我们编写的测试类,进入修改。            if ("com/huawei/App".equals(className)) {                try {                    // 从ClassPool获得CtClass对象                    final ClassPool classPool = ClassPool.getDefault();                    final CtClass clazz = classPool.get("com.huawei.App");                     //打印App类中的所有成员函数                    CtMethod[] methodList = clazz.getDeclaredMethods();                    for(CtMethod method: methodList){                        System.out.println("premain method: "+ method.getName());                    }                     // 获取add函数并替换,$1表示函数的第一个入参                    CtMethod convertToAbbr = clazz.getDeclaredMethod("add");                    String methodBody = "{return $1 + $2 + 11;}";                    convertToAbbr.setBody(methodBody);                     // 返回字节码,并且detachCtClass对象                    byte[] byteCode = clazz.toBytecode();                    //detach的意思是将内存中曾经被javassist加载过的Date对象移除,如果下次有需要在内存中找不到会重新走javassist加载                    clazz.detach();                    return byteCode;                } catch (Exception ex) {                    ex.printStackTrace();                }            }            // 如果返回null则字节码不会被修改            return null;        }    }}
复制代码


​功能与静态加载相同。需要注意的是,Instrumentation 增加了 transformer 之后,调用了 retransformClasses 函数。这是由于 transformer 只有在 Java 虚拟机加载 class 时才会调用。如果是通过动态加载的方式,需要监控的 class 文件可能已经加载完成了。所以需要调用 retransformClasses 重新加载。


另外一点不同是 MANIFEST.MF 文件需要添加 Agent-Class,如下所示


Can-Redefine-Classes: trueCan-Retransform-Classes: truePremain-Class: com.huawei.PreMainAgent-Class: com.huawei.AgentMain
复制代码


​最后一点不同是加载方式不同。动态挂载需要编写一个加载脚本。如下所示,在这段脚本中,首先遍历所有的 java 进程,通过启动类名辨识需要监控的进程。通过进程 id 获取 VirtualMachine 实例,并加载 agentmain 的 jar 文件。


import com.sun.tools.attach.*;import java.io.IOException;import java.util.List; public class TestAgentMain {     public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{        //获取当前系统中所有 运行中的 虚拟机        System.out.println("running JVM start ");        List<VirtualMachineDescriptor> list = VirtualMachine.list();        for (VirtualMachineDescriptor vmd : list) {             System.out.println(vmd.displayName());            String aim = "com.huawei.App";            if (vmd.displayName().endsWith(aim)) {                System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id()));                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());                virtualMachine.loadAgent("/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar");                virtualMachine.detach();            }        }    }}
复制代码


Scala 程序监控


Scala 与 Java 兼容性很好,所以使用 java-agent 监控 scala 应用也是可行的。但是仍然需要注意一些问题。第一点是程序替换只对 class 有作用,对 object 是无效的。第二个问题是,动态替换中是将程序编译为字节码之后再去替换的。java-agent 使用的是 java 的编译规则,所以替换程序要使用 java 的语言规则,否则会出现编译错误。例如示例中使用

System.out.println

输出参数信息,如果使用 scala 的 println 会出现编译错误。


参考资料:

Java 动态调试技术原理及实践

javaagent使用指南


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 04 月 22 日阅读数: 51
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
轻松带你学习java-agent