关于java:轻松带你学习javaagent

53次阅读

共计 7293 个字符,预计需要花费 19 分钟才能阅读完成。

摘要: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: true
Can-Retransform-Classes: true
Premain-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: true
Can-Retransform-Classes: true
Premain-Class: com.huawei.PreMain
Agent-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 使用指南

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

正文完
 0