摘要: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使用指南

点击关注,第一工夫理解华为云陈腐技术~