-
Notifications
You must be signed in to change notification settings - Fork 8
Java Agent
Java从1.5提供了java.lang.instrument包,用于给JVM植入java编程语言编写的Instrumentation,这种通过java.lang.instrument实现工具被称为java agent。java agent可以在JVM加载class时候修改目标字节码,或者修改已经加载的class的信息(已经加载的class不会再次初始化,可以调整方法体,但是不能添加/删除/重命名字段或方法,不能修改方法的签名或者更改继承)。
目前java agent支持两种模式加载,启动时加载,通过在JVM启动时配置-javaagent:jarpath[=options],还有一种通过Attach API动态加载java agent。
目前基于java agent实现应用的工具有很多,例如SkyWalking(APM),Arthas(诊断),ChaosBlade(混沌注入),JaCoCo(代码覆盖率)等等。
JVM命令行启动配置模式:
首先命令行需要增加-javaagent:jarpath[=options]参数,jarpath为java agent Jar包的路径,agent Jar中的manifest文件必须包含Premain-Class属性,属性值为agent class,该类必须包含premain方法,premain方法可以有两种形式:
public static void premain(String agentArgs, Instrumentation inst); (两个方法同时出现时,该方法优先级较高)
public static void premain(String agentArgs);java agent由system class loader负责加载(ClassLoader.getSystemClassLoader),所以在premain阶段,很多时候是看不到当前目标程序引用的资源的。通过agentArgs可以获取到Agent配置的options,具体参数的解析需要由Agent自身解析。
JVM启动后Attach模式:
在目标JVM启动后attach上去,agent Jar中的manifest文件必须包含Agent-Class属性,属性值为agent class,该类必须包含agentmain方法,agentmain方法可以有两种形式:
public static void agentmain(String agentArgs, Instrumentation inst);(两个方法同时出现时,该方法优先级较高)
public static void agentmain(String agentArgs);attach模式java agent也是由system class loader负责加载(ClassLoader.getSystemClassLoader)。
目标代码:
package io.manbang.asm.demo;
/**
* @author weilong.hu
* @since 2021/11/23 14:17
*/
public class CargoTest {
public static void main(String[] args){
publishCargo();
}
/**
* 发货
*/
public static void publishCargo() {
System.out.println("publish cargo success...");
}
}java agent代码
package io.manbang.asm.demo;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
/**
* @author weilong.hu
* @since 2021/09/24 10:14
*/
public class Premain {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if ("io/manbang/asm/demo/CargoTest".equals(className)) {
final ClassReader classReader = new ClassReader(classfileBuffer);
final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
final ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
final byte[] data = classWriter.toByteArray();
return data;
}
return null;
}
});
}
public static class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
if (name.equals("publishCargo") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start.......");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end.......");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
}pom配置
<!--增加asm依赖-->
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>9.2</version>
</dependency>
</dependencies>
<!--增加manifest配置,也可以自己手动配置文件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>
io.manbang.asm.demo.Premain
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>无论是命令行启动测试用例,还是在ide中启动测试用例,在VM Options中加上-javaagent:jarpath[=options]配置,示例如下:指向java agent jar包
-javaagent:your\path\asm-demo-1.0-SNAPSHOT.jar启动后命令行CargoTest的main方法后,控制台输出如下:
start.......
publish cargo success...
end.......可以看到相关的织入逻辑已经成功织入目标class中,这个简单的示例只是演示agent如何使用,还有一些其他的问题没有考虑,比如agent由system class loader负责加载,如果用户也引入了asm相关包,如何解决类的冲突,与用户资源如何隔离,如何发现用户的资源等等问题。