如何通过asm生成子类

时间:2018-07-18 09:59:11

标签: java java-bytecode-asm

我在Google上搜索了如何通过asm生成子类,似乎很少有人关注此问题。这种需求本身不合适吗?也许asm做的最常见的事情是在AdviceAdapter的方法之前和之后添加额外的代码。 我认为生成子类也是一个很常见的要求,实际上,做到这一点并不容易。像HttpServletRequest的子类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很容易。

2 个答案:

答案 0 :(得分:1)

您应该真正看到ByteBuddy库-因为它更适合您的目标,因此没有理由将此类低级库用于此类常规任务。

在ASM中,您需要单独完成所有操作-您不能仅仅告诉它为您生成和实现方法,ASM库仅是修改原始字节码,因此您需要阅读我生成的超类的所有方法每个字节码。您可以使用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代码以生成给定的类,如下所示:

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)

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

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

java/lang/Object是您要扩展完成的超类。
但是对于方法,您需要手动循环所有方法并生成所需的代码。
除非您想创建一些不典型的东西或诸如自己的库之类的更通用的东西来生成诸如ByteBuddy之类的代理类,否则最好使用一些现有的解决方案:ByteBuddy,Javassist,CGLib。 (它们全部也使用ASM生成字节码)

答案 1 :(得分:1)

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

我们有一个Generator类,它包含了ASM的GeneratorAdapter。

我们使用类似以下的方式创建类:

    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();

,然后以相同方式继续生成其他方法。