首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >动态追踪之java agent

动态追踪之java agent

作者头像
索码理
发布于 2022-12-28 06:53:48
发布于 2022-12-28 06:53:48
1K0
举报
文章被收录于专栏:索码理索码理

上篇文章我们说到阿里的诊断工具Arthas对方法和类的监控使用的是动态追踪技术,本文我们将介绍动态追踪技术Java Agent。

Java Agent是什么?

Java Agent技术,也被称为Java代理、Java探针,从JDK1.5它就出现了,它允许程序员利⽤其构建⼀个独⽴于应⽤程序的代理程序。Java Agent本身就是个jar包,它利用JVM提供的Instrumentation API来更改加载在JVM中的现有字节码,Java Agent可以理解为是JVM级别的AOP。

Java Agent 怎么用?

Java Agent使用包括两个部分:代码实现和配置文件。

  • 代码实现:这部分包括3个步骤:
  1. 实现ClassFileTransformer接口并重写transform方法。 ClassFileTransformer用于在JVM加载实现类之前转换类文件。
  2. 编写agent类 agent类中有两个方法:premain方法和agentmain方法。
    • premain用于在代理方法执行前调用,在 JVM 启动时必须为其指明Java代理。它有两个实现
    • agentmain用于在代理方法运行时执行。
  3. 修改配置文件并打包
  • 配置文件:配置文件名为MANIFEST.MF,需放在META-INF文件夹下或者在maven中配置。
代码语言:javascript
AI代码解释
复制
Premain-Class:表示实现premain方法的类。
Agent-Class:表示实现agentmain方法的类。
Can-Redefine-Classes:表示Java agent是否可以重新定义被代理类。
Can-Retransform-Classes:表示是否允许Java Agent转换被代理类。

Java Agent的加载

Java Agent的加载分为静态加载和动态加载。

静态加载

在应用程序启动时加载Java代理称为静态加载,静态加载在任何代码执行之前在启动时修改字节码。它是以premain方法为入口的,premain方法有两个重载方法:

代码语言:javascript
AI代码解释
复制
public static void premain(String agentArgs, Instrumentation inst)
public static void premain(String agentArgs)
  • agentArgs:字符串参数,通过-javaagent传递
  • inst:java.lang.instrument.Instrumentation类对象

JVM 会优先加载带 Instrumentation 对象参数的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。

下面写个简单的例子玩一下静态加载:

  1. 新建一个maven项目并引入javassist包
代码语言:javascript
AI代码解释
复制
 <dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.29.2-GA</version>
</dependency>
  1. 新建一个ClassFileTransformer接口实现类PrintMethodCostTransformer,用户打印方法耗时
代码语言:javascript
AI代码解释
复制
public class PrintMethodCostTransformer implements ClassFileTransformer {

    private final String targetClassName;

