本文分享自华为云社区《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{@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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使用指南

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

轻松带你学习java-agent相关推荐

  1. 一文带你了解Java Agent

    转载自  一文带你了解Java Agent Java Agent这个技术,对于大多数同学来说都比较陌生,像个黑盒子.但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于Java Agent实现 ...

  2. 几行代码,轻松带你实现Java生成二维码功能!

    作者:瑞 新 https://0x9.me/rVjw2 文章目录 效果图 步骤 maven依赖 工具类 service 扩展 局域网调试 线上调试 效果图 步骤 maven依赖 <!--生成二维 ...

  3. 教你学Java | 带你学习Java多线程(续:创建线程的其他方式与activeCount方法的浅谈)

    前言 相信每一位程序猿对"多线程"这个概念应该都不陌生,无论是在开发还是面试的时候,都会遇到多线程的问题.不过,一定有很多小伙伴才刚刚接触到多线程,那么在此就由小弟为各位小伙伴细细 ...

  4. 两道面试题,带你解析Java类加载机制

    2019独角兽企业重金招聘Python工程师标准>>> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa {static ...

  5. Java Agent的隔离实现以及卸载时一些坑

    转载自   Java Agent的隔离实现以及卸载时一些坑 在<一文带你了解Java Agent>中,让大家了解了Java Agent的来龙去脉,当通过attach方式去动态加载一个Jav ...

  6. 菜鸟浅谈自己学习Java过程的经历

    [size=large][/size][size=medium] 国庆第二天,昨晚北京下了好大的雨...吵死了...半夜醒来.一个人的寝室,唯有寂寞伴随.习惯了...程序员的真实生活写照...哎,想起 ...

  7. 快速学习java学习的秘籍

    作者主页:paper jie的博客_CSDN博客-C语言,算法详解领域博主 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦. 前言 跟其他行业相比,做Java开发的岗位确实算 ...

  8. 适合零基础学习者的Java学习路线图到底长啥样?一篇文章带你学会Java

    很多小伙伴在转行互联网的时候,都担心自己不能坚持,不知道Java适不适合自己. 那最好的方式就是先不要着急直接转行,自己可以先去试着学习一些基础知识,看看对Java的学习难度能否适应以及自己是否真心喜 ...

  9. gateway 内存溢出问题_带你学习jvm java虚拟机 arthas/性能调优/故障排除/gc回收/内存溢出等...

    学完本课程,您将掌握: 内存溢出问题实战 CPU飙升问题实战 阿里巴巴Arthas在线诊断 Class字节详细拆解 手写类加载器.四种类加载器.双亲委托模型 对象创建.存储.访问.加载解析 性能调优. ...

最新文章

  1. SAPscripts 到导数程序中取数据的实例
  2. js+php聊天程序
  3. 如何构建高并发高可用的剧场直播云端混流服务?
  4. 无穷大功率电源matlab仿真,MATLAB-Simulink系统建模与仿真-实验报告
  5. 程序员常用网站收藏[定期更新]——csdn博客
  6. php mysql grant_mysql grant命令详解_MySQL
  7. 【Python】Numpy中对向量、矩阵的使用
  8. PHP面向对象构造函数,析构函数
  9. 软件工程专插本_2021年广东专插本8所学校招生专业(拟),华师停招专插本?...
  10. 西门子ug通用后处理_五轴UG(NX)后处理
  11. 总线、设备和驱动的关系
  12. 操作系统课程设计报告
  13. 计算机设计大赛(微课教学辅助类)国赛总结(国赛二等奖)
  14. 案例分享:Qt西门子PLC调试模拟工具(包含PLC上位机通讯,PLC服务器,读写Byte、Int、DInt、Real)(持续更新,当前v1.6.0)
  15. 计算机专业的男生喜欢你,男生真心喜欢你的五个表现
  16. 用于使计算机发出嘟嘟声的宏命令,2012年计算机等级考试二级Access辅导笔记:Access宏的使用...
  17. doucument.referrer部分安卓机型一直为空问题
  18. 生产环境nginx集群高可用方案
  19. 【电脑技巧】如何使用dxdiag查看电脑信息(Win11)
  20. 获取Excel种文字的拼音首字母函数

热门文章

  1. web系统备份mysql_学会自动备份MYSQL和WEB的数据
  2. 陈玉林是哪里人_玉林仿古砖公司在哪里
  3. main函数的类型定义
  4. oracle liunx 6.5安装目录,linux redhat 6.5 多路径配置
  5. php网站商品图片上传代码,PHP实现图片上传代码
  6. API的String 一些用法
  7. form-create教程:给内置组件和自定义组件添加事件
  8. Linux 7.x 防火墙端口
  9. [BZOJ]3926 诸神眷顾的幻想乡(ZJOI2015)
  10. mysql 忘记密码