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

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

轻松带你学习java-agent的更多相关文章

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

    文章首发于[博客园-陈树义],点击跳转到原文<两道面试题,带你解析Java类加载机制> 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Gr ...

  2. 【转】两道面试题,带你解析Java类加载机制(类初始化方法 和 对象初始化方法)

    本文转自 https://www.cnblogs.com/chanshuyi/p/the_java_class_load_mechamism.html 关键语句 我们只知道有一个构造方法,但实际上Ja ...

  3. 带你解析Java类加载机制

      目录 Java类加载机制的七个阶段 加载 验证 准备(重点) 解析 初始化(重点) 使用 卸载 实战分析 方法论 树义有话说 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如 ...

  4. 你所不知道的库存超限做法 服务器一般达到多少qps比较好[转] JAVA格物致知基础篇:你所不知道的返回码 深入了解EntityFramework Core 2.1延迟加载(Lazy Loading) EntityFramework 6.x和EntityFramework Core关系映射中导航属性必须是public? 藏在正则表达式里的陷阱 两道面试题,带你解析Java类加载机制

    你所不知道的库存超限做法 在互联网企业中,限购的做法,多种多样,有的别出心裁,有的因循守旧,但是种种做法皆想达到的目的,无外乎几种,商品卖的完,系统抗的住,库存不超限.虽然短短数语,却有着说不完,道不 ...

  5. 程序员带你学习安卓开发,十天快速入-对比C#学习java语法

    关注今日头条-做全栈攻城狮,学代码也要读书,爱全栈,更爱生活.提供程序员技术及生活指导干货. 如果你真想学习,请评论学过的每篇文章,记录学习的痕迹. 请把所有教程文章中所提及的代码,最少敲写三遍,达到 ...

  6. 登录模块的进化史,带大家回顾java学习历程(二)

    接着前面的登录模块的进化史,带大家回顾java学习历程(一) 继续往下面讲 前面我们去实现登录功能,都是想着要完成这个功能,直接在处理实际业务的类中去开始写具体的代码一步步实现,也就是面向过程的编程. ...

  7. 学习java设计模式有用吗?懂这六个原则,编程更轻松

    学习java设计模式有用吗?懂这六个原则,编程更轻松 1.开闭原则(Open Close Principle) 开闭原则就是说对扩展开放,对修改关闭.在程序需要进行拓展的时候,不能去修改原有的代码,实 ...

  8. 零基础的人怎么学习Java

    编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...

  9. Java 调式、热部署、JVM 背后的支持者 Java Agent

    我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent 技术,看一下你就知道为什么了. -各个 J ...

  10. [转] Java Agent使用详解

    以下文章来源于古时的风筝 ,作者古时的风筝 我们平时写 Java Agent 的机会确实不多,也可以说几乎用不着.但其实我们一直在用它,而且接触的机会非常多.下面这些技术都使用了 Java Agent ...

随机推荐

  1. 为何 DevOps 会给开发人员带来压力和倦怠?

    企业正在享受 DevOps 实施带来的好处,但这也是有代价的.开发人员需要承担额外的责任,可能会导致他们感到疲惫不堪.因此我们可以采取一些方法来确保 DevOps 工程师的满意度. DevOps 的支 ...

  2. 如何借助python第三方库存取不同应用程序的用户名、密码

    在之前的一系列文章中,小爬分享了很多用Pywin32.uiAutomation.sap Gui Script等技术实现应用程序或者Web网站(如SAP.Excel.outLook邮件系统.OA系统)的 ...

  3. 一篇搞定Sentinel-搭建Spring Cloud Alibaba服务组件Sentinel实现服务资源控制

    1.Sentinel介绍 随着微服务的流行,服务和服务之间的稳定性变得越来越重要.Sentinel 是面向分布式.多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由.流量控制.流量整形 ...

  4. JAVA类的加载(3) ——类加载后能够有效运行

    例1: 1 package classobject; 2 3 import java.lang.reflect.Constructor; 4 import java.lang.reflect.Invo ...

  5. [Python急救站]密码判断

    用Python做一个密码判断,用户输入注册密码,需要6位以上,包含数字.大写字母.小写字母. import re a = re.compile('[a-z]') b = re.compile('[A- ...

  6. 微信支付:wxpay.unifiedOrder(data)返回appid 与 openId 不配

    原因:小程序和APP.公众号等支付方式夸端口调用支付,后台配置多个appId时 A程序中的openid 在B程序中支付.即使用A程序的openid和B程序的appIdy去调用wxpay.unified ...

  7. node 实现上传 和 下载 文件接口 简易版

    var express = require('express');var fs = require('fs');var path = require('path');var multipart = r ...

  8. .NET8 Blazor新特性 流式渲染

    什么是SSR Blazor中的流式渲染结合了SSR(服务端渲染),服务端将HTML拼好返回给前端,有点像我们熟知的Razor Pages 或 MVC . 当已经有了 Razor Pages 或 MVC ...

  9. Batrix企业能力库之物流交易域能力建设实践

    简介 Batrix企业能力库,是京东物流战略级项目-技术中台架构升级项目的基础底座.致力于建立企业级业务复用能力平台,依托能力复用业务框架Batrix,通过通用能力/扩展能力的定义及复用,灵活支持业务 ...

  10. 【Javaweb】Servlet十 | HttpServletResponse类和HttpServletRequest类

    HttpServletResponse类的作用 HttpServletResponse类和HttpServletRequest类一样.每次请求进来,Tomcat服务器都会创建一个Response对象传 ...