asm如何生成子类

how to generate subclass by asm

我在Google上搜索了asm如何生成subclass,好像很少有人关心这个问题。这个需求本身不合适吗?也许 asm 做的最常见的事情就是在 AdviceAdapter 方法前后添加额外的代码。 我觉得生成一个subclass也是一个很常见的requirement.In其实,做到这一点并不容易。如何使所有 public 或受保护的方法自动覆盖父 class 的方法,就像 HttpServletRequest' subclass HttpServletRequestWrapper 所做的那样。

我用org.ow2.asm:asm:6.2实现如下:

public class InheritMethodVisitor extends ClassVisitor {

/** the return opcode for different type */
public static final Map<Type, Integer> RETURN_OPCODES = new HashMap<>();
/** the load opcode for different type */
public static final Map<Type, Integer> LOAD_OPCODES = new HashMap<>();

static {
    RETURN_OPCODES.put(Type.VOID_TYPE, Opcodes.RETURN);
    RETURN_OPCODES.put(Type.BOOLEAN_TYPE, Opcodes.IRETURN);
    RETURN_OPCODES.put(Type.BYTE_TYPE, Opcodes.IRETURN);
    RETURN_OPCODES.put(Type.SHORT_TYPE, Opcodes.IRETURN);
    RETURN_OPCODES.put(Type.INT_TYPE, Opcodes.IRETURN);
    RETURN_OPCODES.put(Type.LONG_TYPE, Opcodes.LRETURN);
    RETURN_OPCODES.put(Type.FLOAT_TYPE, Opcodes.FRETURN);
    RETURN_OPCODES.put(Type.DOUBLE_TYPE, Opcodes.DRETURN);

    LOAD_OPCODES.put(Type.BOOLEAN_TYPE, Opcodes.ILOAD);
    LOAD_OPCODES.put(Type.BYTE_TYPE, Opcodes.ILOAD);
    LOAD_OPCODES.put(Type.SHORT_TYPE, Opcodes.ILOAD);
    LOAD_OPCODES.put(Type.INT_TYPE, Opcodes.ILOAD);
    LOAD_OPCODES.put(Type.LONG_TYPE, Opcodes.LLOAD);
    LOAD_OPCODES.put(Type.FLOAT_TYPE, Opcodes.FLOAD);
    LOAD_OPCODES.put(Type.DOUBLE_TYPE, Opcodes.DLOAD);
}


private Class<?> superClass;
private ClassVisitor cv;

public InheritMethodVisitor(int api, ClassVisitor classVisitor, Class<?> superClass) {
    super(api);
    this.cv = classVisitor;
    this.superClass = Objects.requireNonNull(superClass);
}

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    //Inherit all public or protect methods
    if (Modifier.isStatic(access) || Modifier.isPrivate(access)) return null;
    if (name.equals("<init>") || Modifier.isProtected(access)) access = Opcodes.ACC_PUBLIC;

    MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);

    Type methodType = Type.getMethodType(descriptor);
    Type[] argumentTypes = methodType.getArgumentTypes();

    if (!name.equals("<init>")) {
        //TODO Customize what you want to do
        //System.out.println(name)
        mv.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(System.class), "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn(name);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(PrintStream.class), "println", "(Ljava/lang/String;)V", false);
    }

    //load this
    mv.visitVarInsn(Opcodes.ALOAD, 0);
    //load arguments
    IntStream.range(0, argumentTypes.length).forEach(value ->
            mv.visitVarInsn(LOAD_OPCODES.getOrDefault(argumentTypes[value], Opcodes.ALOAD), value + 1)
    );

    //invoke super.{method}()
    mv.visitMethodInsn(
            Opcodes.INVOKESPECIAL,
            Type.getInternalName(superClass),
            name,
            descriptor,
            false);

    //handle return
    mv.visitInsn(RETURN_OPCODES.getOrDefault(methodType.getReturnType(), Opcodes.ALOAD));
    //for ClassWriter.COMPUTE_FRAMES the max*s not required correct
    int maxLocals = argumentTypes.length + 1;
    mv.visitMaxs(maxLocals + 2, maxLocals);
    mv.visitEnd();
    return null;
}}

    @Test
