问题
I need to transform Java bytecode with ASM to initialize public static final
fields inside a static {...}
block in the class. For example:
Input:
public static final int CONSTANT = 10;
Output:
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.
回答1:
For such a transformation, you can use the usual ClassReader
→ClassVisitor
(transformer) →ClassWriter
chain. There are three fundamental steps:
Override
visitField
to track all fields with a constant value and call the super visit method without a constant, i.e. withnull
, to keep the field declaration but remove the constant value.Override
visitMethod
to notice whether there’s an already existing class initializer (<clinit>
method). If so, return a specialMethodVisitor
which will inject the field initialization at the beginning of the code and clear the map so the third step becomes a no-op.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 aninjectFieldInit
method. Then, we only have to append the mandatoryRETURN
instruction which we don’t need to add for an already existing initializer.
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]);
}
}
来源:https://stackoverflow.com/questions/61167940/how-to-transform-bytecodes-to-initialize-primitive-constants-in-static-block-wit