前言
分布式链路追踪中为了获取服务之间调用链信息,采集器通常需要在方法的前后做埋点。在 Java 生态中,常见的埋点方式有两种:
- 依赖 SDK 手动埋点;
- 利用 Java Agent 技术来做无侵入埋点。
我们所熟知的分布式监控系统,是 Zipkin 开始的,最经典的是搞懂 X-B3 Ttrace 协议,使用 Brave SDK,手动埋点生成 Trace。但是 SDK 埋点的方式,对业务代码存在侵入性,当升级埋点时,必须要做代码的变更。
那么如何和业务逻辑解绑呢? ? Java 还提供了另外一种方式:依赖 Java Agent 技术,修改目标方法的字节码,做到无侵入的埋点。这种利用 Java Agent 的方式的采集器,也叫做探针。在应用程序启动时使用 -javaagent 参数 ,或者运行时使用 attach(pid) 方式,就可以将探针包注入目标应用程序,完成埋点的植入。对业务代码无侵入的方式,可以做到无感的热升级。用户不需要理解深层的原理,就可以使用完整的监控服务
关于字节码的基础知识可以参考美团的这篇文章:
Java Agent 简介
Java Agent 是 Java 1.5 版本之后引?的特性,其主要作?是在 class 被加载之前对其拦截,已插?我们的监听字节码。使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。
基本的思路是在 JVM 启动的时候添加一个代理(Java Agent),每个代理是一个 Jar 包,其 MANIFEST.MF 文件里指定了代理类,这个代理类包含一个 premain 方法。JVM 在类加载时候会先执行代理类的 premain 方法,再执行 Java 程序本身的 main 方法,这就是 premain 名字的来源。在 premain 方法中可以对加载前的 class 文件进行修改。
这种机制可以认为是虚拟机级别的 AOP,无需对原有应用做任何修改,就可以实现类的动态修改和增强。
从 JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach(pid) 远程加载。 ? 注意:
无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。JVMTI 是一套 Native 接口,在 Java 1.5 之前,要实现一个 Agent 只能通过编写Native 代码来实现。
Java Instrumentation 核心方法
Instrumentation 是 java.lang.instrument 包下的一个接口,这个接口的方法提供了注册类文件转换器、获取所有已加载的类等功能,允许我们在对已加载和未加载的类进行修改,实现 AOP、性能监控等功能。
常用方法:
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
Class[] getAllLoadedClasses()
它的 addTransformer 给 Instrumentation 注册一个 transformer,transformer 是 ClassFileTransformer 接口的实例,这个接口就只有一个 transform 方法,调用 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): ?
Class 装载时序图:
Java Agent 所使用的 Instrumentation 依赖 JVMTI 实现,当然也可以绕过 Instrumentation 直接使用 JVMTI 实现 Agent。JVMTI 与 JDI 组成了 Java 平台调试体系(JPDA)的主要能力。
Java Agent 使?
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-Class: com.zuozewei.javaagent01.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
demo 预演
1、创建 POM 项目 Java Agent,项目结构如下:
2、修改 pom 文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>com.zuozewei</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>javaagent01</artifactId>
<build>
<finalName>agent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<outputDirectory>${basedir}</outputDirectory>
<archive>
<index>true</index>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.zuozewei.javaagent01.Agent</Premain-Class>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
?
3、创建 AgentMain 类,实现控制台打印,addTransformer 给 Instrumentation 注册一个 transformer。
package com.zuozewei.javaagent01;
import java.lang.instrument.Instrumentation;
public class Agent {
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)) {
long length = file.length();
byte[] bytes = new byte[(int) length];
int offset = 0;
int numRead = 0;
while (offset <bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - 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、实现需要修改的类的。
main:
package com.zuozewei;
public class Main {
public static void main(String[] args) {
System.out.println("7DGroup");
System.out.println(new Dog().hello());
}
}
Dog:
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 探针库,作为监控服务的提供者,进一步降低了开发成本,不过开发门槛比较高,对测试人员来说有很大的一部分的学习成本。
源码地址:
|