如果 class 引用原始类型,则访问通过 ASM 生成的 class 的构造函数的所有反射方法都会抛出 NoClassDefFoundError

All reflection methods accessing constructor of class generated through ASM throw NoClassDefFoundError if class references primitive type

我正在编写一个应用程序,其中将具有特定签名的反射方法对象解包到通过 ASM 生成的 classes 中的常规 INVOKEVIRTUAL 调用,以便可以以更注重性能的方式重复调用这些方法。要解包的方法将始终具有特定的 return 类型和第一个参数,但在该点之后可以具有任何给定数量的任何类型的其他参数。

我定义了两个 class 来执行此操作,InvokerProxyNewInvokerProxyFactory

public interface InvokerProxy {
    ExitCode execute(IODescriptor io, Object... args);
}

public final class NewInvokerProxyFactory {

    private static final String GENERATED_CLASS_NAME = "InvokerProxy";

    private static final Map<Class<?>, Consumer<MethodVisitor>> UNBOXING_ACTIONS;

    private static final AtomicInteger NEXT_ID = new AtomicInteger();

    private NewInvokerProxyFactory() {}

    public static InvokerProxy makeProxy(Method backingMethod, Object methodParent) {
        String proxyCanonicalName = makeUniqueName(InvokerProxyFactory.class.getPackage(), backingMethod);
        String proxyJvmName = proxyCanonicalName.replace(".", "/");

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;

        cw.visit(V1_8, ACC_PUBLIC | ACC_SUPER, proxyJvmName, null, Type.getInternalName(Object.class), new String[]{Type.getInternalName(InvokerProxy.class)});

        cw.visitSource("<dynamic>", null);

        {
            fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, "parent", Type.getDescriptor(Object.class), null, null);
            fv.visitEnd();
        }

        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Object.class)), null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitFieldInsn(PUTFIELD, proxyJvmName, "parent", Type.getDescriptor(Object.class));
            mv.visitInsn(RETURN);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }

        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_VARARGS, "execute", Type.getMethodDescriptor(Type.getType(ExitCode.class), Type.getType(IODescriptor.class), Type.getType(Object[].class)), null, null);
            mv.visitCode();

            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, proxyJvmName, "parent", Type.getDescriptor(Object.class));
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(methodParent.getClass()));
            mv.visitVarInsn(ALOAD, 1);

            Class<?>[] paramTypes = backingMethod.getParameterTypes();
            for (int i = 1; i < paramTypes.length; i++) {
                mv.visitVarInsn(ALOAD, 2);
                mv.visitLdcInsn(i-1);
                mv.visitInsn(AALOAD);
                mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i]));
                if (paramTypes[i].isPrimitive()) {
                    UNBOXING_ACTIONS.get(paramTypes[i]).accept(mv);
                }
            }

            mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(methodParent.getClass()), backingMethod.getName(), Type.getMethodDescriptor(backingMethod), false);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(backingMethod.getParameterTypes().length + 2, 3);
            mv.visitEnd();
        }
        cw.visitEnd();

        try {
            return (InvokerProxy) SystemClassLoader.defineClass(proxyCanonicalName, cw.toByteArray()).getDeclaredConstructor(Object.class).newInstance(methodParent);
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new InvokerProxyGenerationException("Exception creating invoker proxy for method '" + backingMethod + "'", e);
        }
    }

    private static String makeUniqueName(Package parentPackage, Method method) {
        return String.format("%s.%s_%d", parentPackage.getName(), GENERATED_CLASS_NAME, NEXT_ID.getAndIncrement());
    }

    static {
        Map<Class<?>, Consumer<MethodVisitor>> actions = new HashMap<>();
        actions.put(Byte.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Byte.class), "byteValue", "()B", false));
        actions.put(Short.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Short.class), "shortValue", "()S", false));
        actions.put(Integer.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Integer.class), "intValue", "()I", false));
        actions.put(Long.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Long.class), "longValue", "()J", false));
        actions.put(Float.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Float.class), "floatValue", "()F", false));
        actions.put(Double.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Double.class), "doubleValue", "()D", false));
        actions.put(Boolean.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Boolean.class), "booleanValue", "()Z", false));
        actions.put(Character.TYPE, mv -> mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Character.class), "charValue", "()C", false));
        UNBOXING_ACTIONS = actions;
    }
}

通过测试我发现,如果被 InvokerProxyFactory 解包的方法有任何原始参数(int、char、float 等),尝试通过任何方法查找 class 的构造函数通常提供的反射方法(Class.getConstructorsClass.getDeclaredConstructor 等)的一部分将导致 java.lang.NoClassDefFoundError 引用方法签名中找到的第一个原始类型作为其消息。异常显然是由 URLClassLoader.findClass 引起的,其中 ClassNotFoundException 与相同的消息一起抛出。

显然这个问题甚至超越了构造函数,因为甚至 Unsafe.allocateInstance 在创建生成的 class 的实例时也会抛出同样的异常。当展开的方法没有任何原始参数时,查找构造函数或创建实例也绝对没有问题。

下面的代码看起来很可疑

mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i]));

此代码被无条件调用,即使 paramTypes[i] 是基本类型。但是,ASM documentation 表示 getInternalName 只能为真实对象或数组类型调用。 ASM 可能只是在给定原语时生成一个伪造的 class 名称,因此出现错误。

public static String getInternalName(Class c)

Returns the internal name of the given class. The internal name of a class is its fully qualified name, as returned by Class.getName(), where '.' are replaced by '/'.

Parameters:

c - an object or array class.

Returns:

the internal name of the given class.

另外,请注意 CHECKCAST 指令对基本类型无效。