0871-64605728
您当前位置:网站首页 >> 知识专区
轻松带你学习java-agent
文章来源:云社区 技术火炬手  上传时间:2021-4-26  浏览量:837

本文分享自华为云社区《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参数,可以使用该变量完成代码的热替换。

	
  1. public static void premain(String agentArgs, Instrumentation inst);
  2. public static void premain(String agentArgs);

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

	
  1. import java.lang.instrument.ClassFileTransformer;
  2. import java.lang.instrument.Instrumentation;
  3. import java.security.ProtectionDomain;
  4. import javassist.*;
  5. public class PreMain {
  6. public static void premain(String agentArgs, Instrumentation inst) {
  7. System.out.println("agentArgs : " + agentArgs);
  8. inst.addTransformer(new DefineTransformer(), true);
  9. }
  10. static class DefineTransformer implements ClassFileTransformer{
  11. @Override
  12. public byte[] transform(ClassLoader loader,
  13. String className,
  14. Class<?> classBeingRedefined,
  15. ProtectionDomain protectionDomain,
  16. byte[] classfileBuffer){
  17. System.out.println("premain load Class:" + className);
  18. return classfileBuffer;
  19. }
  20. }
  21. }

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

	
  1. Can-Redefine-Classes: true
  2. Can-Retransform-Classes: true
  3. 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

	
  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-assembly-plugin</artifactId>
  4. <version>2.6</version>
  5. <configuration>
  6. <appendAssemblyId>false</appendAssemblyId>
  7. <descriptorRefs>
  8. <descriptorRef>jar-with-dependencies</descriptorRef>
  9. </descriptorRefs>
  10. <archive>
  11. <manifest>
  12. <addClasspath>true</addClasspath>
  13. </manifest>
  14. <manifestEntries>
  15. <Premain-Class>com.huawei.PreMain</Premain-Class>
  16. <Can-Redefine-Classes>true</Can-Redefine-Classes>
  17. <Can-Retransform-Classes>true</Can-Retransform-Classes>
  18. </manifestEntries>
  19. </archive>
  20. </configuration>
  21. <executions>
  22. <execution>
  23. <id>assemble-all</id>
  24. <phase>package</phase>
  25. <goals>
  26. <goal>single</goal>
  27. </goals>
  28. </execution>
  29. </executions>
  30. </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的功能不仅限于输出类的加载过程,通过下面这个样例可以实现代码的热替换。首先编写一个测试类。

	
  1. public class App
  2. {
  3. public static void main( String[] args )
  4. {
  5. try{
  6. System.out.println( "main start!" );
  7. App test = new App();
  8. int x1 = 1;
  9. int x2 = 2;
  10. while(true){
  11. System.out.println(Integer.toString(test.add(x1, x2)));
  12. Thread.sleep(2000);
  13. }
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. System.out.println("main end");
  17. }
  18. }
  19. private int add(int x1, int x2){
  20. return x1+x2;
  21. }
  22. }

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

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

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

动态挂载

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

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

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

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

	
  1. Can-Redefine-Classes: true
  2. Can-Retransform-Classes: true
  3. Premain-Class: com.huawei.PreMain
  4. Agent-Class: com.huawei.AgentMain

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

	
  1. import com.sun.tools.attach.*;
  2. import java.io.IOException;
  3. import java.util.List;
  4. public class TestAgentMain {
  5. public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{
  6. //获取当前系统中所有 运行中的 虚拟机
  7. System.out.println("running JVM start ");
  8. List<VirtualMachineDescriptor> list = VirtualMachine.list();
  9. for (VirtualMachineDescriptor vmd : list) {
  10. System.out.println(vmd.displayName());
  11. String aim = "com.huawei.App";
  12. if (vmd.displayName().endsWith(aim)) {
  13. System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id()));
  14. VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
  15. virtualMachine.loadAgent("/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar");
  16. virtualMachine.detach();
  17. }
  18. }
  19. }
  20. }

Scala程序监控

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

参考资料:

Java 动态调试技术原理及实践
javaagent使用指南

30

2022-12

机器学习编译器的前世今生

机器学习编译器的前世今生

27

2021-08

设计模式

设计模式

11

2021-06

鸿蒙App开发(4)---初识鸿蒙开发

鸿蒙App开发(4)---初识鸿蒙开发

26

2021-04

轻松带你学习java-agent

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

10

2022-06

LLVM之父Chris Lattner:编译器的黄金时代

LLVM之父Chris Lattner:编译器的黄金时代

10

2022-06

Hugging Face创始人亲述:一个GitHub史上增长最快的AI项目

Hugging Face创始人亲述:一个GitHub史上增长最快的AI项目

18

2022-02

静态路由简介及配置

静态路由简介及配置

21

2022-10

作业帮董晓聪:作业帮云原生降本增效实践之路

作业帮董晓聪:作业帮云原生降本增效实践之路
返回顶部
客服电话
0871-64605728
用微信扫一扫关注我们
请各公司推销人员注意:我单位拒绝任何方式、任何形式的电话推销,请勿拔打我单位客服热线进行电话推销,谢谢合作!
公司名称:云南昂略科技有限公司
联系地址:云南省昆明市官渡区永平路188号鑫都韵城写字楼6栋1004号
联系电话:0871-64605728、传真号码:0871-64605728
电子邮箱:19701580@qq.com
关键词:知识专区:轻松带你学习java-agent,云南昂略科技有限公司,云南移动执法平台建设,云南智慧安防调度系统,云南头戴式安全终端,昂略科技
云南网站建设,云南网页设计,昆明网站建设,昆明网页设计  网站管理
【版权声明】本站部分内容由互联网用户自行发布,著作权或版权归原作者所有。如果侵犯到您的权益请发邮件致info@ynjwz.com,我们会第一时间进行删除并表示歉意。