Search code examples
javareflectionconstructorbytecodejava-bytecode-asm

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


I am writing an application in which reflected Method objects with specific signatures are unwrapped to regular INVOKEVIRTUAL calls in classes generated through ASM so that those methods can be repeatedly invoked in a more performance-conscious manner. Methods to be unwrapped will always have a specific return type and first parameter, but can have any given number of other parameters of any type past that point.

I have defined two classes to do this, InvokerProxy and NewInvokerProxyFactory.

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;
    }
}

Through testing I have discovered that if the method being unwrapped by the InvokerProxyFactory has any primitive parameters (int, char, float, etc..), attempting to look up a constructor for that class through any of the normally provided reflection methods (Class.getConstructors, Class.getDeclaredConstructor, etc...) will result in a java.lang.NoClassDefFoundError citing the first primitive type found in the method signature as its message. The exception is apparently caused by URLClassLoader.findClass, where a ClassNotFoundException is thrown with the same message.

Apparently this issue going even beyond constructors since even Unsafe.allocateInstance throws this same exception when creating an instance of the generated class. There are also absolutely no issues looking up constructors or creating instances when the unwrapped method does not have any primitive parameters.


Solution

  • The following code looks very suspicious

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

    This code is called unconditionally, even if paramTypes[i] is a primitive type. However, the ASM documentation says that getInternalName can only be called for a real object or array type. ASM probably just generates a bogus class name when given a primitive, hence the error.

    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.

    Also, note that the CHECKCAST instruction is not valid for primitive types anyway.