如何使用 java 代理和 ASM 动态记录任何调用的 java 方法的所有参数?

How to record all parameters of any invoked java method dynamically using java agent and ASM?

我想做的是记录这些参数,并与之前输入的参数做一些比较。我需要为调用的每个方法记录参数,所以参数列表的长度是不确定的。

我想我可以解析方法描述符来知道这个方法有多少参数。但问题是,如何从操作数栈中记录任意数量的值?

现在只能写一些示例代码了。在我的 MethodVisitorAdapter Class:

public void visitMethodInsn(int opc, String owner, String name, String desc, boolean isInterface){
    int n = getParameterCount(desc);
    // How to duplicate arbitrary number of values at operand stack?
    // ...
    mv.visitMethodInsn(INVOKESTATIC, MyClass, "recordParameters",
                    /* should be what kind of descriptor (may have arbitrary number of parameters)? */, 
                    false);
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);
}

您可以使 recordParameters 成为可变元数方法 (varargs)。它将接受所有参数作为单个 Object[] 参数。

因此,您需要创建一个数组并用(可能是盒装的)参数填充它。以下构建器样式 class 将有助于从堆栈中收集参数。它包含所有可能类型的 push 方法,还处理自动装箱。

记录参数后,helper 可以通过相反的顺序调用相应的 pop 方法将参数放回堆栈。

public class ArgCollector {
    private final Object[] args;
    private int index;

    public ArgCollector(int length) {
        this.args = new Object[length];
        this.index = length;
    }

    public ArgCollector push(Object o) {
        args[--index] = o;
        return this;
    }

    public Object pop() {
        return args[index++];
    }

