使用 ASM 将 RunTimeVisibleAnnotations 添加到 Java class 文件

Adding RunTimeVisibleAnnotations to a Java class file using ASM

我正在做一个项目,该项目要求我直接在现有 class 文件中向局部变量添加注释,以便重新创建下面 Java 代码的效果。

@Target({ElementType.LOCAL_VARIABLE, ElementType.TYPE, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}

public class MyClass {
    public static void main(String[] args) {
        int x = 512;
        @MyAnnotation int y = 5;
        ...
    }
}

class 文件中 javap -p -v 输出的注释部分如下所示。

...
      RuntimeVisibleTypeAnnotations:
        0: #15(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2}
          MyAnnotation

为此,我一直在研究 ASM。现在我对 ASM 没有任何经验,但看到一些例子,我想我对如何进行有一些想法。这是我的尝试。

public class ASMTransformer {
    public static void main(String[] args) throws Exception {
        FileInputStream fis = new FileInputStream(args[0]);
        ClassReader cr = new ClassReader(fis);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
        cr.accept(new ASMClass(cw), ClassReader.EXPAND_FRAMES);
        FileOutputStream fos = new FileOutputStream(args[0]);
        fos.write(cw.toByteArray());
        fos.close();
    }

    public static class ASMClass extends ClassVisitor {
        public ASMClass(ClassVisitor cv) {
            super(Opcodes.ASM9, cv);
        }

        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            if (name == "main")
                return new ASMMethod(super.visitMethod(access, name, desc, signature, exceptions));
            else
                return super.visitMethod(access, name, desc, signature, exceptions);
        }
    }

    public static class ASMMethod extends MethodVisitor {
        public ASMMethod(MethodVisitor mv) {
            super(Opcodes.ASM9, mv);
        }

        public void visitEnd() {
            super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible);
            super.visitEnd();
        }
    }
}

我通过从带注释的 class 文件中打印出 visitLocalVariableAnnotation() 中某些参数的值来计算它们。但是变量的startLabel[]endLabel[]index是我想不出来的

有人可以验证我是否朝着正确的方向前进并帮助我获得这些参数的值吗?

这些数组允许您指定多个变量和范围以应用注释。这允许更短的 class 文件,特别是对于具有多个(相同)值的注释。

最简单的方法是传递长度为 1 的数组,指定 x 变量及其范围。您可以从局部变量 table 中获取所需的信息,假设 class 已编译并包含调试信息。

public static class ASMMethod extends MethodVisitor {
    public ASMMethod(MethodVisitor mv) {
        super(Opcodes.ASM9, mv);
    }

    @Override
    public void visitLocalVariable(String name, String descriptor, String signature,
                                   Label start, Label end, int index) {

        super.visitLocalVariable(name, descriptor, signature, start, end, index);
        if(name.equals("x")) {
            super.visitLocalVariableAnnotation(TypeReference.LOCAL_VARIABLE << 24, null,
                new Label[] { start }, new Label[] { end }, new int[]{ index },
                "LMyAnnotation;", true)
                .visitEnd();
        }
    }
}

这会产生此 javap 输出,表明注解信息已为 x 和一次为 y 存储一次。

      RuntimeVisibleTypeAnnotations:
        0: #41(): LOCAL_VARIABLE, {start_pc=4, length=28, index=1}
          MyAnnotation
        1: #41(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2}

您可以改为实施“克隆 y 的注释”逻辑,这也会减少 class 文件大小:

public static class ASMMethod extends MethodVisitor {
    private int xIndex, yIndex;
    private Label xStart, xEnd, yStart, yEnd;

    public ASMMethod(MethodVisitor mv) {
        super(Opcodes.ASM9, mv);
    }

    @Override
    public void visitLocalVariable(String name, String descriptor, String signature,
                                   Label start, Label end, int index) {

        super.visitLocalVariable(name, descriptor, signature, start, end, index);
        if(name.equals("x")) {
            xIndex = index;
            xStart = start;
            xEnd  = end;
        }
        else if(name.equals("y")) {
            yIndex = index;
            yStart = start;
            yEnd  = end;
        }
    }

    @Override
    public AnnotationVisitor visitLocalVariableAnnotation(int typeRef,
        TypePath typePath, Label[] start, Label[] end, int[] index,
        String descriptor, boolean visible) {

        if(Arrays.stream(index).anyMatch(ix -> ix == yIndex)) {
            int num = start.length, newSize = num + 1;
            index = Arrays.copyOf(index, newSize);
            index[num] = xIndex;
            start = Arrays.copyOf(start, newSize);
            start[num] = xStart;
            end = Arrays.copyOf(end, newSize);
            end[num] = xEnd;
        }
        return super.visitLocalVariableAnnotation(
            typeRef, typePath, start, end, index, descriptor, visible);
    }
}

这会将 y 的所有注释复制到 x,包括它们的值。对于您的示例 class,javap 现在表示注释信息已共享给 xy

      RuntimeVisibleTypeAnnotations:
        0: #43(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2; start_pc=4, length=28, index=1}
          MyAnnotation

对您的代码的一些补充说明:

您不得将字符串与 == 进行比较。将 name == "main" 替换为 name.equals("main"),使其正常工作。但是您也可以减少相同 super.visitMethod 调用的代码重复。

根据您 运行 使用的系统,不关闭 FileInputStream 可能会使您尝试覆盖同一文件失败。但一般情况下建议尽早关闭资源

中的COMPUTE_FRAMES隐含了COMPUTE_MAXS,所以你不需要把两者结合起来。此外,COMPUTE_FRAMES 表示“从头开始计算所有帧”,因此它不使用原始代码的帧。因此,除非您正在执行一些其他处理,例如使用 Analyzer,否则在使用 COMPUTE_FRAMES 时不需要原始帧。因此,将 SKIP_FRAMES 传递给 ClassReader 更合适。相比之下,选项 EXPAND_FRAMES 将对原始帧执行准备工作,以进行此处从未发生的处理,它会浪费 CPU 个周期。

但是当您要做的只是注入注释时,换句话说,您没有更改任何 executable 代码,根本没有理由干预堆栈映射帧.检测代码可以只保留原始帧,这是最简单和最有效的处理。

合并所有这些点产量

public class ASMTransformer {
    public static void main(String[] args) throws IOException {
        String clazz = args[0];
        byte[] code;
        try(InputStream fis = new FileInputStream(clazz)) {
            ClassReader cr = new ClassReader(fis);
            ClassWriter cw = new ClassWriter(cr, 0);
            cr.accept(new ASMClass(cw), 0);
            code = cw.toByteArray();
        }
        Files.write(Paths.get(clazz), code);
    }

    public static class ASMClass extends ClassVisitor {
        public ASMClass(ClassVisitor cv) {
            super(Opcodes.ASM9, cv);
        }

        @Override
        public MethodVisitor visitMethod(
            int access, String name, String desc, String sig, String[] exceptions) {

            MethodVisitor mv = super.visitMethod(access, name, desc, sig, exceptions);
            if(name.equals("main")) mv = new ASMMethod(mv);
            return mv;
        }
    }

    public static class ASMMethod extends MethodVisitor { // as above
    …
}