SkyWalking-3--Java Agent开发和集成示例
1、Java Agent的开发步骤说明
1、定义Agent类
创建一个包含premain方法的类,即:Agent类。
该类中的premain方法会在JVM启动时被调用,该方法的主要作用是注册类转换器ClassTransformer对象。
代码示例:
public class MyAgent {// JVM 启动时自动调用public static void premain(String args, Instrumentation inst) {// 添加类转换器inst.addTransformer(new MyClassTransformer());}
}
说明:
- premain是JVM启动时加载Agent的入口方法。
- Instrumentation inst参数由JVM提供,是操作类定义的核心工具。
- 通过inst.addTransformer(…)注册一个转换器,相当于告诉JVM:“后续加载的类,你可以让我先处理一下”。
2、实现ClassFileTransformer
在ClassFileTransformer的transform方法中,动态修改目标类的字节码。
代码示例:
public class MyClassTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) {// 1、判断是否是目标类if (!"target/MyService".equals(className)) {return null; // 不修改}// 2、使用ASM等工具修改字节码byte[] modified = modifyBytecode(classfileBuffer);// 3、返回修改后的字节码return modified;}
}
说明:
- transform()方法是字节码修改的“主战场”。
- 当JVM即将定义一个类(ClassLoader.defineClass)之前,会先调用所有注册的transform方法。
- 你可以在其中:
- 读取原始字节码(classfileBuffer)
- 使用ASM / Javassist / Byte Buddy修改字节码
- 返回修改后的byte[]
- 返回null表示不修改
3、打包Agent JAR
在MANIFEST.MF中指定Premain-Class。
MANIFEST.MF示例:
Manifest-Version: 1.0
Premain-Class: your.package.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
说明:
- Premain-Class:必须,指定包含premain方法的类
- Can-Redefine-Classes:允许重新定义类(如Arthas使用)
- Can-Retransform-Classes:允许重新转换类(关键!如果你要修改已加载的类,必须开启)
4、启动应用时加载Agent
使用-javaagent参数指定Agent JAR。
bash示例:
java -javaagent:my-agent.jar -jar my-app.jar
5、JVM调用流程
- 类加载时触发:当JVM加载类(ClassLoader.defineClass)时,会调用所有注册的ClassFileTransformer。
- 字节码修改:transform方法会修改源字节码文件,并返回新的字节数组被JVM使用,作为新的类定义。
- 类缓存:修改后的类会被缓存,后续直接使用修改后的版本。
总结:
Java Agent通过premain注册ClassFileTransformer,在类被JVM加载的瞬间修改其字节码,从而实现无侵入的代码增强。
2、Java Agent具体开发示例
1、目标
假设我们有一个MyService类。我们希望不修改源码,通过Java Agent在hello()方法执行前后自动插入日志。
Service代码示例:
public class MyService {public void hello() {System.out.println("Hello, World!");}
}
日志效果要求:
BEFORE: hello()
Hello, World!
AFTER: hello()
2、实现
1、方式一:使用ASM+Java Agent
1、项目结构
java-agent-demo/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── agent/
│ │ │ ├── MyAgent.java
│ │ │ └── LoggingClassVisitor.java
│ │ └── resources/
│ │ └── META-INF/
│ │ └── MANIFEST.MF
│ └── test/
│ └── java/
│ └── MyService.java
2、pom.xml(Maven)
<?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"><modelVersion>4.0.0</modelVersion><groupId>cn.demo</groupId><artifactId>java-agent-demo</artifactId><version>1.0.0</version><packaging>jar</packaging><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.6</version></dependency><dependency><groupId>org.ow2.asm</groupId><artifactId>asm-commons</artifactId><version>9.6</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.3.0</version><configuration><archive><manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile></archive></configuration></plugin></plugins></build>
</project>
注意:
一定要添加之间的这一段。
3、src/main/resources/META-INF/MANIFEST.MF
示例:
Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
注意:
每行后面不能有空格,最后一行要换行
4、agent/MyAgent.java
代码示例:
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Java Agent 已加载(ASM 版)");inst.addTransformer(new ClassFileTransformer() { // 注册ClassFileTransformer类@Overridepublic byte[] transform(ClassLoader loader, // transform方法修改字节码String className,Class<?> classBeingRedefined,ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {// 只处理 MyService 类if (!"com/example/MyClass".equals(className)) {return null;}System.out.println("正在转换类: " + className);ClassReader cr = new ClassReader(classfileBuffer);ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);ClassVisitor cv = new LoggingClassVisitor(cw);cr.accept(cv, ClassReader.EXPAND_FRAMES);return cw.toByteArray();}});}
}
5、agent/LoggingClassVisitor.java
代码示例:
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import static org.objectweb.asm.Opcodes.*;public class LoggingClassVisitor extends ClassVisitor {public LoggingClassVisitor(ClassVisitor cv) {super(Opcodes.ASM9, cv);}@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor,String signature, String[] exceptions) {MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);if (mv != null && name.equals("hello")) {return new AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {@Overrideprotected void onMethodEnter() {// 在hello()方法执行前插入日志:System.out.println("BEFORE: hello()");getStatic(Type.getType(System.class), "out", Type.getType(java.io.PrintStream.class));push("BEFORE: hello()");invokeVirtual(Type.getType(java.io.PrintStream.class),new Method("println", "(Ljava/lang/String;)V"));}@Overrideprotected void onMethodExit(int opcode) {// 在方法返回前插入日志:System.out.println("AFTER: hello()"); if (opcode != ATHROW) {getStatic(Type.getType(System.class), "out", Type.getType(java.io.PrintStream.class));push("AFTER: hello()");invokeVirtual(Type.getType(java.io.PrintStream.class),new Method("println", "(Ljava/lang/String;)V"));}}};}return mv;}
}
6、MyService.java(测试类)
代码示例:
// src/test/java/MyService.java
public class MyService {public static void main(String[] args) {new MyService().hello();}public void hello() {System.out.println("Hello, World!");}
}
2、方式二:使用Byte Buddy+Java Agent(更简单)
步骤同方法一一样,只需替依赖和MyAgent.java。
依赖示例:
<dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy</artifactId><version>1.14.0</version>
</dependency>
<dependency><groupId>net.bytebuddy</groupId><artifactId>byte-buddy-agent</artifactId><version>1.14.0</version>
</dependency>
代码示例:
// agent/MyAgent.java
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Java Agent 已加载(Byte Buddy 版)");new AgentBuilder.Default().type(ElementMatchers.named("MyService")) // 拦截类.transform((builder, typeDescription, classLoader, module) ->builder.method(ElementMatchers.named("hello")) // 拦截方法.intercept(Advice.to(LoggingAdvice.class)) // 插入逻辑).installOn(inst);}@SuppressWarnings("unused")public static class LoggingAdvice {@Advice.OnMethodEnterstatic void enter(@Advice.Origin String method) { // 方法进入前执行System.out.println("BEFORE: " + method);}@Advice.OnMethodExitstatic void exit(@Advice.Origin String method) { // 方法进入后执行System.out.println("AFTER: " + method);}}
}
3、编译和运行步骤
1、编译打包
bash示例:
mvn clean package
生成:target/java-agent-demo-1.0.0.jar
2、运行测试程序(带上Agent)
bash示例:
java -javaagent:target/java-agent-demo-1.0.0.jar \-cp target/test-classes \MyService
3、输出结果
BEFORE: hello()
Hello, World!
AFTER: hello()
4、验证Agent是否生效
1、如果不加 -javaagent,不会打印 BEFORE/AFTER。
2、加上后就会插入日志,说明字节码已被修改。
4、ASM vs Byte Buddy Agent
向阳前行,Dare To Be!!!