javaAgent通过字节码对方法增强和使用 byte-buddy 来实现类的增强

2023-11-20 16:28:23 浏览数 (1)

前言

在上一篇讲述了入门和实操https://cloud.tencent.com/developer/article/2360594 本章节使用字节码和byte-buddy来玩

通过字节码对方法增强

新建一个 Test.java 然后运行一下程序编译成字节码,然后将字节码拷贝到 resources 当中如下图:

Test.java:

代码语言:java复制
public class Test {
    String name = "Hello agent";

    public void show() {
        System.out.println("新功能");
        System.out.println(name);
    }

    public void test() {
        System.out.println("test");
    }
}

修改 PreMainAgent.java 注意 JDK 版本必须是 1.8:

代码语言:java复制
/**
 * @author yby6
 **/
public class PreMainAgent {

    /**
     * 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
     * java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
     * 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
     **/
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("========= premain方法执行1 ========");
        System.out.println(agentArgs);
        enhanceTest(agentArgs, inst);
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
     */
    public static void premain(String agentArgs) {
        System.out.println("========= premain方法执行2 ========");
        System.out.println(agentArgs);
    }

    public static void enhanceTest(String agentOps, Instrumentation inst) {
        inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
            // 判断是指定的class
            if ("top/it6666/Test".equals(className)) {
                try {
                    // 获取更改后的类class,字节数组
                    classfileBuffer = Files
                        .readAllBytes(Paths.get("D:\Develop\IDEAPro\JavaAgent\src\main\resources\Test.class"));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return classfileBuffer;
        });
    }
}

修改 TestMain.java

代码语言:java复制
/**
 * @author yby6
 **/
public class TestMain {
    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                new Test().show();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

PreMainAgent 重新 install,然后在运行效果如下图所示:

使用 byte-buddy 来实现类的增强

依赖上方已经添加过了,添加拦截器 MyInterceptor.java

代码语言:java复制
/**
 * @author yby6
 */
public class MyInterceptor {
    @RuntimeType
    public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
        long start = System.currentTimeMillis();
        try {
            // 执行原方法
            System.out.println("新增内容");
            Object call = callable.call();
            System.out.println("尾部新增");
            return call;
        } finally {
            // 打印调用时长
            System.out.println(method.getName()   ":"   (System.currentTimeMillis() - start)   "ms");
        }
    }
}

修改 PreMainAgent.java:

代码语言:java复制
/**
 * @author yby6
 **/
public class PreMainAgent {

    /**
     * 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
     * java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
     * 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
     **/
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("========= premain方法执行1 ========");
        System.out.println(agentArgs);
        buttyBuddyDemo(agentArgs, inst);
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
     */
    public static void premain(String agentArgs) {
        System.out.println("========= premain方法执行2 ========");
        System.out.println(agentArgs);
    }

    public static void buttyBuddyDemo(String agentOps, Instrumentation inst) {
        // Byte-Buddy专门有个AgentBuilder来处理Java Agent的场景
        new AgentBuilder.Default()
            // 根据包名前缀拦截类
            .type(ElementMatchers.nameStartsWith("com.yby6"))
            // 拦截到的类由transformer处理
            .transform(
                (builder, typeDescription, classLoader, javaModule) -> builder.method(ElementMatchers.named("show"))
                    // .method(ElementMatchers.<MethodDescription>any())
                    .intercept(MethodDelegation.to(MyInterceptor.class)))
            .installOn(inst);
    }
}

测试方式,先用 maven.package 打包,然后在运行我们之前的测试类,运行效果如下图所示:

动态

我这里就单独创建了一个模块,就是专门用来演示和存放动态增强的代码,java-agent-attach,修改 pom.xml 添加如下依赖:

代码语言:html复制
<dependencies>
    <dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>1.8.0</version>
        <scope>system</scope>
        <systemPath>D:/Develop/Java/Jdk/jdk1.8.0_281/lib/tools.jar</systemPath>
    </dependency>
</dependencies>

取消 java-agent 的 JVM 参数配置:

image-20211030104313487image-20211030104313487

将你需要进行增强的 .class 文件放入 java-agent 工程的 resources 当中:

image-20211030103357618image-20211030103357618

然后改写 PreMainAgent.java 改写之后的内容如下:

代码语言:java复制
/**
 * @author yby6
 **/
public class PreMainAgent {

    /**
     * 1、agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。 与 main函数不同的是, 这个参数是一个字符串而不是一个字符串数组 2、Inst 是一个
     * java.lang.instrument.Instrumentation 的实例, 由 JVM 自动传入 java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,
     * 也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
     **/
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("========= premain方法执行1 ========");
        System.out.println(agentArgs);
        simpleDemo(agentArgs, inst);
    }

    /**
     * 如果不存在 premain(String agentArgs, Instrumentation inst) 则会执行 premain(String agentArgs)
     */
    public static void premain(String agentArgs) {
        System.out.println("========= premain方法执行2 ========");
        System.out.println(agentArgs);
    }

    public static void agentmain(String agentOps, Instrumentation inst) {
        System.out.println("=========agentmain方法执行========");
        simpleDemo(agentOps, inst);
        //transform是会对尚未加载的类进行增加代理层,这里是已经运行中的jvm,所以类以及被加载了
        //必须主动调用retransformClasses让jvm再对运行中的类进行加上代理层
        //下一次执行的时候, 要重新读取class字节码

        Arrays.stream(inst.getAllLoadedClasses()).forEach(allLoadedClass -> {
            //这里的Test路径,修改成你自己机器agent-demo-web工程的Test类的路径
            if (allLoadedClass.getName().contains("com.yby6.Test")) {
                try {
                    System.out.println(allLoadedClass.getName());
                    inst.retransformClasses(allLoadedClass);
                } catch (UnmodifiableClassException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public static void simpleDemo(String agentOps, Instrumentation inst) {
        inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
            System.out.println(className);
            //判断是指定的class
            if ("top/it6666/Test".equals(className)) {
                try {
                    System.out.println("获取更改后的类class 字节数组");
                    //获取更改后的类class 字节数组
                    String path = "D:\Develop\IdeaPro\my-demo-pro\java-agent\java-agent-pro\src\main\resources\Test.class";
                    classfileBuffer = Files.readAllBytes(Paths.get(path));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return classfileBuffer;
        }, true);
    }
}

如上的 path 路径里面的 .class 改为你自己的 .class 真实文件路径,然后其它的没啥需要注意的,紧接着添加 Attach.java:

代码语言:java复制
/**
 * @author yby6
 */
public class Attach {
    public static void main(String[] args) {
        // 查找所有jvm进程,排除attach测试工程
        List<VirtualMachineDescriptor> attach = VirtualMachine.list()
                .stream()
                .filter(jvm -> {
                    System.out.println(jvm.displayName());
                    return !jvm.displayName().contains("Attach");
                }).collect(Collectors.toList());

        for (int i = 0; i < attach.size(); i  ) {
            System.out.println("["   i   "] "   attach.get(i).displayName()   ":"   attach.get(i).id());
        }

        System.out.println("请输入需要attach的pid编号:");

        Scanner scanner = new Scanner(System.in);
        String s = scanner.nextLine();
        VirtualMachineDescriptor virtualMachineDescriptor = attach.get(new Integer(s));
        try {
            VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor.id());
            virtualMachine.loadAgent("D:\Develop\IdeaPro\my-demo-pro\java-agent\java-agent-pro\target\java-agent-pro-1.0-SNAPSHOT.jar", "param");
            virtualMachine.detach();
        } catch (AttachNotSupportedException e) {
            System.out.println("AttachNotSupportedException:"   e.getMessage());
        } catch (IOException e) {
            System.out.println("IOException:"   e.getMessage());
        } catch (AgentLoadException e) {
            System.out.println("AgentLoadException:"   e.getMessage());
        } catch (AgentInitializationException e) {
            System.out.println("AgentInitializationException:"   e.getMessage());
        }
    }
}

virtualMachine.loadAgent 中的 jar 是你 java-agent 打包好的 jar 存放位置,一定要指定为你的其它的也没有什么需要注意的。

最后

本期结束咱们下次再见

0 人点赞