    public PrintMethodCostTransformer(String targetClassName) {
        this.targetClassName = targetClassName;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        try {
            if (className != null) {
                className = className.replaceAll("/", ".");

                if (className.contains(targetClassName)) {
                    ClassPool classPool = ClassPool.getDefault();
                    classPool.insertClassPath(className);
                    CtClass ctClass = classPool.get(className);
                    CtMethod[] methods = ctClass.getDeclaredMethods();
                    if (methods != null && methods.length > 0) {
                        for (CtMethod ctMethod : methods) {
                            String methodName = ctMethod.getName();
                            if (!"main".equals(methodName)) {
                                //修改字节码,定义两个long类型变量
                                ctMethod.addLocalVariable("begin", CtClass.longType);
                                ctMethod.addLocalVariable("end", CtClass.longType);
                                //方法执行前操作
                                ctMethod.insertBefore("System.out.println(\"进入 ["+methodName+"] 方法\");");
                                ctMethod.insertBefore("begin = System.nanoTime();");
                                //方法执行后操作
                                ctMethod.insertAfter("end = System.nanoTime();");
                                ctMethod.insertAfter("System.out.println(\"方法 [" + methodName + "] 耗时:\"+ (end - begin) +\"ns\");");
                                ctMethod.insertAfter("System.out.println(\"退出 ["+methodName+"] 方法\");");
                            }
                        }
                        ctClass.detach();
                        return ctClass.toBytecode();
                    }
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return new byte[0];
    }
}
  1. 定义一个agent代理类AgentClass
代码语言:javascript
AI代码解释
复制
/**
 * @author 索码理(suncodernote)
 */
public class AgentClass {

    // main方法执行前的修改
    public static void premain(String agentArgs , Instrumentation inst){
        System.out.println("enter premain(String agentArgs , Instrumentation inst)");
        inst.addTransformer(new PrintMethodCostTransformer(agentArgs));
    }
}

修改配置文件并打包

4.1 在META-INF文件夹下文件夹下创建一个MANIFEST.MF文件

文件内容如下

代码语言:javascript
AI代码解释
复制
Manifest-Version: 1.0
Created-By: 索码理(suncodernote)
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.agent.AgentClass

4.2 在修改pom.xml,指定MANIFEST.MF文件覆盖掉自动生成的MANIFEST.MF文件。

代码语言:javascript
AI代码解释
复制
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
        </archive>
    </configuration>
</plugin>

最后通过maven install打包成一个jar包,我这里打成的jar包完成路径为D:\Program Files\apache-maven-3.6.3\repository\BasicJava\basic_java_demos\1.0-SNAPSHOT\basic_java_demos-1.0-SNAPSHOT.jar。一个简单的静态加载就完成了,接下来进行测试。

静态加载测试

  1. 新建一个maven项目,并新建一个测试类

MainTest每隔两秒钟调用一次print方法

代码语言:javascript
AI代码解释
复制
public class MainTest {

    public static void main(String[] args) throws InterruptedException {
        while (true){
            print("agents");
            TimeUnit.SECONDS.sleep(2);
        }
    }

    private static void print(String name){
        System.out.println("时间:"+LocalDateTime.now()+","+"hello "+name);
    }
}
  1. 在MainTest测试类添加启动参数
代码语言:javascript
AI代码解释
复制
-javaagent:"D:\Program Files\apache-maven-3.6.3\repository\BasicJava\basic_java_demos\1.0-SNAPSHOT\basic_java_demos-1.0-SNAPSHOT.jar"=com.example.jvmlearing.agent.MainTest

上面这段代码是指定了上面打包的agent jar包的完成路径,并通过-javaagent命令向agent包传递参数com.example.jvmlearing.agent.MainTest

-Xbootclasspath/a:D:\javassist-3.28.0-GA.jar是引用javassist包,当然也可以通过maven引入到项目中

  1. 启动MainTest进行测试

控制台打印结果:

从结果中可以看到premain方法是最先启动的,而且打印了方法耗时。

动态加载

将Java代理加载到已经运行的JVM中的过程称为动态加载。它利用的是Java的Attach API,Attach API不是Java标准API,而是Sun公司提供的一套扩展API,用来向目标JVM”依附”(Attach)代理工具程序的。Attach 机制对外提供了一种进程间的通信能力,能让一个进程传递命令给 JVM;其次,Attach 机制内置一些重要功能,可供外部进程调用。比如 jstack 工具就是要依赖这个机制来工作的。

动态加载的入口是agentmain方法,同样它也有两个重载方法

代码语言:javascript
AI代码解释
复制
public static void agentmain(String agentArgs, Instrumentation inst)
public static void agentmain(String agentArgs)

premain方法一样,JVM 会优先加载带 Instrumentation 对象参数的方法,加载成功忽略第二种;如果第一种没有,则加载第二种方法。

举个例子,简单玩一下动态加载:

  1. 在刚刚的java agent项目的AgentClass中加入agentmain方法
代码语言:javascript
AI代码解释
复制
public class AgentClass {

    // main方法执行前的修改
    public static void premain(String agentArgs , Instrumentation inst){
        System.out.println("enter premain(String agentArgs , Instrumentation inst)");
        inst.addTransformer(new PrintMethodCostTransformer(agentArgs));
    }

    // 控制类运行时的行为
    public static void agentmain(String agentArgs , Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
        System.out.println("enter agentmain(String agentArgs , Instrumentation inst)");
        inst.addTransformer(new PrintMethodCostTransformer(agentArgs) , true);

        Class<?> claz = Class.forName(agentArgs);
        //要重新定义的类
        inst.retransformClasses(claz);
    }
}
  1. MANIFEST.MF文件中加入agent-class
  1. 重新再打包

动态加载测试

  1. 在刚刚测试的项目中引入本地依赖tools.jar
代码语言:javascript
AI代码解释
复制
<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8.0</version>
    <scope>system</scope>
    <systemPath>${JAVA_HOME}\lib\tools.jar</systemPath>
</dependency>
  1. 新建一个测试类AttachTest
代码语言:javascript
AI代码解释
复制
public class AttachTest {
    public static void main(String[] args) throws AgentLoadException, IOException, AgentInitializationException, AttachNotSupportedException, InterruptedException {
        //传递给java代理的参数
        String targetClassName = "com.example.jvmlearing.agent.MainTest";
        String jar = "D:\\Program Files\\apache-maven-3.6.3\\repository\\BasicJava\\basic_java_demos\\1.0-SNAPSHOT\\basic_java_demos-1.0-SNAPSHOT.jar";
        for (VirtualMachineDescriptor descriptor: VirtualMachine.list()) {
            String displayName = descriptor.displayName();
            if (displayName.equals(targetClassName)) {
                String processId = descriptor.id();
                System.out.println("process id="+processId);
                VirtualMachine virtualMachine = VirtualMachine.attach(processId);
                //加载代理jar
                virtualMachine.loadAgent(jar, targetClassName);
                //解除绑定
                virtualMachine.detach();
            }
        }
    }
}
  1. 删除测试类MainTest的启动参数并先后启动MainTest和AttachTest

可以看到agentmain方法是在MainTest运行时执行的。

启动AttachTest之后,可以看到AttachTest的控制台打印的MainTest的进程id

再通过jps命令看下MainTest的进程id

可以看到两个进程id是一样的,也说明动态加载依附的是虚拟机进程。

Attach API 很简单,只有2个主要的类,都在 com.sun.tools.attach 包里面:

  • VirtualMachine类:代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了获取系统信息(比如内存dump、线程dump,类信息统计)、加载代理程序、Attach 和 Detach 等方法 。
  • VirtualMachineDescriptor类:一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

通过VirtualMachine类的attach(pid)方法,便可以attach到一个运行中的java进程上,之后便可以通loadAgent(agentJarPath)来将agent的jar包注入到对应的进程,然后对应的进程会调用agentmain方法。

静态加载和动态加载的区别

从上面的例子中可以发现静态加载是需要和被代理的程序一起启动,需要在启动的时候通过-javaagent参数指定静态加载的jar包,被代理的程序是“知道”自己被代理的。而动态加载则需要被代理程序先启动,只要获取到被代理程序的进程id,通过loadAgent方法指定动态加载jar包就行了,它属于热插拔式的。

总结

本篇文章我们分别使用Java Agent的静态加载和动态加载成功的对字节码进行了修改、追踪,并完成了一个打印方法耗时的简单示例。Java Agent能够访问加载到JVM中的类,它的应用十分广泛,可用于实现Java IDE的调试功能、热部署功能、线上诊断⼯具和性能分析⼯具。本篇只是触及了Java Agent的皮毛,感兴趣的可以深入了解一下。下篇文章将介绍一个动态追踪框架BTrace。

参考资料: https://www.baeldung.com/java-instrumentation https://www.developer.com/design/what-is-java-agent/ https://www.jianshu.com/p/6967d4dfbc49

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 索码理 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
99%的Java程序员不知道的Java Instrument-IDEA 破解的原理
Java Instrumentation API 是一个强大的工具,它允许开发人员在运行时修改字节码,而无需重新编译或修改源代码。这对于性能监控、日志记录、安全审计等场景非常有用。本文将深入探讨Java Instrumentation的基础知识,并通过具体的代码示例来展示如何使用-javaagent选项以及premain和agentmain方法来实现一些实用的功能。
井九
2024/10/12
2660
99%的Java程序员不知道的Java Instrument-IDEA 破解的原理
谈谈Java Agent技术的实现
Java Agent本质上可以理解为一个插件,该插件就是一个精心提供的Jar包,这个Jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。
FB客服
2022/11/14
4950
谈谈Java Agent技术的实现
实现一个javaagent需要几步?
在介绍javaagent之前,我想有必要向大家介绍一下JVMTI,因为javaagent是基于这个技术实现的
tnt阿信
2021/11/11
8890
实现一个javaagent需要几步?
深入理解Instrument(一)
很早之前就了解到目前主流的APM开源框架如Pinpoint、SkyWalking等等都是通过java.lang.instrument包提供的字节码增强功能来实现的。趁着对这块的热情还没消退,抽时间分析一下java.lang.instrument包的使用方式,记录下来写成一个系列的文章。本系列博文针对的是JDK11,其他版本的JDK可能不适合。
Throwable
2020/06/23
3.2K0
深入理解Instrument(一)
基于javaagent监控方法执行耗时
背景描述 javaagent是在JDK5之后提供的新特性,也可以叫java代理。开发者通过这种机制(Instrumentation)可以在加载class文件之前修改方法的字节码(此时字节码尚未加入JVM),动态更改类方法实现AOP,提供监控服务如;方法调用时长、可用率、内存等。
小傅哥
2020/07/14
2K0
基于javaagent监控方法执行耗时
Java-Agent 实现字节码热替换
正在运行中的 SpringBoot 项目需要在不停机的情况下,针对某个 Aop 切面的方法体内容进行修改,需要执行字节码替换的类为:
浪漫主义狗
2024/08/07
2310
Java 调试工具、热部署、JVM 监控工具都用到了它
我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着。但其实我们一直在用它,而且接触的机会非常多。下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了。
猿天地
2019/09/25
1.1K0
Java 调试工具、热部署、JVM 监控工具都用到了它
agentmain | JVM运行时的代码增强
在前文中,介绍了Instrumentation中的premain功能, 这次再一起看下它的agentmain功能.
一个架构师
2022/06/27
9600
agentmain | JVM运行时的代码增强
JAVA安全之Java Agent打内存马
Java Agent是一种特殊的Java程序,它允许开发者在Java虚拟机(JVM)启动时或运行期间通过java.lang.instrument包提供的Java标准接口进行代码插桩,从而实现在Java应用程序类加载和运行期间动态修改已加载或者未加载的类,包括类的属性、方法等,而Java Agent内存马的实现便是利用了这一特性使其动态修改特定类的特定方法将我们的恶意方法添加进去
Al1ex
2025/01/07
2990
JAVA安全之Java Agent打内存马
Java Agent内存马演变历史
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,可以让我们动态修改已加载或者未加载的类,包括类的属性和方法。
JDArmy
2022/11/14
1.7K0
Java Agent内存马演变历史
探秘Java:“润物细无声”的Java Agent
  在日常开发当中我们经常会需要编写一些和业务相关性不高的监测代码,比如方法出入口处的日志打印、方法执行耗时统计等。对于Java程序来说,最方便不过的就是使用Spring当中的AOP来完成对应的监测程序编写。那么在Spring框架诞生之前,一个纯粹的Java应用程序应该如何编写相应的监测程序呢?下面就来介绍一个JDK自带的工具—— Java Agent 。
闲宇非鱼
2022/02/08
1K0
探秘Java:“润物细无声”的Java Agent
IDEA + maven 零基础构建 java agent 项目
Java Agent(java 探针)虽说在 jdk1.5 之后就有了,但是对于绝大多数的业务开发 javaer 来说,这个东西还是比较神奇和陌生的;虽说在实际的业务开发中,很少会涉及到 agent 开发,但是每个 java 开发都用过,比如使用 idea 写了个 HelloWorld.java,并运行一下, 仔细看控制台输出
一灰灰blog
2020/03/20
2.4K0
IDEA + maven 零基础构建 java agent 项目
Java Agent入门实战(三)-JVM Attach原理与使用
之前的permain方法只能在java程序启动之前执行,并不能程序启动之后再执行,但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了instrument的应用。而Java SE 6的新特性改变了这种情况,可以通过Java Tool API中的attach方式来达到这种程序启动之后设置代理的效果。
蒋老湿
2020/01/15
5.8K0
Java高级用法,写个代理侵入你 ?
大家好,我是小菜。一个希望能够成为 吹着牛X谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤单!
蔡不菜丶
2022/09/21
5600
Java高级用法,写个代理侵入你 ?
字节码调试的入口 —— JVM 的寄生插件 javaagent 那些事
Java Instrumentation 这个技术看起来非常神秘,很少有书会详细介绍。但是有很多工具是基于 Instrumentation 来实现的:
架构狂人
2023/08/16
1.1K0
字节码调试的入口 —— JVM 的寄生插件 javaagent 那些事
Java Agent 简介
Java Agent 这个技术出现在 JDK1.5 之后,对于大多数人来说都比较陌生,但是多多少少又接触过,实际上,我们平时用的很多工具,都是基于 Java Agent 实现的,例如常见的热部署 JRebel,各种线上诊断工具(Btrace, Greys),还有阿里开源的 Arthas。
JMCui
2020/03/26
1.1K0
Java Agent(一)、初步认识Instrumentation
Instrumentation 是 Java SE 5 引入的一套 API,它允许开发者在运行时修改类的字节码。Java Instrumentation 可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。Java.lang.instrument包的最大功能就是可以在已有的类上附加(修改)字节码来实现增强的逻辑,它最常见的用途包括:
有一只柴犬
2024/12/10
6270
Java Agent(一)、初步认识Instrumentation
Java Agent (JVM Instrumentation 机制) 极简教程
Java 代理 (agent) 是在你的main方法前的一个拦截器 (interceptor),也就是在main方法执行之前,执行agent的代码。
一个会写诗的程序员
2021/04/15
11K0
javaAgent通过字节码对方法增强和使用 byte-buddy 来实现类的增强
在上一篇讲述了入门和实操https://cloud.tencent.com/developer/article/2360594 本章节使用字节码和byte-buddy来玩
杨不易呀
2023/11/19
1.5K0
javaAgent通过字节码对方法增强和使用 byte-buddy 来实现类的增强
浅谈 Java Agent 内存马
https://www.yuque.com/tianxiadamutou/zcfd4v/tdvszq
亿人安全
2022/06/30
2.9K0
浅谈 Java Agent 内存马
推荐阅读
相关推荐
99%的Java程序员不知道的Java Instrument-IDEA 破解的原理
更多 >
LV.0
这个人很懒,什么都没有留下~
作者相关精选
交个朋友
加入腾讯云官网粉丝站
双11活动抢先看 更有社群专属礼券掉落
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档