文章采集调用(使用Java的Instrumentation接口(java.lang.instrument)(组图) )
优采云 发布时间: 2022-01-16 20:24文章采集调用(使用Java的Instrumentation接口(java.lang.instrument)(组图)
)
前言
在分布式链路跟踪中,为了获取服务之间的调用链信息,采集器通常需要在方法前后进行埋藏。在Java生态系统中,常见的埋点方式有两种:
依靠SDK手动埋点;使用Java Agent技术做非侵入式埋葬。
著名的分布式监控系统是由 Zipkin 开创的。最经典的就是了解X-B3 Ttrace协议,使用Brave SDK,手动埋点生成Trace。但是 SDK 中埋点的方式对业务代码是有侵入性的。升级埋点时,必须进行代码更改。
那么如何将其与业务逻辑解绑呢?
Java还提供了另一种方式:依靠Java Agent技术对目标方法的字节码进行修改,从而实现非侵入式埋葬。这种使用 Java 代理的 采集器 方式也称为探针。在应用启动时使用-javaagent参数,或者在运行时使用attach(pid)方法,可以将探针包注入到目标应用中,完成埋点的植入。以对业务代码无侵入的方式,实现不敏感的热升级。用户无需了解深层原理,即可使用完整的监控服务
Java代理介绍
Java Agent 是 Java 1.5 版本之后引入的一个特性。它的主要作用是在类加载之前对其进行拦截,并且已经插入到我们的监控字节码中。代理是使用 Java 的 Instrumentation 接口 (java.lang.instrument) 编写的。
基本思路是在JVM启动时添加一个代理(Java Agent),每个代理是一个Jar包,在MANIFEST.MF文件中指定代理类,代理类收录一个premain方法。JVM在类加载的时候会先执行代理类的premain方法,然后再执行Java程序本身的main方法,这就是premain名的来源。在premain方法中,可以修改加载前的类文件。
这种机制可以认为是虚拟机级别的AOP,无需对原创应用程序进行任何修改即可实现类的动态修改和增强。
从 JDK 1.6 开始,支持更强大的动态 Instruments,在 JVM 启动后通过 Attach(pid) 远程加载。
注意:
无论 Agent 是用 Native 方式还是 Java Instrumentation 接口方式编写的,它们的工作都是在 JVMTI 的帮助下完成的。 JVMTI 是一组 Native 接口。在 Java 1.5 之前,Agent 只能通过编写 Native 代码来实现。
Java Instrumentation 核心方法
Instrumentation 是 java.lang.instrument 包下的一个接口。该接口的方法提供了注册类文件转换器、获取所有加载的类等功能,允许我们修改加载和卸载的类来实现AOP、性能监控等功能。
常用方法:
/**
* 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 对JVM已经加载的类重新触发类加载
*/
void retransformClasses(Class... classes) throws UnmodifiableClassException;
/**
* 获取当前 JVM 加载的所有类对象
*/
Class[] getAllLoadedClasses()
它的 addTransformer 为 Instrumentation 注册了一个转换器。Transformer 是 ClassFileTransformer 接口的一个实例。这个接口只有一个转换方法。调用addTransformer设置transformer后,后续JVM会在加载所有类之前被这个transform方法拦截。此方法接收原创类文件。字节数组,返回转换后的字节数组,可以在这个方法中做任何类文件的重写。
public class MyClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classBytes) throws IllegalClassFormatException {
// 在这里读取、转换类文件
return classBytes;
}
}
Java Agent 核心流程
Java Agent加载时序图(premain):
类加载时序图:
Java 代理使用的 Instrumentation 取决于 JVMTI 实现。当然也可以绕过Instrumentation,直接使用JVMTI来实现Agent。JVMTI 和 JDI 构成了 Java 平台调试架构 (JPDA) 的主要功能。
Java 代理使用
Java Agent 实际上是一个特殊的 Jar 包。它不能单独启动,而必须附加到 JVM 进程。可以看成是JVM的寄生插件。它使用 Instrumentation API 来读取和重写当前 JVM 的类。文件,并通过 -javaagent:xxx.jar 导入目标应用程序。
那么这个Jar和普通Jar有什么区别呢?
Agent需要打包成jar包,在Maininfe.MF属性中指定“Premain-Class”或“Agent-Class”,根据需求定义Can-Redefine-Classes和Can-Retransform-Classes。
Java Agent Jar包MANIFEST.MF配置参数:
Manifest-Version: 1.0
#动态 agent 类
Agent-Class: com.zuozewei.javaagent01.Agent
#静态 agent 类
Premain-Class: com.zuozewei.javaagent01.Agent
是否允许重复装载
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_112
演示预览
1、创建POM项目Java Agent,项目结构如下:
2、修改pom文件:
parent
com.zuozewei
1.0-SNAPSHOT
4.0.0
jar
javaagent01
agent
org.apache.maven.plugins
maven-jar-plugin
2.3.1
src/main/resources/META-INF/MANIFEST.MF
maven-assembly-plugin
${basedir}
true
true
com.zuozewei.javaagent01.Agent
jar-with-dependencies
org.apache.maven.plugins
maven-compiler-plugin
1.7
1.7
3、创建一个AgentMain类实现控制台打印,添加Transformer为Instrumentation注册一个transformer。
package com.zuozewei.javaagent01;
import java.lang.instrument.Instrumentation;
public class Agent {
// public static void premain(String agentArgs) {
// System.out.println("我是一个萌萌哒的 Java Agent");
// try {
// Thread.sleep(2000L);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
public static void premain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new ClassFileTransformerDemo());
System.out.println("7DGroup Java Agent");
}
}
4、创建一个ClassFileTransformerDemo类,截取并打印所有类名。
package com.zuozewei.javaagent01;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ClassFileTransformerDemo implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
System.out.println("className: " + className);
if (!className.equalsIgnoreCase("com/zuozewei/Dog")) {
return null;
}
return getBytesFromFile("/Users/zuozewei/IdeaProjects/javaagent/example01/target/classes/com/zuozewei/Dog.class");
}
public static byte[] getBytesFromFile(String fileName) {
File file = new File(fileName);
try (InputStream is = new FileInputStream(file)) {
// precondition
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset = 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
}
5、定义需要修改的项目example01
6、需要修改的类的实现。
主要的:
package com.zuozewei;
public class Main {
public static void main(String[] args) {
System.out.println("7DGroup");
System.out.println(new Dog().hello());
// System.out.println(new Cat().hello());
}
}
狗:
package com.zuozewei;
public class Dog {
public int hello() {
return 0;
}
}
7、运行example01的main方法:
8、将javaagent项目打包生成jar文件,将java文件与上图example01项目的jar放在同一目录下(方便执行放在同一目录下)
执行以下命令:
java -jar -javaagent:agent.jar example.jar
我们的函数实现了,执行结果如下:
总结
本文详细介绍了Java Agent启动加载实现字节码增强关键技术的实现细节。字节码增强技术为测试人员监控性能提供了新思路。目前很多开源监控产品都提供了丰富的Java探针库。作为监控服务商,开发成本进一步降低,但开发门槛较高,对测试人员的学习成本有很大的影响。
源地址: