为什么 ASM 不调用我的 ``visitCode``?

Why doesn't ASM call my ``visitCode``?

我会将我的代码添加到此 post 的末尾。

我正在使用 byteBuddy 1.7.9 以及随附的任何 ASM 版本。

一言以蔽之

我有

byte[] rawClass = ...;
ClassReader cr = new ClassReader(rawClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
cr.accept(ma,ClassReader.EXPAND_FRAMES);

MethodAdder 中,我想添加一个静态初始化程序:

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    if(mv != null){
        if(!name.equals(CLINIT_NAME)) return mv;
        else{
            hasStaticInitialiser = true;
            return new ClinitReplacer(api,mv,classname);
        }
    }else return null;
}

hasStaticInitialiser = true 已达到,但 ClinitReplacer.visitCode 永远不会执行。 为什么?

原委

假设我想使用 byteBuddy 从 this example 生成 class B

为什么选择字节好友?好吧,一方面它应该很方便,另一方面,我需要它的 class 重新加载功能。

但是正如您在 the tutorial 中看到的那样,使用 "pure" 字节伙伴代码存在一些不便之处。最重要的是,

if you really need to create byte code with jump instructions, make sure to add the correct stack map frames using ASM since Byte Buddy will not automatically include them for you.

我不想那样做。

即使我想,我也试过了

builder = builder
        .defineMethod("<clinit>",void.class, Modifier.STATIC)
        .withParameters(new LinkedList<>())
        .withoutCode()
        ;

我得到的只是一个

Exception in thread "main" java.lang.IllegalStateException: Illegal explicit declaration of a type initializer by class B
    at net.bytebuddy.dynamic.scaffold.InstrumentedType$Default.validated(InstrumentedType.java:901)
    at net.bytebuddy.dynamic.scaffold.MethodRegistry$Default.prepare(MethodRegistry.java:465)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:162)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:155)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2639)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$Delegator.make(DynamicType.java:2741)
    at Main.main(Main.java)

所以我做的是,我在添加所有字段后停止,获取字节码并加载 class。

然后我让 ASM 为我添加方法。 (在实际应用中,反正我也需要运行通过其他一些ASM访问器来获取字节码。)

然后使用 ByteBuddy 重新加载重新检测的字节码。

重新加载失败

Exception in thread "main" java.lang.ClassFormatError
    at sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:170)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy.apply(ClassReloadingStrategy.java:261)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:171)
    at Main.main(Main.java)

原因似乎是 B 在反汇编时看起来像这样:

super public class B
    extends A
    version 51:0
{

public static final Field foo:"Ljava/util/Set;";

public Method "<init>":"()V"
    stack 1 locals 1
{
        aload_0;
        invokespecial   Method A."<init>":"()V";
        return;
}

static Method "<clinit>":"()V";

} // end Class B

将其与 rawClass 字节码进行比较,我们注意到

static Method "<clinit>":"()V";

不存在,确实是由 MethodAdder 添加的。

然而,访客在

返回
return new ClinitReplacer(api,mv,classname);

从未使用过。因此,静态初始化程序主体保持为空,导致错误的 class化为 native.

代码

A.java

import java.util.HashSet;
import java.util.Set;

public class A{
    public static final Set foo;
    static{
        foo = new HashSet<String>();
        foo.add("A");
    }
}

