如何使用 ASM 转换字节码以初始化静态块中的原始常量?
How to transform bytecodes to initialize primitive constants in static block with ASM?
我需要使用 ASM 转换 Java 字节码以初始化 class 中 static {...}
块内的 public static final
字段。例如:
输入:
public static final int CONSTANT = 10;
输出:
public static final int CONSTANT;
static {
CONSTANT = 10;
}
我需要这种转换,因为编译器将原始常量替换为字节码中的实际值,因此它们的用法变得无法追踪。此转换允许跟踪常量的使用。
对于这样的变换,可以使用通常的ClassReader
→ClassVisitor
(transformer)→ClassWriter
链。共有三个基本步骤:
覆盖visitField
以跟踪所有具有常量值的字段并调用不带常量的超级访问方法,即使用null
,以保留字段声明但删除常数值。
重写 visitMethod
以注意是否已经存在 class 初始化程序(<clinit>
方法)。如果是这样,return 一个特殊的 MethodVisitor
将在代码的开头注入字段初始化并清除映射,因此第三步变为空操作。
重写 visitEnd
以创建一个 class 初始值设定项(如果有常量字段且没有现有的 class 初始值设定项)。新创建的 class 初始化程序必须执行相同的字段分配,因此值得在 injectFieldInit
方法中使用通用代码。然后,我们只需要附加强制性的 RETURN
指令,我们不需要为已经存在的初始化器添加。
此代码使用数组作为映射键,这在这里没有问题,因为每个字段都是不同的,因此数组没有基于内容的 equals
方法这一事实是无关紧要的。我们本可以使用 List<Map.Entry<…>>
代替,或者使用一个专用元素类型的列表来保存所有必要的值,结果与代码不查找但只迭代发现的字段一次相同。
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 转换 Java 字节码以初始化 class 中 static {...}
块内的 public static final
字段。例如:
输入:
public static final int CONSTANT = 10;
输出:
public static final int CONSTANT;
static {
CONSTANT = 10;
}
我需要这种转换,因为编译器将原始常量替换为字节码中的实际值,因此它们的用法变得无法追踪。此转换允许跟踪常量的使用。
对于这样的变换,可以使用通常的ClassReader
→ClassVisitor
(transformer)→ClassWriter
链。共有三个基本步骤:
覆盖
visitField
以跟踪所有具有常量值的字段并调用不带常量的超级访问方法,即使用null
,以保留字段声明但删除常数值。重写
visitMethod
以注意是否已经存在 class 初始化程序(<clinit>
方法)。如果是这样,return 一个特殊的MethodVisitor
将在代码的开头注入字段初始化并清除映射,因此第三步变为空操作。重写
visitEnd
以创建一个 class 初始值设定项(如果有常量字段且没有现有的 class 初始值设定项)。新创建的 class 初始化程序必须执行相同的字段分配,因此值得在injectFieldInit
方法中使用通用代码。然后,我们只需要附加强制性的RETURN
指令,我们不需要为已经存在的初始化器添加。
此代码使用数组作为映射键,这在这里没有问题,因为每个字段都是不同的,因此数组没有基于内容的 equals
方法这一事实是无关紧要的。我们本可以使用 List<Map.Entry<…>>
代替,或者使用一个专用元素类型的列表来保存所有必要的值,结果与代码不查找但只迭代发现的字段一次相同。
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]);
}
}