本文介绍了如何使用ASM转换字节码以初始化静态块中的原始常量?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要使用ASM转换Java字节码,以初始化类中 static {...} 块内的 public static final 字段.例如:

I need to transform Java bytecode with ASM to initialize public static final fields inside a static {...} block in the class. For example:

输入:

public static final int CONSTANT = 10;

输出:

public static final int CONSTANT;

static {
    CONSTANT = 10;
}

我需要这种转换,因为编译器将原始常量替换为字节码中的实际值,因此它们的用法变得不可追踪.这种转换可以跟踪常量的用法.

I need this transformation because the compiler replaces primitive constants by their actual value in the bytecode, so their usage becomes untraceable. This transformation allows tracing the usage of constants.

推荐答案

对于这样的转换,您可以使用常规的 ClassReader ClassVisitor (transformer)→ ClassWriter 链.基本三个步骤:

For such a transformation, you can use the usual ClassReaderClassVisitor(transformer) →ClassWriter chain. There are three fundamental steps:

  • 覆盖 visitField 以跟踪具有恒定值的所有字段,并在不使用常量的情况下调用超级访问方法,即使用 null 来保留字段声明,但删除常数值.

  • Override visitField to track all fields with a constant value and call the super visit method without a constant, i.e. with null, to keep the field declaration but remove the constant value.

覆盖 visitMethod 以注意是否已经存在类初始化程序(< clinit> 方法).如果是这样,则返回一个特殊的 MethodVisitor ,它将在代码的开头注入字段初始化并清除映射,以便第三步成为空操作.

Override visitMethod to notice whether there’s an already existing class initializer (<clinit> method). If so, return a special MethodVisitor which will inject the field initialization at the beginning of the code and clear the map so the third step becomes a no-op.

重写 visitEnd 来创建一个类初始化器.新创建的类初始化程序必须执行相同的字段分配,因此值得在 injectFieldInit 方法中使用通用代码.然后,我们只需要添加必需的 RETURN 指令,而对于已经存在的初始化程序,则无需添加该指令.

Override visitEnd to create a class initializer if there were constant fields and no existing class initializer. The newly created class initializer has to do the same field assignments, so it’s worth having the common code in an injectFieldInit method. Then, we only have to append the mandatory RETURN instruction which we don’t need to add for an already existing initializer.

此代码使用数组作为映射键,在这里没有问题,因为每个字段都是不同的,因此数组不具有基于内容的 equals 方法是无关紧要的.我们可以改用 List< Map.Entry<…> 或专用元素类型的列表来保存所有必要的值,其结果与代码不查找而是仅查找相同遍历发现的字段一次.

This code uses an array as map key, which is no problem here, as each field is distinct, so the fact that arrays have no content based equals method is irrelevant. We could have used a List<Map.Entry<…>> instead or a list of a dedicated element type for holding all necessary values, with the same result as the code does no lookup but only iterates over the discovered fields once.

public static byte[] transform(byte[] classFile) {
    ClassReader cr = new ClassReader(classFile);
    ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
    ClassVisitor trans = new ClassVisitor(Opcodes.ASM5, cw) {
        private String currClassName;
        private Map<String[],Object> constants = new HashMap<>();
        @Override public void visit(int version, int acc, String name,
                                    String sig, String superName, String[] ifs) {
            currClassName = name;
            super.visit(version, acc, name, sig, superName, ifs);
        }
        @Override public FieldVisitor visitField(int acc, String name, String desc,
                                                 String sig, Object value) {
            if(value != null && (acc & Opcodes.ACC_STATIC) != 0)
                constants.put(new String[]{currClassName, name, desc}, value);
            return super.visitField(acc, name, desc, sig, null);
        }
        @Override public MethodVisitor visitMethod(int acc, String name, String desc,
                                                   String sig, String[] ex) {
            MethodVisitor mv = super.visitMethod(acc, name, desc, sig, ex);
            if(name.equals("<clinit>")) {
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override public void visitCode() {
                        super.visitCode();
                        injectFieldInit(this, constants);
                        constants.clear();
                    }
                };
            }
            return mv;
        }
        @Override public void visitEnd() {
            if(!constants.isEmpty()) {
                MethodVisitor mv = super.visitMethod(
                    Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
                mv.visitCode();
                injectFieldInit(mv, constants);
                mv.visitInsn(Opcodes.RETURN);
                mv.visitMaxs(-1, -1);
                mv.visitEnd();
            }
            super.visitEnd();
        }
    };
    cr.accept(trans, 0);
    return cw.toByteArray();
}
static void injectFieldInit(MethodVisitor target, Map<String[], Object> constants) {
    for(Map.Entry<String[],Object> e: constants.entrySet()) {
        target.visitLdcInsn(e.getValue());
        String[] field = e.getKey();
        target.visitFieldInsn(Opcodes.PUTSTATIC, field[0], field[1], field[2]);
    }
}

这篇关于如何使用ASM转换字节码以初始化静态块中的原始常量?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-29 14:23