>我正在编写一个应用程序,其中具有特定签名的反射方法对象被解包为通过 ASM 生成的类中的常规 INVOKEVIRTUAL 调用,以便可以以更注重性能的方式重复调用这些方法。要解包的方法将始终具有特定的返回类型和第一个参数,但可以具有超过该点的任何类型的任何给定数量的其他参数。
我定义了两个类来执行此操作,InvokerProxy
和 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;
}
}
通过测试,我发现如果 InvokerProxyFactory 解包的方法有任何基元参数(int、char、float 等),尝试通过任何通常提供的反射方法(Class.getConstructors
、Class.getDeclaredConstructor
等)查找该类的构造函数将导致java.lang.NoClassDefFoundError
引用方法签名中的第一个基元类型作为其消息。该异常显然是由URLClassLoader.findClass
引起的,其中抛出具有相同消息的ClassNotFoundException
。
显然,这个问题甚至超出了构造函数的范围,因为即使Unsafe.allocateInstance
在创建生成的类的实例时也会抛出相同的异常。当解包的方法没有任何基元参数时,查找构造函数或创建实例也绝对没有问题。
以下代码看起来很可疑
mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramTypes[i]));
此代码是无条件调用的,即使paramTypes[i]
是基元类型也是如此。但是,ASM 文档说只能为实际对象或数组类型调用getInternalName
。ASM 在给定原语时可能只会生成一个虚假的类名,因此会出现错误。
public static String getInternalName(Class c)
返回内部给定类的名称。类的内部名称是其完整的限定名称,由 Class.getName() 返回,其中"." 被替换通过"/"。
参数:C - 对象或数组类。
返回:给定类的内部名称。
另外,请注意,无论如何,CHECKCAST 指令对基元类型都无效。