public void create() throws Exception {
    //generate a subclass of AdviceAdapter to add logger info
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    //generate class name
    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/github/Generated",
            null, Type.getInternalName(AdviceAdapter.class), null);
    //generate overwrite methods
    new ClassReader(AdviceAdapter.class.getName())
            .accept(
                    new InheritMethodVisitor(Opcodes.ASM6, cw, AdviceAdapter.class),
                    ClassReader.EXPAND_FRAMES
            );
    //TODO AdviceAdapter.class.getSuperclass() not supported
    //end
    cw.visitEnd();
    //save to file
    byte[] bytes = cw.toByteArray();
    Files.write(Paths.get(getClass().getResource("/com/github").getPath(), "Generated.class"), bytes);
    //show use of it
    ClassReader classReader = new ClassReader(AdviceAdapter.class.getName());
    classReader.accept(
            new ClassVisitor(Opcodes.ASM6, new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES)) {
                public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                    MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
                    try {
                        Class<?> aClass = Class.forName("com.github.Generated");
                        return (MethodVisitor) aClass.getConstructors()[0].newInstance(Opcodes.ASM6, methodVisitor, access, name, descriptor);
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
                        throw new IllegalStateException(e);
                    }
                }
            },
            ClassReader.EXPAND_FRAMES
    );
}

可行,但不是很容易。事实上,我认为 TraceClassVisitor 工作起来很简单。

您真的应该看看 ByteBuddy 库 - 因为它更适合您的目标,所以没有理由为此类通用任务使用如此低级的库。

在 ASM 中,您需要单独完成所有这些工作 - 您不能只告诉它为您生成和实现方法,ASM 库只是修改原始字节码,因此您需要阅读 super class 我为他们每个人生成字节码。您可以使用 asm 模块为您打印 asm 代码:​​ https://static.javadoc.io/org.ow2.asm/asm/5.2/org/objectweb/asm/util/ASMifier.html 。 或从命令行:

java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifier \
java.lang.Runnable

这将创建工作的 ASM 代码以生成给定的 class,如下所示:

package asm.java.lang;
import org.objectweb.asm.*;

public class RunnableDump implements Opcodes {
    public static byte[] dump() throws Exception {
        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;
        cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
                "java/lang/Runnable", null, "java/lang/Object", null);
        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V",
                    null, null);
            mv.visitEnd();
        }
        cw.visitEnd();
        return cw.toByteArray();
    }
}

(Intellij IDEA也有插件可以查看字节码和ASMifed代码,在插件里找ASM即可)

要创建一个子class,您需要做的就是在 ASM 中生成 class 时传递该子class 的名称,如上例所示:

cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
                "java/lang/Runnable", null, "java/lang/Object", null); 

java/lang/Object 是您想扩展的超级class,完成。
但是对于方法,你需要手动循环遍历所有方法并生成你想要的代码。
除非你想创建一些不太典型的东西或一些更通用的东西,比如自己的库来生成像 ByteBuddy 这样的代理 classes——那么最好使用一些已经存在的解决方案:ByteBuddy、Javassist、CGLib。 (他们也都使用ASM生成字节码)

我们使用 ASM 为 Saxon XSLT/XQuery 处理器生成字节码,并且生成已知抽象 class 的子 class 是我们通常做事的方式。我不会假装这很容易,我没有时间给你写教程,不幸的是我不能发布我们的代码,但我可以向你保证这是可能的。我认为您不必为覆盖方法做任何特别的事情。

我们有一个 class 生成器,它子类化了 ASM 的生成器适配器。

我们使用类似以下内容创建 class:

    String className = "xxx" + compiler.getUniqueNumber();
    ClassVisitor cv = new ClassWriter(flags);
    cv.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC, className, null,
            "com/saxonica/ee/bytecode/iter/CompiledBlockIterator", new String[]{});
    // CompiledBlockIterator is the superclass name

    // generate constructor

    Method m = Method.getMethod("void <init> ()");
    Generator ga = new Generator(Opcodes.ACC_PUBLIC, m, false, cv);
    ga.loadThis();
    ga.invokeConstructor(Type.getType(CompiledBlockIterator.class), m);
    ga.returnValue();
    ga.endMethod();

然后以同样的方式继续生成其他方法。