赞
踩
分布式链路追踪中为了获取服务之间调用链信息,采集器通常需要在方法的前后做埋点。在 Java 生态中,常见的埋点方式有两种:
我们所熟知的分布式监控系统,是 Zipkin 开始的,最经典的是搞懂 X-B3 Ttrace 协议,使用 Brave SDK,手动埋点生成 Trace。但是 SDK 埋点的方式,对业务代码存在侵入性,当升级埋点时,必须要做代码的变更。
那么如何和业务逻辑解绑呢?
Java 还提供了另外一种方式:依赖 Java Agent 技术,修改目标方法的字节码,做到无侵入的埋点。这种利用 Java Agent 的方式的采集器,也叫做探针。在应用程序启动时使用 -javaagent 参数 ,或者运行时使用 attach(pid) 方式,就可以将探针包注入目标应用程序,完成埋点的植入。对业务代码无侵入的方式,可以做到无感的热升级。用户不需要理解深层的原理,就可以使用完整的监控服务
关于字节码的基础知识可以参考美团的这篇文章:
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 代码来实现。
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,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 装载时序图(premain):
Class 装载时序图:
Java Agent 所使用的 Instrumentation 依赖 JVMTI 实现,当然也可以绕过 Instrumentation 直接使用 JVMTI 实现 Agent。JVMTI 与 JDI 组成了 Java 平台调试体系(JPDA)的主要能力。
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 文件:
<?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) { // 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 <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());
// System.out.println(new Cat().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 探针库,作为监控服务的提供者,进一步降低了开发成本,不过开发门槛比较高,对测试人员来说有很大的一部分的学习成本。
源码地址:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。