Java方法日志打印之javaagent

1,493 阅读3分钟

javaagent

我们先来了解一下javaagent,-javaagent是java命令的一个参数,后加一个独立运行的jar包,jar包的内容是什么呢?我们先来参阅java.lang.instrument

image.png

打开包java.lang.instrument image.png 里面有几个接口和类,我们来看一下Instrumentation接口

Instrumentation

image.png 这里说到当JVM以指示代理类的方式(也就是后带-javaagent参数的方式)启动时,会将Instrumentation传递给premain方法。 Instrumentation的实现类sun.instrument.InstrumentationImpl,loadClassAndStartAgent方法会优先加载带Instrumentation参数的方法

public static void premain(String arg, Instrumentation instrumentation) 

public static void premain(String arg)

image.png 那就是说-javaagent 后带的jar包需要有premain方法,并提供参数接受Instrumentation实例。

打jar包

现在创建一个maven工程,创建类Javaagent,并提供premain方法

import java.lang.instrument.Instrumentation;

public class Javaagent {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("获取到Instrumentation实例:" + instrumentation);
    }
}

使用插件maven-assembly-plugin打包项目为一个jar包,

<?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>org.example</groupId>
    <artifactId>javaagent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>Javaagent</Premain-Class>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

打包生成javaagent-1.0-SNAPSHOT-jar-with-dependencies.jar

image.png

运行java程序并使用-javaagent参数

现在再创建一个项目工程test-agent,创建App类、UserService类

public class UserService {
    public String queryUserByName(String name) {
        return "User[ name: " + name + " ]";
    }
}
public class App {
    public static void main(String[] args) {
        UserService userService = new UserService();
        System.out.println(userService.queryUserByName("MinXie"));
    }
}

App的main方法调用UserService的方法queryUserByName打印输出Name

image.png 添加VM参数并运行程序

image.png

可以看到在queryUserByName方法执行前会调用premain方法,并传递Instrumentation实例

ClassFileTransformer

再回过头来看一下Instrumentation接口,接口提供了addTransformer方法,这个方法用于添加类文件转换器ClassFileTransformer

image.png

通过实现ClassFileTransformer可以实现类文件的转换,现在通过ASM框架插入日志打印的操作。

CusClassFileTransformer

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class CusClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if ("UserService".equals(className)) {
            System.out.println("transform..." + className);
            //修改类
            return modifyClass(classfileBuffer);
        }
        return null;
    }

    private byte[] modifyClass(byte[] classfileBuffer) {
        ClassReader classReader = new ClassReader(classfileBuffer);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        //覆写ClassVisitor的方法
        CusClassAdapter classAdapter = new CusClassAdapter(classReader.getClassName(), classWriter);
        classReader.accept(classAdapter, 0);
        return classWriter.toByteArray();
    }
}

CusClassAdapter

过滤不需要处理的方法

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class CusClassAdapter extends ClassVisitor {
    private final String className;

    public CusClassAdapter(String className, ClassWriter classWriter) {
        super(Opcodes.ASM9, classWriter);
        this.className = className;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if ((access & Opcodes.ACC_PRIVATE) != 0) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }

        if ((access & Opcodes.ACC_ABSTRACT) != 0
                || (access & Opcodes.ACC_NATIVE) != 0
                || (access & Opcodes.ACC_BRIDGE) != 0
                || (access & Opcodes.ACC_SYNTHETIC) != 0) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }

        //<init>和<clinit>不需要处理
        if ("<init>".equals(name) || "<clinit>".equals(name)) {
            return super.visitMethod(access, name, descriptor, signature, exceptions);

        }
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new CusMethodAdapter(className, access, name, descriptor, methodVisitor);
    }
}

CusMethodAdapter

处理方法调用日志打印指令插入

import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.util.ArrayList;
import java.util.List;

public class CusMethodAdapter extends MethodVisitor {
    private boolean isStaticMethod = false;
    private final String className;
    private final String methodName;
    private final String descriptor;
    private final String[] paramDescriptors;
    //异常处理程序范围的开始
    private final Label start = new Label();
    //异常处理程序范围的结
    private final Label end = new Label();
    //异常处理程序代码的开头
    private final Label handler = new Label();