    public static ArgCollector push(boolean a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(byte    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(char    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(short   a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(int     a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(long    a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(float   a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(double  a, ArgCollector c) { return c.push(a); }
    public static ArgCollector push(Object  a, ArgCollector c) { return c.push(a); }

    public boolean popZ() { return (boolean) pop(); }
    public byte    popB() { return (byte)    pop(); }
    public char    popC() { return (char)    pop(); }
    public short   popS() { return (short)   pop(); }
    public int     popI() { return (int)     pop(); }
    public long    popJ() { return (long)    pop(); }
    public float   popF() { return (float)   pop(); }
    public double  popD() { return (double)  pop(); }

    public Object[] toArray() {
        return args;
    }
}

现在,任务是为以下 Java 代码生成等效的字节码:

    ArgCollector collector = new ArgCollector(N);
    recordParameters(
            ArgCollector.push(arg1,
                ArgCollector.push(arg2,
                    ArgCollector.push(argN, collector)))
            .toArray()
    );

    originalMethod(
            collector.popI(),
            collector.popJ(),
            (String) collector.pop()
    );

以下是使用 ASM 执行此操作的方法:

    Type[] args = Type.getArgumentTypes(desc);
    String collector = Type.getInternalName(ArgCollector.class);

    // new ArgCollector(argCount)
    mv.visitTypeInsn(NEW, collector);
    mv.visitInsn(DUP);
    mv.visitIntInsn(SIPUSH, args.length);
    mv.visitMethodInsn(INVOKESPECIAL, collector, "<init>", "(I)V", false);

    // For each argument call ArgCollector.push(arg, collector)
    for (int i = args.length; --i >= 0; ) {
        Type arg = args[i];
        String argDesc = arg.getDescriptor().length() == 1 ? arg.getDescriptor() : "Ljava/lang/Object;";
        mv.visitMethodInsn(INVOKESTATIC, collector, "push",
                "(" + argDesc + "L" + collector + ";)L" + collector + ";", false);
    }

    // Call recordParameters(collector.toArray())
    mv.visitInsn(DUP);
    mv.visitMethodInsn(INVOKEVIRTUAL, collector, "toArray", "()[Ljava/lang/Object;", false);
    mv.visitMethodInsn(INVOKESTATIC, MyClass, "recordParameters", "([Ljava/lang/Object;)V", false);

    // Push original arguments back on stack
    for (Type arg : args) {
        String argDesc = arg.getDescriptor().length() == 1 ? arg.getDescriptor() : "Ljava/lang/Object;";

        mv.visitInsn(DUP);
        if (argDesc.length() == 1) {
            mv.visitMethodInsn(INVOKEVIRTUAL, collector, "pop" + argDesc, "()" + argDesc, false);
        } else {
            mv.visitMethodInsn(INVOKEVIRTUAL, collector, "pop", "()Ljava/lang/Object;", false);
            if (!arg.getDescriptor().equals("Ljava/lang/Object;")) {
                // Need to cast object arguments to the original type
                mv.visitTypeInsn(CHECKCAST, arg.getDescriptor());
            }
        }

        // Swap the last argument with ArgCollector, so that ArgCollector is on top again
        if (arg.getSize() == 1) {
            mv.visitInsn(SWAP);
        } else {
            mv.visitInsn(DUP2_X1);
            mv.visitInsn(POP2);
        }
    }

    // Pop off the remaining ArgCollector, and call the original method
    mv.visitInsn(POP);
    mv.visitMethodInsn(opc, owner, name, desc, isInterface);

要在操作数堆栈上复制任意序列的值,没有办法将它们暂时存储到新的局部变量中。然后,将所有这些值压入操作数堆栈,调用您的报告方法,再次压入它们,并调用原始方法。

一种避免LocalVariablesSorter开销的简单方法是检查每条使用局部变量的指令并记住第一个空闲索引。这要求源代码已经具有有效的堆栈映射帧,以便在单次访问过程中正确处理向后分支。对于代码定位 Java 7 或更高版本,无论如何这是强制性的。

由于报告调用的代码没有分支并且之后不需要临时变量,我们甚至不需要像在分支合并点那样对堆栈映射帧进行昂贵的重新计算,只需要原始变量是需要的。只有 max stack 和 locals 需要重新计算。

要调用报告方法,我们可以使用持有者对象,如 , but alternatively, we can use the java.lang.invoke package,它允许我们生成 varargs 收集器,其中包含精确的方法描述符-飞.

以下代码将获取一个 MethodHandle to PrintStream.printf(String,Object...) via a single ldc instruction, followed by binding System.out as first argument, followed by binding a constant String suitable for the current number of arguments (and the receiver for non-static methods), then adapt the handle calling .asVarargsCollector(Object[].class).asType(targetType). The targetType is the method type descriptor, with an additional first parameter type for non-static method invocations. This MethodType is also loaded with a single ldc instruction. Then, the handle can be used by calling invokeExact,其参数与堆栈上的实际调用相同。

仅对于构造函数调用,接收者对象被省略,因为我们不允许在初始化之前使用对象。

{store           n + i } for each argumentᵢ
 ldc              MethodHandle invokeVirtual java/io/PrintStream.printf(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
 getstatic        java/lang/System.out Ljava/io/PrintStream;
 invokevirtual    java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
 ldc              String containing <method name> and as many %s place holders as needed
 invokevirtual    java/lang/invoke/MethodHandle.bindTo(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
 ldc              class [Ljava/lang/Object;
 invokevirtual    java/lang/invoke/MethodHandle.asVarargsCollector(Ljava/lang/Class;)Ljava/lang/invoke/MethodHandle;
 ldc              MethodType («actual argument types»)Ljava/io/PrintStream;
 invokevirtual    java/lang/invoke/MethodHandle.asType(Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
{load            n + i } for each argumentᵢ
 invokevirtual    java/lang/invoke/MethodHandle.invokeExact(«actual argument types»)Ljava/io/PrintStream;
 pop              // remove the PrintStream return by printf
{load            n + i } for each argumentᵢ
 invoke...        original method

转换代码和示例

public class LogMethodCalls {
    public static void main(String[] args) throws IOException, IllegalAccessException {
        MethodHandles.lookup().defineClass(instrument(LogMethodCalls.class
            .getResourceAsStream("LogMethodCalls$ToInstrument.class")));
        runInstrumented();
    }
    private static void runInstrumented() {
        new ToInstrument().run();
    }
    static class ToInstrument implements Runnable {
        @Override
        public void run() {
            double min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
            for(double i: List.of(4, 2, 9, 6)) {
                min = Math.min(min, i);
                max = Math.max(max, i);
            }
            System.out.printf("min %.0f, max %.0f%n", min, max);
        }
    }
    static byte[] instrument(InputStream is) throws IOException {
        ClassReader cr = new ClassReader(is);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);

        cr.accept(new ClassVisitor(Opcodes.ASM7, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name,
                    String descriptor, String signature, String[] exceptions) {
                return new LogInjector(
                    super.visitMethod(access, name, descriptor, signature, exceptions),
                    access, descriptor);
            }
        }, ClassReader.EXPAND_FRAMES);

        return cw.toByteArray();
    }
    static class LogInjector extends MethodVisitor {
        static final String PS_T = "java/io/PrintStream", PS_S = "L" + PS_T + ";";
        static final String PRINTF_DESC="(Ljava/lang/String;[Ljava/lang/Object;)"+PS_S;
        static final String MH_T="java/lang/invoke/MethodHandle", MH_S="L" + MH_T + ";";

        private int firstUnusedVar;
        public LogInjector(MethodVisitor mv, int acc, String desc) {
            super(Opcodes.ASM7, mv);
            int vars = Type.getArgumentsAndReturnSizes(desc) >> 2;
            if((acc & Opcodes.ACC_STATIC) != 0) vars--;
            firstUnusedVar = vars;
        }
        @Override
        public void visitFrame(int type,
            int numLocal, Object[] local, int numStack, Object[] stack) {
            super.visitFrame(type, numLocal, local, numStack, stack);
            firstUnusedVar = Math.max(firstUnusedVar, numLocal);
        }
        @Override
        public void visitVarInsn(int opcode, int var) {
            super.visitVarInsn(opcode, var);
            if(opcode == Opcodes.LSTORE || opcode == Opcodes.DSTORE) var++;
            if(var >= firstUnusedVar) firstUnusedVar = var + 1;
        }
        @Override
        public void visitMethodInsn(int opcode,
            String owner, String name, String descriptor, boolean isInterface) {
            Type[] arg = Type.getArgumentTypes(descriptor);

            int[] vars = storeArguments(arg, opcode, name, owner);

            String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);

            mv.visitLdcInsn(new Handle(Opcodes.H_INVOKEVIRTUAL,
                PS_T, "printf", PRINTF_DESC, false));
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", PS_S);
            bindTo();
            mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
            bindTo();
            mv.visitLdcInsn(Type.getObjectType("[Ljava/lang/Object;"));
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "asVarargsCollector", "(Ljava/lang/Class;)"+MH_S, false);
            mv.visitLdcInsn(Type.getMethodType(reportDesc));
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
            pushArguments(arg, vars);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                MH_T, "invokeExact", reportDesc, false);
            mv.visitInsn(Opcodes.POP);

            pushArguments(arg, vars);
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        }

        String getReportDescriptor(
            String owner, String descriptor, Type[] arg, int[] vars) {
            StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
            sb.append('(');
            if(arg.length != vars.length) {
                if(owner.charAt(0) == '[') sb.append(owner);
                else sb.append('L').append(owner).append(';');
            }
            sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
            return sb.append(PS_S).toString();
        }

        int[] storeArguments(Type[] arg, int opcode, String name, String owner) {
            int nArg = arg.length;
            boolean withThis = opcode != Opcodes.INVOKESTATIC && !name.equals("<init>");
            if(withThis) nArg++;
            int[] vars = new int[nArg];
            int slot = firstUnusedVar;
            for(int varIx = nArg-1, argIx = arg.length-1; argIx >= 0; varIx--,argIx--) {
                Type t = arg[argIx];
                mv.visitVarInsn(t.getOpcode(Opcodes.ISTORE), vars[varIx] = slot);
                slot += t.getSize();
            }
            if(withThis)
                mv.visitVarInsn(Opcodes.ASTORE, vars[0] = slot);
            return vars;
        }
        private void bindTo() {
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
                "bindTo", "(Ljava/lang/Object;)"+MH_S, false);
        }
        private void pushArguments(Type[] arg, int[] vars) {
            int vIx = 0;
            if(arg.length != vars.length)
                mv.visitVarInsn(Opcodes.ALOAD, vars[vIx++]);
            for(Type t: arg)
                mv.visitVarInsn(t.getOpcode(Opcodes.ILOAD), vars[vIx++]);
        }
        private String messageFormat(int opcode, String owner, String name, Type[] arg){
            StringBuilder sb = new StringBuilder();
            switch(opcode) {
                case Opcodes.INVOKESPECIAL:
                    if(name.equals("<init>")) {
                        name = Type.getObjectType(owner).getClassName();
                        break;
                    }
                    // else no break
                case Opcodes.INVOKEINTERFACE: // no break
                case Opcodes.INVOKEVIRTUAL:
                    sb.append("[%s].");
                    break;
                case Opcodes.INVOKESTATIC:
                    sb.append(Type.getObjectType(owner).getClassName()).append('.');
                    break;
            }
            sb.append(name);
            if(arg.length == 0) sb.append("()%n");
            else {
                sb.append('(');
                for(int i = arg.length; i > 1; i--) sb.append("%s, ");
                sb.append("%s)%n");
            }
            return sb.toString();
        }
    }
}

示例调用使用 Java 9 并依赖于延迟加载的 JVM,因此它可以在实际使用之前(重新)定义 class。它可能会被实际的 Instrumentation 场景取代,因为它与实际的转换逻辑无关。在我的设置中,示例打印

java.lang.Object()
java.lang.Integer.valueOf(4)
java.lang.Integer.valueOf(2)
java.lang.Integer.valueOf(9)
java.lang.Integer.valueOf(6)
java.util.List.of(4, 2, 9, 6)
[[4, 2, 9, 6]].iterator()
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[4].intValue()
java.lang.Math.min(2.147483647E9, 4.0)
java.lang.Math.max(-2.147483648E9, 4.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[2].intValue()
java.lang.Math.min(4.0, 2.0)
java.lang.Math.max(4.0, 2.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[9].intValue()
java.lang.Math.min(2.0, 9.0)
java.lang.Math.max(4.0, 9.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
[java.util.ImmutableCollections$ListItr@5ce65a89].next()
[6].intValue()
java.lang.Math.min(2.0, 6.0)
java.lang.Math.max(9.0, 6.0)
[java.util.ImmutableCollections$ListItr@5ce65a89].hasNext()
java.lang.Double.valueOf(2.0)
java.lang.Double.valueOf(9.0)
[java.io.PrintStream@6e5e91e4].printf(min %.0f, max %.0f%n, [Ljava.lang.Object;@2cdf8d8a)
min 2, max 9

请注意,您的用例可能更简单。如果您的日志记录方法是 static 方法,不需要 PrintStream,则不需要绑定它。当它不是 return 一个值时,你也不需要弹出它。当它接受可变参数(包括格式字符串或方法名称)时,它会更简单。然后,我们可以像普通参数一样传递字符串,而不是绑定它,并且由于方法句柄现在未被修改,当目标方法是 varargs 收集器时,它已经是一个 可变参数 方法:

static void yourLog(Object... arg) {
    String name = (String) arg[0]; // or format string
    arg = Arrays.copyOfRange(arg, 1, arg.length);
    …
}
@Override
public void visitMethodInsn(int opcode,
    String owner, String name, String descriptor, boolean isInterface) {

    Type[] arg = Type.getArgumentTypes(descriptor);
    int[] vars = storeArguments(arg, opcode, name, owner);
    String reportDesc = getReportDescriptor(owner, descriptor, arg, vars);
    mv.visitLdcInsn(new Handle(Opcodes.H_INVOKESTATIC,
        YOUR_TARGET_TYPE, "yourLog", "([Ljava/lang/Object;)V", false));
    mv.visitLdcInsn(Type.getMethodType(reportDesc));
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T,
        "asType", "(Ljava/lang/invoke/MethodType;)"+MH_S, false);
    mv.visitLdcInsn(messageFormat(opcode, owner, name, arg));
    pushArguments(arg, vars);
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, MH_T, "invokeExact", reportDesc, false);

    pushArguments(arg, vars);
    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}

String getReportDescriptor(String owner, String descriptor, Type[] arg, int[] vars) {
    StringBuilder sb = new StringBuilder(owner.length()+descriptor.length()+2);
    sb.append("(Ljava/lang/String;");
    if(arg.length != vars.length) {
        if(owner.charAt(0) == '[') sb.append(owner);
        else sb.append('L').append(owner).append(';');
    }
    sb.append(descriptor, 1, descriptor.lastIndexOf(')')+1);
    return sb.append('V').toString();
}

另一种选择是用 invokedynamic 替换 invoke* 指令,并提供 bootstrap 方法:

private static final Handle BSM = new Handle(H_INVOKESTATIC, "com/example/Bootstraps", "invokeProxy", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;)Ljava/lang/invoke/CallSite;", false);

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    if (opcode == INVOKESPECIAL && name.equals("<init>") {
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
        return;
    }
    Type method = Type.getMethodType(descriptor);
    Type[] oldTypes = method.getArgumentTypes();
    final int htype;
    switch (opcode) {
        case INVOKEINTERFACE:
            htype = H_INVOKEINTERFACE;
            break;
        case INVOKESPECIAL:
            htype = H_INVOKESPECIAL;
            break;
        case INVOKESTATIC:
            htype = H_INVOKESTATIC;
            break;
        case INVOKEVIRTUAL:
            htype = H_INVOKEVIRTUAL;
            break;
        default:
            throw new IllegalArgumentException("Unknown opcode: " + opcode);
    }
    final Type[] newTypes;
    switch (opcode) {
        case INVOKESPECIAL:
        case INVOKEINTERFACE:
        case INVOKEVIRTUAL:
            newTypes = new Type[oldTypes.length + 1];
            newTypes[0] = Type.getObjectType(owner);
            System.arraycopy(oldTypes, 0, newTypes, 1, oldTypes.length);
            break;
        case INVOKESTATIC:
            newTypes = oldTypes;
            break;
        default:
            throw new AssertionError(); // can not happen.
    }
    Handle h = new Handle(htype, owner, name, descriptor, isInterface);
    String indyDesc = Type.getMethodType(method.getReturnType(), newTypes).getDescriptor();
    visitInvokeDynamicInsn("_", indyDesc, BSM, h);
}

还有一个像这样的简单 bootstrap 方法:

package com.example; // Change the com/example/Bootstraps if you use a different package.

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleInfo;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.util.Arrays;

import static java.lang.invoke.MethodType.methodType;

public class Bootstraps {

    private static final MethodHandle PRINT_ARRAY;

    static {
        try {
            Lookup l = MethodHandles.lookup();
            MethodHandle printLn = l.findVirtual(PrintStream.class, "println", methodType(void.class, String.class))
                    .bindTo(System.err);
            MethodHandle arraysDeepToString = l.findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
            PRINT_ARRAY = MethodHandles.foldArguments(
                    MethodHandles.dropArguments(printLn, 1, Object[].class), arraysDeepToString);
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }

    public static CallSite invokeProxy(Lookup lookup, String name, MethodType type, MethodHandle target) {
        Object method = lookup.revealDirect(target);
        MethodHandle printArgsMH = MethodHandles.foldArguments(target, 
                PRINT_ARRAY.asCollector(Object[].class, type.parameterCount() + 1)
                    .bindTo(method)
                    .asType(type.changeReturnType(void.class)))
                .asType(type);
        return new ConstantCallSite(printArgsMH);
    }
}

这可能看起来比实际情况更复杂。