用invokedynamic实现Java多分派(3)——用invokedynamic实现

在本系列的最后一篇我们将用invokedynamic指令来实现Java多分派。

既然Java本身未提供生成invokedynamic指令的接口,我们便只好借助于字节码操纵工具了,这里用的是ASM。ASM的MethodVisitor提供了visitInvokeDynamicInsn方法来生成该指令。

我们将继续使用本系列第一篇中的Friendly接口家族。基本思路是:把那个最简单的Main类(也就是new出各个男女,互相sayHello的主程序)的字节码,用ASM改写,所有的sayHello调用(invokeinterface)都替换成invokedynamic。把invokedynamic需要的启动方法(bootstrap method,BSM)放到一个类里,作为静态方法存在。BSM可看作invokedynamic调用其它方法的中间代理人。invokedynamic指令的参数和BSM的概念请具体参考官方文档。在BSM所在的类里我们将用MethodHandle家族API找出符合运行期参数类型的方法,并调用之。

原始Main类如下:

package multidispatch.invokedynimpl;

import multidispatch.Friendly;
import multidispatch.Man;
import multidispatch.Woman;

public class Main {

    public static void main(String[] args) {
        Friendly tom = new Man("Tom");
        Friendly jerry = new Man("Jerry");
        Friendly jessie = new Woman("Jessie");
        Friendly mary = new Woman("Mary");
        
        // This is single-dispatch, multi-dispatch is in MainHacker via invokedynamic instrumentation
        tom.sayHelloTo(jerry);
        jerry.sayHelloTo(mary);
        jessie.sayHelloTo(mary);
        mary.sayHelloTo(tom);
    }

}

然后我们用如下指令来生成Main类的dump类:

java org.objectweb.asm.util.ASMifier Main.class

这样就生成一个MainDump的类,是ASM为我们生成的visitor模式的字节码访问类。我们将之重命名为MainHacker,并修改如下:

package multidispatch.invokedynimpl;

import java.lang.invoke.CallSite;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Generated by "java org.objectweb.asm.util.ASMifier Main.class" and renamed from MainDump to MainHacker.
 * Changed invokeinterface instructions to invokedynamic ones.
 *
 */