Main.java

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.jar.asm.*;
import net.bytebuddy.jar.asm.commons.InstructionAdapter;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        ByteBuddyAgent.install();

        String targetClassname = "B";
        Class superclass = A.class;


        ByteBuddy byteBuddy = new ByteBuddy();

        DynamicType.Builder builder = byteBuddy
                .subclass(superclass)
                .name(targetClassname)
                ;

        for(Field f : superclass.getFields()){
            builder = builder.defineField(f.getName(),f.getType(),f.getModifiers());
        }

        DynamicType.Unloaded<?> loadable = builder.make();
        byte[] rawClass = loadable.getBytes();
        loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);

        ClassReader cr = new ClassReader(rawClass);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
        cr.accept(ma,ClassReader.EXPAND_FRAMES);

        byte[] finishedClass = cw.toByteArray();

        Class unfinishedClass;
        try {
            unfinishedClass = Class.forName(targetClassname);
        }catch(ClassNotFoundException e){
            throw new RuntimeException(e);
        }

        ClassReloadingStrategy.fromInstalledAgent()
                .load(
                        A.class.getClassLoader(),
                        Collections.singletonMap((TypeDescription)new TypeDescription.ForLoadedType(unfinishedClass), finishedClass)
                );

        Set<String> result;
        try {
            result = (Set<String>)Class.forName("B").getField("foo").get(null);
        }catch(ClassNotFoundException | NoSuchFieldException | IllegalAccessException e){
            throw new RuntimeException(e);
        }
        System.out.println(result);
    }

    private static void store(String name, byte[] finishedClass) {
        Path path = Paths.get(name + ".class");
        try {
            FileChannel fc = null;
            try {
                Files.deleteIfExists(path);
                fc = new FileOutputStream(path.toFile()).getChannel();
                fc.write(ByteBuffer.wrap(finishedClass));
            } finally {
                if (fc != null) {
                    fc.close();
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static class MethodAdder extends ClassVisitor implements Opcodes{

        private static final String INIT_NAME       = "<init>";
        private static final String INIT_DESC       = "()V";

        private static final int CLINIT_ACCESS      = ACC_STATIC;
        private static final String CLINIT_NAME     = "<clinit>";
        private static final String CLINIT_DESC     = "()V";
        private static final String CLINIT_SIG      = null;
        private static final String[] CLINIT_EXCEPT = null;


        public MethodAdder(int api, ClassVisitor cv) {
            super(api, cv);
        }

        private String classname = null;
        private boolean hasStaticInitialiser = false;

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            classname = name;
            hasStaticInitialiser = false;
            cv.visit(version, access, name, signature, superName, interfaces);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            if(mv != null){
                if(!name.equals(CLINIT_NAME)) return mv;
                else{
                    hasStaticInitialiser = true;
                    return new ClinitReplacer(api,mv,classname);
                }
            }else return null;
        }

        @Override
        public void visitEnd() {
            if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
            if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
            super.visitEnd();
        }

        private static class ClinitReplacer extends InstructionAdapter implements Opcodes{
            private final String classname;

            public ClinitReplacer(int api, MethodVisitor mv, String classname) {
                super(api, mv);
                this.classname = classname;
            }

            @Override
            public void visitCode() {
            mv.visitCode();
            InstructionAdapter mv = new InstructionAdapter(this.mv);

            mv.anew(Type.getType(HashSet.class));
            mv.dup();
            mv.dup();
            mv.invokespecial(Type.getInternalName(HashSet.class),INIT_NAME,INIT_DESC,false);
            mv.putstatic(classname,"foo",Type.getDescriptor(Set.class));
            mv.visitLdcInsn(classname);
            mv.invokevirtual(Type.getInternalName(HashSet.class),"add","(Ljava/lang/Object;)Z",false);
            mv.visitInsn(RETURN);
            }
        }
    }
}

问题是您的源 class 文件没有 <clinit> 方法,因此,ASM 根本不会调用 visitMethod

@Override
public void visitEnd() {
    if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
    if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
    super.visitEnd();
}

在这里,如果您之前没有遇到过 <clinit>,那么您正在为 visitMethod 调用它,但是您没有对返回的 MethodVisitor 做任何事情,因此,no-one 正在用它做任何事情。

如果你想把一个不存在的 <clinit> 当作访问一个空的初始化器,进行转换,你必须自己执行适当的方法调用,即

@Override
public void visitEnd() {
    if(!hasStaticInitialiser) {
        MethodVisitor mv = visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
        mv.visitCode();
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }
    if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
    super.visitEnd();
}

但是请注意,您不能进行代码热替换,因为它不支持添加任何方法,包括<clinit>。此外,热代码替换不会(重新)执行 class 初始化程序。

但是在您的代码中,无需在执行 ASM 转换之前加载类型。您可以删除行

loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);

然后只使用生成的 finishedClass 字节码,例如

ClassLoadingStrategy.Default.INJECTION.load(A.class.getClassLoader(),
    Collections.singletonMap(loadable.getTypeDescription(), finishedClass));

请注意,您不会看到太多效果,因为您正在注入创建 HashMap 的代码,但没有对它做任何有用的事情。您可能想将其分配给一个字段……

而且,顺便说一下,您编写字节数组的代码不必要地复杂:

private static void store(String name, byte[] finishedClass) {
    Path path = Paths.get(name + ".class");
    try {
        FileChannel fc = null;
        try {
            Files.deleteIfExists(path);
            fc = new FileOutputStream(path.toFile()).getChannel();
            fc.write(ByteBuffer.wrap(finishedClass));
        } finally {
            if (fc != null) {
                fc.close();
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

只需使用

private static void store(String name, byte[] finishedClass) {
    Path path = Paths.get(name + ".class");
    try {
        Files.write(path, finishedClass);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

“如果不存在则创建”和“overwrite/truncate如果存在”都是默认行为。

要回答有关在 Byte Buddy 中定义类型初始值设定项的部分,可以使用以下方法完成:

builder = builder.invokable(isTypeInitializer()).intercept(...);

您不能显式定义类型初始化器,因为这些初始化器永远不会被反射公开 API,这有助于保持 Byte Buddy 的类型描述模型的连贯性。相反,您匹配类型初始值设定项,Byte Buddy 确保添加一个看起来合适的初始值设定项。