在运维监控系统开发过程中我们往往需要在特定的方法出入口进行记录日志、采集参数,甚至在特定场景下需要对方法的出入参数或者整个方法逻辑进行重写。目前市面上开源的APM系统主要有CAT、Zipkin、Pinpoint、SkyWalking,大都是参考Google的 Dapper 实现的。个人在项目中主要使用skywallking,在java、golang等多语言中有过实践,所以这里主要记录自己在java项目中使用skywallking的一些心得和笔记。
Skywalking介绍
Skywalking是一个国产的开源框架,2015年有吴晟个人开源,2017年加入Apache孵化器,国人开源的产品,主要开发人员来自于华为,2019年4月17日Apache董事会批准SkyWalking成为顶级项目,支持Java、.Net、NodeJs等探针,数据存储支持Mysql、Elasticsearch等,跟Pinpoint一样采用字节码注入的方式实现代码的无侵入,探针采集数据粒度粗,但性能表现优秀,且对云原生支持,目前增长势头强劲,社区活跃。
Skywalking是分布式系统的应用程序性能监视工具,专为微服务,云原生架构和基于容器(Docker,K8S,Mesos)架构而设计,它是一款优秀的APM(Application Performance Management)工具,包括了分布式追踪,性能指标分析和服务依赖分析等。
Skywallking通过集成开源的Bytebuddy来实现对特定类的方法、字段等实现字节码修改,从而达到上下文传递、数据采集等功能,其底层是基于Java Instrumentation(jdk1.5+)技术实现。
Instrumentation介绍
从 JDK5 版本开始引入了java.lang.instrument 包,可以通过 addTransformer 方法设置一个 ClassFileTransformer,通过ClassFileTransformer 来实现类的字节码修改。
JDK 1.5 支持静态 Instrumentation,在 JVM 启动的时候通过 -javaagent:xxxx.jar的方式加载一个agent,该jar包含MANIFEST.MF 文件同时在里面指定代理类(Premain-class属性)和一个premain静态方法。JVM 启动时先执行代理类的 premain 方法完成transformer的注册,再执行 Java 程序本身的 main 方法运行程序。
JDK 1.6 开始支持更加强大的动态 Instrument,在JVM 启动后通过 Attach API 远程加载一个agent,和javaagent一样该jar需要包含MANIFEST.MF同时在里面制定代理类(Agent-class属性)和一个agentmain静态方法。JVM Attach成功后会通过进程间通信让目标jvm加载agent并且执行agentmain方法完成transformer的注册。
主要接口说明
/**
* 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码
void addTransformer(ClassFileTransformer transformer);
* 为 Instrumentation 注册一个类文件转换器,可以修改读取类文件字节码, 并指定是否支持重复转换
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
* 为 Instrumentation 移除一个类文件转换器
boolean removeTransformer(ClassFileTransformer transformer);
* 重复进行类文件转,修改读取类文件字节码
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
* 重新定义类信息
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
* 将jar包加入bootstrapClassLoader扫描范围,常用语加载外部jar
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
* 将jar包加入AppClassLoader扫描范围,常用于加载外部jar
void appendToSystemClassLoaderSearch(JarFile jarfile);
javaagent方式修改类
// Demo.class 需要运行的java文件
public class Demo {
public static void main(String[] args) {
new Demo().hello();
public void hello() {
helloBob();
helloArno();
public void helloBob() {
public void helloArno() {
// demoAgent.jar,包含DemoAgent代理类、MANIFEST.MF文件
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.commons.AdviceAdapter;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import static jdk.internal.org.objectweb.asm.Opcodes.V1_8;
* DemoAagent
public class DemoAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new DemoClassFileTransformer());
public static class DemoMethodAdapter extends AdviceAdapter {
private final String methodName;
protected DemoMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
methodName = name;
@Override
protected void onMethodEnter() {
super.onMethodEnter();
// 改写方法,在这里输出"hello xxxx"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("hello " + methodName);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
public static class DemoClassVisitor extends ClassVisitor {
public DemoClassVisitor(ClassVisitor classVisitor) {
super(V1_8, classVisitor);
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equalsIgnoreCase("helloBob") || name.equalsIgnoreCase("helloArno")) {
return new DemoMethodAdapter(V1_8, mv, access, name, descriptor);
return mv;
* 自定义ClassFileTransformer
public static class DemoClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// TODO 对字节码进行修改; 原始字节码为 classfileBuffer;
// 可以使用ASM、Javassist等字节码操作框架进行字节码修改操作, 这里使用jdk自带asm示例
if (!"Demo".equals(className)) return classfileBuffer;
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new DemoClassVisitor(cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
// 2. MANIFEST.MF内容示例
Manifest-Version: 1.0
Premain-Class: DemoAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
// 3. 运行程序
java -javaagent:demoAgent.jar Demo
java attach方式修改类
// 相比agent方式有三个不同
// 1. 启动命令中不在需要javaagent参数
// 2. agent类中premain方法需要切换成agentmian
// 3. 需要在一个新的进程中执行attach动作
* DemoAagent
public class DemoAgent {
// 这里改成agentmain提供给attach的方式执行
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
instrumentation.addTransformer(new DemoClassFileTransformer());
// 启动新进程执行attach动作
import com.sun.tools.attach.VirtualMachine;
* AttachMain
public class AttachMain {
// 这里改成agentmain提供给attach的方式执行
public static void main(String[]] args) {
// 需要attach的目标java进程
String pid = "";
// attach目标jvm,通过 jvm_pidxxxx 文件进行进程间通信
VirtualMachine vm = VirtualMachine.attach(pid);
// 发送信号,让目标jvm加载该agent,并执行agentmain方法
vm.loadAgent(new File("demoAgent.jar").getAbsolutePath());
// agent加载成功后,当前进程退出attach
vm.detach();
}
ByteBuddy使用
ByteBuddy主要通过自定义一套ClassFileTransformer机制降低字节码修改门槛。
ByteBuddy会根据不同修改方式生成不同的ClassFileTransformer,然后注册到Instrumentation中达到动态修改字节码的目的,Instrumentation有两种方式获得:
第一种由应用自行实现代理类,在类中实现premain/agentmain方法,在这两个方法中完成ByteBuddy类转换器注册。
第二种通过ByteBuddyAgent.install()方法由ByteBuddy自动生成临时agent jar文件(保存在java.io.tmpdir目录)并返回instrumentation。
注意:ByteBuddy实现的Agent容易和其他应用出现冲突,所以在maven打包时可以通过maven-shade-plugin直接将ByteBuddy的依赖和应用打包在同一个jar中,并且对包路径进行重定义。
参考地址: https://javadoc.io/doc/net.bytebuddy/byte-buddy#
// 1. 自行实现Agent
public class DemoAgent {
// javaagent 方式执行
public static void premain(String agentArgs, Instrumentation instrumentation) {
// attach api方式执行
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
public static void initByteBuddy(String agentArgs, Instrumentation instrumentation) {
// 这里实现ByteBuddy相关逻辑
// 2. 委托Bytebuddy生成
public class DemoAgent {
// javaagent 方式执行
public static void main(String[] args) {
// 业务逻辑 ....
// 初始化Bytebuddy
Instrumentation instrumentation = ByteBuddyAgent.install();
initByteBuddy(null, instrumentation);
// 业务逻辑.....
public static void initByteBuddy(String agentArgs, Instrumentation instrumentation) {
// 这里实现ByteBuddy相关逻辑
}
ByteBuddy使用示例
// Demo.class 需要运行的java文件
public class Demo {
public void hello() {
System.out.println("====我是原始方法====")
// 2. DemoAgent.java agent文件
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
import static net.bytebuddy.matcher.ElementMatchers.named;
* DemoAagent
public class DemoAgent {
// javaagent 方式执行
public static void main(String[] args) {
// 业务逻辑 ....
// 初始化Bytebuddy
Instrumentation instrumentation = ByteBuddyAgent.install();
initByteBuddy(null, instrumentation);
// 业务逻辑.....
public static void initByteBuddy(String agentArgs, Instrumentation instrumentation) {
AgentBuilder agentBuilder = new AgentBuilder.Default()
.disableClassFormatChanges()
// 设置需要忽略的类、方法等,支持多种ElementMatcher匹配模式
.ignore(nameStartsWith("net.bytebuddy.").or(nameStartsWith("java.")))
// 设置默认字节码修改策略,比如TypeStrategy、InjectionStrategy等
.with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
.with(AgentBuilder.TypeStrategy.Default.REDEFINE)
.with(AgentBuilder.InjectionStrategy.UsingUnsafe.INSTANCE)
.with(AgentBuilder.RedefinitionStrategy.REDEFINITION);
// 1. 通过Advice方式(ASM visitor方式),进行方法处理
AgentBuilder.Transformer demoTransform =
(builder, typeDescription, classLoader, module, protectionDomain) ->
builder.visit(Advice.to(DemoAdvice.class)
.on(ElementMatchers.isMethod().and(ElementMatchers.named("hello"))));
agentBuilder = agentBuilder.type(ElementMatchers.named("Demo")).transform(demoTransform);
// 2. 通过MethodDelegation(方法委托)方式,进行方法处理
AgentBuilder.Transformer demoTransform1 =
(builder, typeDescription, classLoader, module, protectionDomain) ->
builder.method((named("hello"))).intercept(MethodDelegation.to(DemoInterceptor.class));
agentBuilder = agentBuilder.type(ElementMatchers.named("Demo")).transform(demoTransform1);
// 将agentBuilder生成的ClassFileTransform注册到instrumentation中
agentBuilder.installOn(instrumentation);
public static class DemoInterceptor {
// 这里通过SuperCall注解将原方法调用注入
public void hello(@SuperCall Callable<?> superCall) throws Exception {
// 进入原方法前进行处理
System.out.println("进入方法: hello");
// 通过call方法对原方法发起调用
superCall.call();
// 原方法调用完成进行处理
System.out.println("退出方法:hello");
public static class DemoAdvice {
* 方法进入时执行
* @param method 方法
@Advice.OnMethodEnter
public static void enter(@Advice.This Object obj, @Advice.Origin Method method) {
System.out.println("进入方法: " + method.getName());
System.out.println("方法参数: " + Arrays.toString(method.getParameterTypes()));
* 方法退出时执行
* @param method 方法
* @param throwable 异常信息
@Advice.OnMethodExit
public static void exit(@Advice.Origin Method method, @Advice.Thrown Throwable throwable) {
System.out.println("退出方法: " + method.getName());
if (throwable != null) {
System.out.println("方法异常: " + throwable.getClass().getName());
}
ByteBuddy常用注解
注解 |
用途 |
示例 |
---|---|---|
@Argument |
绑定参数,序号从0开始 |
@Argument(0) Object param |
@AllArguments |
绑定所有参数的数组 |
@AllArguments Object[] params |
@This |
当前被拦截的、动态生成的那个对象,注入后会使得原方法被调用 |
@This Object obj |
@DefaultCall |
调用默认方法而非super的方法 |
|
@SuperCall |
用于调用父类版本的方法(原方法,不能修改参数) |
|
@RuntimeType |
可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查 |
|
@Super |
当前被拦截的、动态生成的那个对象的父类对象 |
|
@FieldValue |
注入被拦截对象的一个字段的值 |
|
@Morph |
允许调用指定超类方法 |
|
Maven打包插件示例
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<!-- 需要处理的包路径 -->
<include>net.bytebuddy:byte-buddy:jar:</include>
<include>net.bytebuddy:byte-buddy-agent:jar:</include>
</includes>
</artifactSet>
<relocations>
<!-- 重新定义包路径, 对net.bytebuddy的包路径重定义为shaded.net.bytebuddy -->
<relocation>
<pattern>net.bytebuddy</pattern>
<shadedPattern>shaded.net.bytebuddy</shadedPattern>
</relocation>
<relocation>
<!-- 建议agent所在的包也要shade -->
<pattern>demo.agent</pattern>
<shadedPattern>shaded.demo.agent</shadedPattern>