    public CusMethodAdapter(String className, int access, String methodName, String descriptor, MethodVisitor methodVisitor) {
        super(Opcodes.ASM9, methodVisitor);
        if ((access & Opcodes.ACC_STATIC) == Opcodes.ACC_STATIC) {
            this.isStaticMethod = true;
        }
        this.className = className;
        this.methodName = methodName;
        this.descriptor = descriptor;
        this.paramDescriptors = getParamDescriptors(descriptor);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        //调用日志记录方法调用前信息
        this.visitLdcInsn(this.className);
        this.visitLdcInsn(this.methodName);
        this.visitLdcInsn(this.descriptor);
        if (paramDescriptors == null) {
            this.visitInsn(Opcodes.ACONST_NULL);
        } else {
            switch (paramDescriptors.length) {
                case 1:
                    mv.visitInsn(Opcodes.ICONST_1);
                    break;
                case 2:
                    mv.visitInsn(Opcodes.ICONST_2);
                    break;
                case 3:
                    mv.visitInsn(Opcodes.ICONST_3);
                    break;
                default:
                    mv.visitVarInsn(Opcodes.BIPUSH, paramDescriptors.length);
            }
        }
        //创建参数大小的Object数组
        mv.visitTypeInsn(Opcodes.ANEWARRAY, Type.getInternalName(Object.class));
        System.out.println(Type.getDescriptor(Object.class));

        int localIndex = isStaticMethod ? 0 : 1;

        for (int i = 0; i < paramDescriptors.length; i++) {
            mv.visitInsn(Opcodes.DUP);
            switch (i) {
                case 0:
                    mv.visitInsn(Opcodes.ICONST_0);
                    break;
                case 1:
                    mv.visitInsn(Opcodes.ICONST_1);
                    break;
                case 2:
                    mv.visitInsn(Opcodes.ICONST_2);
                    break;
                case 3:
                    mv.visitInsn(Opcodes.ICONST_3);
                    break;
                default:
                    mv.visitVarInsn(Opcodes.BIPUSH, i);
            }

            String type = paramDescriptors[i];
            //
            if ("B".equals(type)) {
                mv.visitVarInsn(Opcodes.ILOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Byte.class), "valueOf",
                        "(B)Ljava/lang/Byte;", false);
            } else if ("Z".equals(type)) {
                mv.visitVarInsn(Opcodes.ILOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Boolean.class), "valueOf",
                        "(Z)Ljava/lang/Boolean;", false);
            } else if ("S".equals(type)) {
                mv.visitVarInsn(Opcodes.ILOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Short.class), "valueOf",
                        "(S)Ljava/lang/Short;", false);
            } else if ("C".equals(type)) {
                mv.visitVarInsn(Opcodes.ILOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Character.class), "valueOf",
                        "(C)Ljava/lang/Character;", false);
            } else if ("I".equals(type)) {
                mv.visitVarInsn(Opcodes.ILOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Integer.class), "valueOf",
                        "(I)Ljava/lang/Integer;", false);
            } else if ("J".equals(type)) {
                mv.visitVarInsn(Opcodes.LLOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Long.class), "valueOf",
                        "(J)Ljava/lang/Long;", false);
            } else if ("F".equals(type)) {
                mv.visitVarInsn(Opcodes.FLOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Float.class), "valueOf",
                        "(F)Ljava/lang/Float;", false);
            } else if ("D".equals(type)) {
                mv.visitVarInsn(Opcodes.DALOAD, localIndex++);
                mv.visitMethodInsn(Opcodes.ACC_STATIC, Type.getInternalName(Double.class), "valueOf",
                        "(D)Ljava/lang/Double;", false);
            } else {
                mv.visitVarInsn(Opcodes.ALOAD, localIndex++);
            }
            mv.visitInsn(Opcodes.AASTORE);
        }

        mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                Type.getInternalName(Log.class), "before",
                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V",
                false);

        // 异常处理程序范围的开始
        this.visitLabel(start);
    }

    private int nextLocalIndex = 0;

    @Override
    public void visitInsn(int opcode) {
        int localIndex = nextLocalIndex == 0 ? 10 : nextLocalIndex;
        //方法返回前打印日志
        switch (opcode) {
            case Opcodes.RETURN:
                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitInsn(Opcodes.ACONST_NULL);
                mv.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
            case Opcodes.IRETURN:
                //复制一份返回值,存在局部变量表后一个位置
                this.visitInsn(Opcodes.DUP);
                this.visitVarInsn(Opcodes.ISTORE, localIndex);

                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitVarInsn(Opcodes.ILOAD, localIndex);
                //包装为对象类型
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Integer.class),
                        "valueOf", "(I)Ljava/lang/Integer;", false);
                //调用埋点返回
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
            case Opcodes.FRETURN:
                //复制一份返回值,存在局部变量表后一个位置
                this.visitInsn(Opcodes.DUP);
                this.visitVarInsn(Opcodes.FSTORE, localIndex);

                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitVarInsn(Opcodes.FLOAD, localIndex);
                //包装为对象类型
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Float.class),
                        "valueOf", "(F)Ljava/lang/Float;", false);
