【问题标题】:Adding RunTimeVisibleAnnotations to a Java class file using ASM使用 ASM 将 RunTimeVisibleAnnotations 添加到 Java 类文件
【发布时间】:2021-09-10 04:14:28
【问题描述】:

我正在处理一个项目,该项目需要我直接在现有类文件中为局部变量添加注释,以便它重新创建下面 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;
        ...
    }
}

类文件上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();
        }
    }
}

我通过从带注释的类文件中打印出visitLocalVariableAnnotation() 中的一些参数的值来计算它们。但是变量的startLabel[]endLabel[]index是我无法弄清楚的。

有人可以验证我是否朝着正确的方向前进并帮助我了解这些论点的价值吗?

【问题讨论】:

    标签: java annotations bytecode java-bytecode-asm


    【解决方案1】:

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

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

    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 的注解”逻辑,这也减少了类文件的大小:

    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,包括它们的值。对于您的示例类,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 周期。

    但是当您要做的就是注入注释时,换句话说,您没有更改任何可执行代码,根本没有理由干涉堆栈映射框架。插桩代码可以只保留原始帧,这是最简单和最有效的处理。

    结合所有这些点产生

    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
        …
    }
    

    【讨论】:

    • 感谢您如此全面的回答!!我非常感谢您花时间提供此解决方案。我运行了您提供的代码,但在检查 javap 时,它没有在类文件中显示任何注释。我尝试从visitLocalVariable 函数内部打印出一个字符串,但它也没有被打印出来,所以似乎根本没有调用visitLocalVariable 函数。
    • 然后,确保在启用调试信息的情况下编译原始源代码(在 javac 的情况下使用 -g 选项)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-06-06
    • 2013-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多