前言
Java Agent基于字节码增强技术研发,支持自动埋点完成数据上报,Java Agent包含(并二次分发)opentelemetry-java-instrumentation CNCF的开源代码,遵循Apache License 2.0协议,在Java Agent包中对opentelemetry License进行了引用。
OpenTelemetry是工具、API 和 SDK 的集合。使用它来检测、生成、收集和导出遥测数据(指标、日志和跟踪),以帮助您分析软件的性能和行为。OpenTelemetry社区活跃,技术更迭迅速,广泛兼容主流编程语言、组件与框架,为云原生微服务以及容器架构的链路追踪能力广受欢迎。通过对Java字节码的增强技术OpenTelemetry-java-instrumentation可以实现自动埋点上报数据。
一、什么是java Agent
Java Agent 直译为 Java 代理,中文圈也流行另外一个称呼 Java 探针 Probe 技术。
它在 JDK1.5 引入,是一种可以动态修改 Java 字节码的技术。Java 类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,并且通过字节码转换器ClassFileTransformer 对这些字节码进行修改,以此来完成一些额外的功能。Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序的 JVM 进程,进行工作
//Java Agent 和目标进程一起启动模式
nohup java -jar -Dspring.profiles.active=prod -DworkId=1 -javaagent:/opentelemetry-javaagent.jar -Dfile.encoding=utf-8 -Xms4028M -Xmx4028M -XX:PermSize=512M -XX:MaxPermSize=512M /launch-adapter-0.0.1-SNAPSHOT.jar >/dev/null 2>&1 &
Agent 启动拦截提供两种方式:一种是程序运行前:在Main方法执行之前,通过一个叫 premain方法来执行
启动时需要在目标程序的启动参数中添加 -javaagent参数,Java Agent 内部通过注册 ClassFileTransformer ,这个转化器在Java 程序 Main方法前加了一层拦截器。在类加载之前,完成对字节码修改
Java Agent 具备以下的能力
• Java Agent 能够在加载 Java 字节码之前拦截并对字节码进行修改;
• Java Agent 能够在 Jvm 运行期间修改已经加载的字节码;
Java Agent 的价值
• IDE 的调试功能,例如 Eclipse、IntelliJ IDEA
• 热部署功能,例如 JRebel、XRebel、spring-loaded
• 各种线上诊断工具,例如 Btrace、Greys,国内阿里的 Arthas
• 各种性能分析工具,例如 Visual VM、JConsole 等
• 全链路性能检测工具,例如 OpenTelemetry、Skywalking、Pinpoint等 。
二、java Agent简单实现
我们先实现一个简单的Java Agent的栗子
引入javassist
在编写类转化器时,我们通过Javassist 来具体操作字节码,首先pom.xml
里面添加依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency
实现premain方法
程序启动时,优先加载Java Agent,执行里面的 premain
方法。这个时候,其实大部分的类没有被加载。
import java.lang.instrument.Instrumentation;
public class MyAgent {
/**
* jvm 参数形式启动,运行此方法
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain");
customLogic(inst);
}
/**
* 动态 attach 方式启动,运行此方法
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agentmain");
customLogic(inst);
}
/**
* 打印所有已加载的类名称
* 修改字节码
* @param inst
*/
private static void customLogic(Instrumentation inst){
inst.addTransformer(new MyTransformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for(Class cls :classes){
System.out.println(cls.getName());
}
}
}
实现ClassFileTransformer
ClassFileTransformer提供了tranform()方法,用于对加载的类进行增强重定义,返回新的类字节码流。
Instrumentation 有一个TransformerInfo 数组保存ClassFileTransformer,像拦截器链表一样,顺序的进行字节码的重定义。
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("正在加载类:" + className);
if (!className.contains("Person")) {
return classfileBuffer;
}
CtClass cl = null;
try {
ClassPool classPool = ClassPool.getDefault();
cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod ctMethod = cl.getDeclaredMethod("test");
System.out.println("获取方法名称:" + ctMethod.getName());
ctMethod.insertBefore("System.out.println(\" 动态插入的打印语句 \");");
ctMethod.insertAfter("System.out.println($_);");
byte[] transformed = cl.toBytecode();
return transformed;
} catch (Exception e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
MANIFEST.MF文件配置
在 MANIFEST.MF
文件中定义Premain-Class
属性,指定一个实现类。类中实现了Premain方法,这就是Java Agent 在类加载启动入口
Manifest-Version: 1.0
Created-By: yyp
Agent-Class: org.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: org.example.MyAgent
-
Premain-Class
包含Premain方法的类 -
Can-Redefine-Classes
为true时表示能够重新定义Class -
Can-Retransform-Classes
为true时表示能够重新转换Class,实现字节码替换
打包和运行
POM打包
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>
运行:
java -javaagent:MyCustomAgent-1.0-SNAPSHOT-jar-with-dependencies.jar -jar myTest-1.0-SNAPSHOT.jar
正在加载类:java/util/regex/Pattern$Ques
正在加载类:java/util/regex/Pattern$Loop
正在加载类:java/util/regex/Pattern$Prolog
1
正在加载类:org/example/Person
获取方法名称:test
动态插入的打印语句
执行测试方法
I'm ok
程序等价于:指定Java类下所有方法进行了如下转换,重新生成字节码加载执行
项目结构图
参考
Java技术专题-Java Agent探针的技术介绍(1) - 简书
深入Java自动化探针技术的原理和实践_java 探针原理-CSDN博客
Java 动态调试技术原理及实践 - 美团技术团队