在本系列的最后一篇我们将用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, "<init>", "()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", "<init>", "()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", "<init>", "(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", "<init>", "(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", "<init>", "(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", "<init>", "(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调用都已经变成了多分派的。