使用 ASM

问题描述

我正在处理一个项目,该项目要求我直接在现有类文件中向局部变量添加注释,以便重新创建下面 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,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() 中某些参数的值。但是变量的 start Label[]end Label[]index 是我无法弄清楚的。

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

解决方法

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

最简单的方法是传递长度为 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,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,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,index,visible);
    }
}

这会将 y 的所有注释复制到 x,包括它们的值。在您的示例类中,javap 现在指示为 xy 共享注释信息。

      RuntimeVisibleTypeAnnotations:
        0: #43(): LOCAL_VARIABLE,index=2; start_pc=4,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
    …
}