//                //调用埋点返回
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
            case Opcodes.LRETURN:
                //复制一份返回值,存在局部变量表后一个位置
                this.visitInsn(Opcodes.DUP2);
                this.visitVarInsn(Opcodes.LSTORE, localIndex);

                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitVarInsn(Opcodes.LLOAD, localIndex);
                //包装为对象类型
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Long.class),
                        "valueOf", "(J)Ljava/lang/Long;", false);
//                //调用埋点返回
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
            case Opcodes.DRETURN:
                //复制一份返回值,存在局部变量表后一个位置
                this.visitInsn(Opcodes.DUP2);
                this.visitVarInsn(Opcodes.DSTORE, localIndex);

                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitVarInsn(Opcodes.DLOAD, localIndex);
                //包装为对象类型
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Double.class),
                        "valueOf", "(D)Ljava/lang/Double;", false);
//                //调用埋点返回
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
            case Opcodes.ARETURN:
                //复制一份返回值,存在局部变量表后一个位置
                this.visitInsn(Opcodes.DUP);
                this.visitVarInsn(Opcodes.ASTORE, localIndex);

                this.visitLdcInsn(this.className);
                this.visitLdcInsn(this.methodName);
                this.visitLdcInsn(this.descriptor);
                this.visitVarInsn(Opcodes.ALOAD, localIndex);
                //调用埋点返回
                this.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Type.getInternalName(Log.class), "after",
                        "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V",
                        false);
                break;
        }

        super.visitInsn(opcode);
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        super.visitVarInsn(opcode, var);
        if (opcode == Opcodes.ILOAD
                || opcode == Opcodes.FLOAD
                || opcode == Opcodes.ALOAD
                || opcode == Opcodes.ISTORE
                || opcode == Opcodes.FSTORE
                || opcode == Opcodes.ASTORE) {
            if (var > nextLocalIndex) {
                nextLocalIndex = var + 1;
            }
        } else if (opcode == Opcodes.LLOAD
                || opcode == Opcodes.DLOAD
                || opcode == Opcodes.LSTORE
                || opcode == Opcodes.DSTORE) {
            if (var + 1 > nextLocalIndex) {
                nextLocalIndex = var + 2;
            }
        }
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        this.visitLabel(end);
        this.visitLabel(handler);
        this.visitVarInsn(Opcodes.ASTORE, maxLocals + 1);
        this.visitLdcInsn(this.className);
        this.visitLdcInsn(this.methodName);
        this.visitLdcInsn(this.descriptor);
        this.visitVarInsn(Opcodes.ALOAD, maxLocals + 1);
        //调用埋点返回
        this.visitMethodInsn(Opcodes.INVOKESTATIC,
                Type.getInternalName(Log.class), "error",
                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V",
                false);

        this.visitVarInsn(Opcodes.ALOAD, maxLocals + 1);
        this.visitInsn(Opcodes.ATHROW);
        this.visitTryCatchBlock(start, end, handler, Type.getInternalName(Throwable.class));
        super.visitMaxs(maxStack, maxLocals);

    }

    public String[] getParamDescriptors(String descriptor) {
        List<String> paramDescriptors = new ArrayList<>();
        String newString = descriptor.substring(descriptor.indexOf("(") + 1, descriptor.indexOf(")"));
        for (String s : newString.split(";")) {
            paramDescriptors.add(s + ";");
        }

        if (paramDescriptors.isEmpty()) return null;
        return paramDescriptors.toArray(new String[0]);
    }
}

添加类处理

再在Javaagent类中调用addTransformer方法添加类文件处理器

import java.lang.instrument.Instrumentation;

public class Javaagent {
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("获取到Instrumentation实例:" + instrumentation);
        instrumentation.addTransformer(new CusClassFileTransformer());
    }
}

效果

最后,调用App看一下类改写之后的效果

image.png