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

时间:2018-02-22 14:23:25

标签: java bytecode instrumentation java-bytecode-asm byte-buddy

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

我正在使用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生成类B

为什么是bytebuddy?好吧,对于一个它应该是方便的,另一个,我需要它的类重新加载功能。

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

  

如果你真的需要用跳转指令创建字节码,请确保使用ASM添加正确的堆栈映射帧,因为Byte Buddy不会自动为你包含它们。

我不想这样做。

即使我想,我也试过

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)

所以我做的是,在我添加了所有字段后,我停下来,获取字节码并加载课程。

然后我让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$1.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);

从未使用过。因此静态初始化主体保持为空,导致错误分类为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);
            }
        }
    }
}

2 个答案:

答案 0 :(得分:2)

问题是您的源类文件没有<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();
}

在这里,如果您之前没有遇到visitMethod,则会调用<clinit>,但是您没有对返回的MethodVisitor执行任何操作,因此,没有人是用它做任何事情。

如果你想要一个缺席的<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>。此外,热代码替换不会(重新)执行类初始化器。

但是在您的代码中,在执行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);
    }
}

两者都是“如果它不存在则创建”和“如果存在则覆盖/截断”是默认行为。

答案 1 :(得分:1)

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

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

您无法显式定义类型初始值设定项,因为这些初始值设定项例如从未被反射API公开,这有助于保持Byte Buddy的类型描述模型一致。相反,您匹配类型初始化程序和Byte Buddy确保添加一个初始化程序,因为它似乎是合适的。