NiceLeeのBlog 用爱发电 bilibili~

javaagent简单使用: 为类对象添加toString方法

2022-08-05
nIceLee

阅读:


简单来说,Javaagent可以让我们在不修改程序代码的前提下通过Instrumentation API改变运行中的java程序。
当Java虚拟机启动时,在执行 main 函数之前,JVM会先运行-javaagent所指定jar包内Premain-Class这个类的premain方法。
这个premain方法应该怎么写呢?这就是我们要讲的。

前言

基础描述什么的我就懒得复制粘贴了,找了俩博文放在这里,先粗略了解一下。

基础案例:重写Test对象的main方法

假设有demo.Test如下,我们要将main方法里的sayHello("java");改为sayHello("修改过后的java");

package demo;

public class Test{

    String aa = "value-aa";
    String bb = "value-bb";


    public static void main(String[] args) {
        System.out.println(new Test());
        sayHello("java");
    }

    public static void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}

我们采取最傻瓜的办法,自己重新再写一个Test类,然后编译成字节码,当需要的时候直接将编译好的内容给上。

生成字节码

编译写好的demo.Test,生成demo.Test.class,为了方便区别,重命名为Test.class.modified
以下为具体实现:

package demo;

public class Test{

    String aa = "value-aa";
    String bb = "value-bb";

    public static void main(String[] args) {
        System.out.println(new Test());
        sayHello("修改过后的java");
    }

    public static void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}

创建agent

如果待处理的类是demo.Test,那么提供修改后的字节码;否则原封不动。

public class MyAgent {
    public static void premain(String args, Instrumentation inst) {
        // args 是命令行的入参
        inst.addTransformer(new Transformer(args));
    }

    private static class Transformer implements ClassFileTransformer {

        public Transformer(String args) {
        }

        static byte[] readAll(InputStream in) throws IOException {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            for (int len = 0; (len = in.read(buffer)) != -1;) {
                out.write(buffer, 0, len);
            }
            in.close();
            return out.toByteArray();
        }

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            //System.out.println(className);
            // 转换指定包名开头的类
            if (className.equals("demo/Test")) {
                try {
                    System.out.println("demo/test被修改");
                    return readAll(MyAgent.class.getResourceAsStream("/resources/Test.class.modified"));
                } catch (Throwable t) {
                    t.printStackTrace();
                    return classfileBuffer;
                }
            } else {
                return classfileBuffer;
            }

        }
    }
}

创建MANIFEST.MF

创建MANIFEST.MF,指定上面premain方法所在的类
为了方便,我把agent和待测试的都打包成一个test.jar,所以下面还多了一行Main-Class: demo.Test

Manifest-Version: 1.0
Class-Path: .
Main-Class: demo.Test
Premain-Class: demo.javaagent.MyAgent
Can-Redefine-Classes: true

测试效果

> java -jar test.jar
demo.Test@75b84c92
Hello, java

> java -javaagent:test.jar=args -jar test.jar
demo/test被修改
demo.Test@75b84c92
Hello, 修改过后的java

基础案例:添加对象的toString方法

Test对象并没有重载Object的toString方法,所以我们打印的时候会直接出现内存地址。
我们期望的Test应该如下:

package demo;

public class Test{

    String aa = "value-aa";
    String bb = "value-bb";

    public String toString() {
        StringBuilder sb =new StringBuilder("Test(");
        sb.append("aa").append("=").append(aa).append(",");
        sb.append("bb").append("=").append(bb).append(",");
        sb.append(")");
        return sb.toString();
    }
    
    public static void main(String[] args) {
        System.out.println(new Test());
        sayHello("java");
    }

    public static void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}

现在我们换个方法,不再死死的自己去重写代码然后编译,而是使用javaassisit去实现agent。

public class MyAgentJavaAssist {
    public static void premain(String args, Instrumentation inst) {
        // args 是命令行的入参
        inst.addTransformer(new Transformer(args));
    }

    private static class Transformer implements ClassFileTransformer {

        public Transformer(String args) {
        }

        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
                ClassPool pool = ClassPool.getDefault();
                ByteArrayInputStream in = new ByteArrayInputStream(classfileBuffer);
                CtClass cc = pool.makeClass(in);
                // 判断有无声明 toString() 方法,没有的话就生成一个
                try {
                    cc.getDeclaredMethod("toString", new CtClass[] {});
                } catch (NotFoundException e) {
                    CtMethod cm = new CtMethod(pool.getCtClass("java.lang.String"), "toString", new CtClass[] {}, cc);
                    StringBuilder sBody = new StringBuilder();
                    sBody.append("{StringBuilder sb =new StringBuilder(\"").append(cc.getSimpleName()).append("(\");");
                    CtField[] cfs = cc.getDeclaredFields();
                    for (CtField cf : cfs) {
                        sBody.append("sb.append(\"").append(cf.getName()).append("\").append(\"=\").append(")
                                .append(cf.getName()).append(").append(\",\");");
                    }
                    sBody.append("sb.append(\")\");").append("return sb.toString();}");
                    cm.setBody(sBody.toString());
                    cc.addMethod(cm);
                }
                // 判断是否是 demo/Test, 是的话修改 main方法
                if (className.equals("demo/Test")) {
                    CtMethod cm = cc.getDeclaredMethod("main",
                            new CtClass[] { pool.getCtClass("[Ljava.lang.String;") });
                    cm.setBody("{System.out.println(new demo.Test());sayHello(\"修改过后的java\");}");
                }
                return cc.toBytecode();
            } catch (Throwable t) {
                t.printStackTrace();
                return classfileBuffer;
            }

        }
    }
}

测试效果

源代码

https://github.com/nICEnnnnnnnLee/javaagent-demo


内容
隐藏