Skip to content

Java Agent

codingPao edited this page Jan 18, 2023 · 22 revisions

一、Java Agent

Java1.5提供了java.lang.instrument包,用于给JVM植入java编程语言编写的Instrumentation,这种通过java.lang.instrument实现工具被称为java agentjava 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]参数,jarpathjava 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 agentsystem 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)

二、Java Agent示例

目标代码:

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

启动后命令行CargoTestmain方法后,控制台输出如下:

start.......
publish cargo success...
end.......

可以看到相关的织入逻辑已经成功织入目标class中,这个简单的示例只是演示agent如何使用,还有一些其他的问题没有考虑,比如agentsystem class loader负责加载,如果用户也引入了asm相关包,如何解决类的冲突,与用户资源如何隔离,如何发现用户的资源等等问题。

三、字节码

字节码结构,字节码指令解析相关文章较多,这里不在赘述,可以参考相关文章:字节码简介官方文档

Clone this wiki locally