public class MainHacker implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter classWriter = new ClassWriter(0);
        //FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;
        //AnnotationVisitor annotationVisitor0;

        classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "multidispatch/invokedynimpl/Main", null, "java/lang/Object", null);

        classWriter.visitSource("Main.java", null);

        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "", "()V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(7, label0);
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
            methodVisitor.visitInsn(RETURN);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLocalVariable("this", "Lmultidispatch/invokedynimpl/Main;", null, label0, label1, 0);
            methodVisitor.visitMaxs(1, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
            methodVisitor.visitCode();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(10, label0);
            methodVisitor.visitTypeInsn(NEW, "multidispatch/Man");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitLdcInsn("Tom");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "multidispatch/Man", "", "(Ljava/lang/String;)V", false);
            methodVisitor.visitVarInsn(ASTORE, 1);
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(11, label1);
            methodVisitor.visitTypeInsn(NEW, "multidispatch/Man");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitLdcInsn("Jerry");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "multidispatch/Man", "", "(Ljava/lang/String;)V", false);
            methodVisitor.visitVarInsn(ASTORE, 2);
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(12, label2);
            methodVisitor.visitTypeInsn(NEW, "multidispatch/Woman");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitLdcInsn("Jessie");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "multidispatch/Woman", "", "(Ljava/lang/String;)V", false);
            methodVisitor.visitVarInsn(ASTORE, 3);
            Label label3 = new Label();
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(13, label3);
            methodVisitor.visitTypeInsn(NEW, "multidispatch/Woman");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitLdcInsn("Mary");
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "multidispatch/Woman", "", "(Ljava/lang/String;)V", false);
            methodVisitor.visitVarInsn(ASTORE, 4);
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(16, label4);
            methodVisitor.visitVarInsn(ALOAD, 1);
            methodVisitor.visitVarInsn(ALOAD, 2);
            //methodVisitor.visitMethodInsn(INVOKEINTERFACE, "multidispatch/Friendly", "sayHelloTo", "(Lmultidispatch/Friendly;)V", true);
            redispatchCall(methodVisitor);
            Label label5 = new Label();
            methodVisitor.visitLabel(label5);
            methodVisitor.visitLineNumber(17, label5);
            methodVisitor.visitVarInsn(ALOAD, 2);
            methodVisitor.visitVarInsn(ALOAD, 4);
            //methodVisitor.visitMethodInsn(INVOKEINTERFACE, "multidispatch/Friendly", "sayHelloTo", "(Lmultidispatch/Friendly;)V", true);
            redispatchCall(methodVisitor);
            Label label6 = new Label();
            methodVisitor.visitLabel(label6);
            methodVisitor.visitLineNumber(18, label6);
            methodVisitor.visitVarInsn(ALOAD, 3);
            methodVisitor.visitVarInsn(ALOAD, 4);
            //methodVisitor.visitMethodInsn(INVOKEINTERFACE, "multidispatch/Friendly", "sayHelloTo", "(Lmultidispatch/Friendly;)V", true);
            redispatchCall(methodVisitor);
            Label label7 = new Label();
            methodVisitor.visitLabel(label7);
            methodVisitor.visitLineNumber(19, label7);
            methodVisitor.visitVarInsn(ALOAD, 4);
            methodVisitor.visitVarInsn(ALOAD, 1);
            //methodVisitor.visitMethodInsn(INVOKEINTERFACE, "multidispatch/Friendly", "sayHelloTo", "(Lmultidispatch/Friendly;)V", true);
            redispatchCall(methodVisitor);
            Label label8 = new Label();
            methodVisitor.visitLabel(label8);
            methodVisitor.visitLineNumber(20, label8);
            methodVisitor.visitInsn(RETURN);
            Label label9 = new Label();
            methodVisitor.visitLabel(label9);
            methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label9, 0);
            methodVisitor.visitLocalVariable("tom", "Lmultidispatch/Friendly;", null, label1, label9, 1);
            methodVisitor.visitLocalVariable("jerry", "Lmultidispatch/Friendly;", null, label2, label9, 2);
            methodVisitor.visitLocalVariable("jessie", "Lmultidispatch/Friendly;", null, label3, label9, 3);
            methodVisitor.visitLocalVariable("mary", "Lmultidispatch/Friendly;", null, label4, label9, 4);
            //methodVisitor.visitMaxs(3, 5);//too small after editing, make it bigger
            methodVisitor.visitMaxs(10, 10);
            methodVisitor.visitEnd();
        }
        classWriter.visitEnd();

        return classWriter.toByteArray();
    }
    
    public static void redispatchCall(MethodVisitor mv) {
        MethodType mt = MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
        
        Handle bootstrap = new Handle(Opcodes.H_INVOKESTATIC, "multidispatch/invokedynimpl/SayHelloRedispatcher", 
                  "bootstrap", mt.toMethodDescriptorString(), false);
        
        // Two parameters, the 1st is the receiver
        mv.visitInvokeDynamicInsn("sayHelloTo", "(Lmultidispatch/Friendly;Lmultidispatch/Friendly;)V", bootstrap);
    }
    
    private static class MyClassLoader extends ClassLoader implements Opcodes {
        public Class defineClass(String name, byte[] b) {
            return super.defineClass(name, b, 0, b.length);
        }
    }
    
    public static void main(String[] args) throws Exception {
        byte[] code = dump();
        Class clazz = new MyClassLoader().defineClass("multidispatch.invokedynimpl.Main", code); //or save the byte[] as a .class file, replacing the existing one
        //run main() by reflection in re-defined class, or issue a direct java command if the byte[] is saved
        clazz.getDeclaredMethod("main", String[].class).invoke(null,new Object[]{new String[]{""}});
    }
}

可以看到,那4个INVOKEINTERFACE的指令都被我们替换成了redispatchCall调用。而在redispatchCall方法里,我们指定了BSM,生成了invokedynamic指令。最后,我们在main函数里调用了dump(),生成新的Main字节码。此时我们可以将这个byte数组写回磁盘,取代原来那个Main.class;也可以用一个class loader直接重定义Main并用反射调用它。我们用了第二种方法。

现在到了本系列的重点和难点,BSM。我们将先写出存放这个BSM的类,然后分析它:

package multidispatch.invokedynimpl;

import static java.lang.invoke.MethodHandles.lookup;
import static java.lang.invoke.MethodType.methodType;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;

public class SayHelloRedispatcher {
    
    private static final MethodHandle MYDISP;
    
    static {
        try {
            MYDISP = lookup().findStatic(SayHelloRedispatcher.class, "myDispatcher",
                methodType(MethodHandle.class, MethodHandle.class, String.class, Object.class, Object.class));
        } catch(NoSuchMethodException|IllegalAccessException ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static CallSite bootstrap(Lookup lookup, String name, MethodType type) {
        MethodHandle invoker = MethodHandles.invoker(type); //MethodHandle(MethodHandle,Friendly,Friendly)void
        MethodHandle target;
        try {
            //target: Friendly.sayHelloTo(Friendly)V
            target = lookup.findVirtual(type.parameterType(0), name, 
				type.dropParameterTypes(0, 1)); //MethodHandle(Friendly,Friendly)void
            
        } catch(NoSuchMethodException | IllegalAccessException ex) {
            throw new BootstrapMethodError(ex);
        }
		
        //MethodHandle(Object,Object)MethodHandle
        MethodHandle curried = MethodHandles.insertArguments(MYDISP, 0, target, name); 
		
        //MethodHandle(Friendly,Object)MethodHandle
        MethodHandle changedParam = curried.asType(curried.type().changeParameterType(0, type.parameterType(0))); 
		
        //MethodHandle(Friendly,Friendly)MethodHandle
        changedParam = changedParam.asType(changedParam.type().changeParameterType(1, type.parameterType(1))); 

        target = MethodHandles.foldArguments(invoker, changedParam); //MethodHandle(Friendly,Friendly)void
        return new ConstantCallSite(target);
    }
    
    public static MethodHandle myDispatcher(MethodHandle invokeInterfaceTarget, String methodName, 
			Object rec, Object param) {
        if(rec != null && param != null) {
            try {
                Class ret = invokeInterfaceTarget.type().returnType(); //void in our case
                MethodHandle redispatched = lookup().findVirtual(rec.getClass(), methodName, 
                    methodType(ret, param.getClass())); //e.g. MethodHandle(Man,Woman)void
                return redispatched;
            } catch(NoSuchMethodException | IllegalAccessException ex) {
                //ex.printStackTrace();
            }
        }
        return invokeInterfaceTarget;
    }

}

这里,bootstrap方法就是BSM,它接受的参数和返回类型都是规定好的。而myDispatcher则是具体实现将父类调用变成子类调用(多分派)的地方。

对于myDispatcher这个方法,类里保存了一个静态引用MYDISP,这样免得每次在BSM里再找。

我们先看myDispatcher方法。它接受4个参数,第一个是目标方法,也就是原先要进行invokeinterface调用的sayHelloTo方法;第二个是方法名,即"sayHelloTo";第三个是sayHelloTo的receiver;第四个是sayHelloTo的参数。在我们这里,后两个都是Friendly对象。该方法完成的功能很简单,就是把本来要调用的目标函数,替换成根据receiver和实参的运行时类型找出来的另一个函数,然后返回这个函数的句柄。

然后我们重点来看BSM。它的第一个参数就是MethodHandles.lookup(),是固定的。第二个无疑是"sayHelloTo"。最后一个参数的值则会是(Friendly,Friendly)void,这是因为我们在MainHacker里写的那个"(Lmultidispatch/Friendly;Lmultidispatch/Friendly;)V"。其中第一个Friendly是receiver,第二个是调用参数,这是栈帧结构决定的。BSM的返回值是一个CallSite,它是个MethodHandle的容器,存放最终要调用的那个函数的句柄。

在BSM中,我们首先声明了一个invoker,它可以调用任何与第三个参数type相容的函数,只不过它的类型是MethodHandle(MethodHandle,Friendly,Friendly)void,即,相比起type对应的类型,它的参数列表里多了一个MethodHandle,联想到上一篇中的foldArgument,我们是想借助它作为一个壳,来combine另一个函数。接着,用lookup从receiver里找出sayHelloTo函数(Friendly.sayHelloTo(Friendly)V),通过insertArguments把它赋给myDispatcher函数的第一个参数;同时也把函数名赋给第二个参数。insertArguments之后返回的MethodHandle变成了MethodHandle(Object,Object)MethodHandle,为了使它与invoker匹配,我们用了两次asType方法,将这两个Object转成Friendly。读者可能问,为什么不直接将myDispatcher后两个参数声明为Friendly呢?这是因为我们试图将这个类做成一个通用invokedynamic拦截器,您应该能注意到,SayHelloRedispatcher没有import除java.lang.invoke包以外的任何类。

最后,我们将这个放入了两个参数的MYDISP,也就是myDispatcher函数,作为combiner,与那个invoker结合到一起。根据上一篇的介绍,foldArguments会将combiner的结果作为invoker的条件,在这里,它的结果是一个MethodHandle,也就是最终要调用的子类sayHelloTo函数,而它就成了invoker的第一个参数,从而达到了替换目标方法的过程。而reciever和实际参数会被myDispatcher的后两个参数捕获。

做完这一切后,运行MainHacker或者替换过字节码的Main,就会发现sayHelloTo调用都已经变成了多分派的。

你可能感兴趣的:(java)