首页 > 代码库 > JDK7动态方法调用

JDK7动态方法调用

   在JDK7中,Java提供了对动态语言特性的支持,实现了JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》规范,这是Java语言发展的一重大进步,而提供对动态语言特性支持也是Java发展的一大趋势与方向。那么动态性表现在哪里呢?其一在Java API层面,新增了java.lang.invoke包,主要包含了CallSite、MethodHandle、MethodType等类;其二,在Java字节码指令层面,新增了invokedynamic指令,而伴随invokedynamic指令新增而在Class类文件常量池中新增了CONSTANT_InvokeDynamic_info, CONSTANT_MethodHandle_info, CONSTANT_MethodType_info常量表、新增BootstrapMethods属性表。
   那么什么是动态性,与动态相对的即是静态。大家应该都听说过,Java是一门静态型语言(C++也是),而动态型语言有Groovy、JavaScript、Ruby、Phthon、PHP、Lisp等等。从这个列举中可以发现,动态语言一大堆,而静态语言最常见的就是Java和C++了,这也从侧映证了动态性是语言发展的趋势。这些动态型语言在语言语法层面上最大的特点就是变量用var/def声明;而是Java中,声明变量时必须指定该变量的类型,如:String name = "zhangsan"。 从深层次一点来讲,动态型语言声明的变量在编译期无法确定该变量的具体类型,只有到了运行时才能确定该变量的具体类型,而静态型语言,变量的具体类型(这里指变量静态类型,多态性不包含在此)在编译期就已经确定,以Java为例:声明了一个实例变量,经过编译器编译后,该变量的简单名称和描述符符号引用都已经存储在了Class文件字节码中,在类的解析阶段,虚拟机就会将变量的符号引用解析为直接引用。这个直接引用将会解析为什么类型,在编译期就已经确定了。下面举个具体的例子:
动态语言以JavaScript为例
function Output() {
}
Output.prototype.println = function(_value) {
	console.info(_value);//FireFox中
}
//执行语句
var output = new Output();
output.println();

在上述代码中,Output类型对象有一个println方法,而在实际运行中,变量output却不一定非得是Output类型。只要output变量指向的对象(方法的接收者)上有println方法就可以,而不管接收者到底是什么类型。

而在Java中,以最有名的HelloWorld为例:

public static void main(String[] args) {
	System.out.println("HelloWorld");
}

System.out被声明为java.io.PrintStream类型,所以System.out对象就必须是java.io.PrintStream类型或者java.io.PrintStream的子类。这里你也许会话,这不对旬真正类型也是可以改变的嘛。的确,但是这个“改变”却有很大的限制,对象真实类型必须是声明类型或者声明类型的子类型,这个是语言多态性最基础的保证。而JDK7对动态性的支持希望做到的是类型JavaScript中一样,对象的真实类型由运行期确定。

下面就举一个使用java.lang.invoke包完成动态方法调用的例子:

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Random;

public class Output {
	
	public void println(Object value) {
		System.out.println("value=http://www.mamicode.com/" + value);>

你会发现这时候,无论receiver是什么类型的对象,只要其有一个名为println带一个参数的方法,程序就可以运行,这样就可以实现方法接收者的动态确定。
从上面的例子看来,Java动态性使用很简单,不过看完它的用法之后,大家也许会有疑问,相同的事情,用反射不是早就可以实现了吗?确实,仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:

1. Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual & invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。


2. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。


3. 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
   MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。

   在JDK7以前,用于方法调用的字节码指令只有invokeinterface、invokestatic、invokespecial、invokevitual四个,这四个指令的具体含义与行为可以参看具体资料,最权威的当然是Java虚拟机规范。这四个指令方法调用时概括有4个要素:
1.方法名称:要调用的方法的名称一般是由开发人员在源代码中指定的符号名称。这个名称同样会出现在编译之后的字节代码中。
2.链接:链接包含了要调用方法的类。这一步有可能会涉及类的加载。
3.选择:选择要调用的方法。在类中根据方法名称和参数选择要调用的方法。
4.适配:调用者和接收者对调用的方式达成一致,即对方法的类型声明达成共识。
确定了上面4个要素之后,Java虚拟机会把控制权转移到被调用的方法中,并把调用时的实际参数传递过去。

   这4个指令在动态性方面颇具有短板,如方法名称与描述符在字节码中就已经确定下来无法改变,4个指令都带有一个运行时常量池索引的参数,指向一个CONSTANT_Methodref_info表,CONSTANT_Methodref_info表包含了该方法所在类的信息(CONSTANT_Class_info索引),这样,方法所在类也不能动态变化,也就决定了方法的接收者不可动态改变(多态性不包含在此)。
在JDK7中,添加了invokedynamic指令,指令格式如下:
技术分享
   indexbyte1与indexbyte2组成一个运行时常量池索引,索引处为一个 CONSTANT_InvokeDynamic表,也被称为调用点描述符(call site specifier),第3与第4个操作数必须为0。CONSTANT_InvokeDynamic表中包含了启动方法(bootstrap method)、动态连接方法名称返回值参数列表等信息。需要注意的一点是CONSTANT_InvokeDynamic表bootstrap_method_attr_index数据项不是运行时常量池的索引,而是BootstrapMethods属性中包含的启动方法数组索引,具体可以查阅Java虚拟机规范中的invokedynamic指令的详细介绍,还有这一篇文章。

invokedynamic指令放宽了方法调用的限制,提升了方法调用的灵活性,以上面方法调用的四个要素来说明:
1.在方法的名称方面,不一定是符合Java命名规范的字符串,可以任意指定。方法的调用者和提供者也不需要在方法名称上达成一致。
2.提供了更加灵活的链接方式。一个方法调用所实际调用的方法可以在运行时再确定。这就相当于把链接操作推迟到了运行时,而不是必须在编译时就确定下来。对于一个已经链接好的方法调用,也可以重新进行链接,让它指向另外的方法。
3.在方法选择方面,不再是只能在方法调用的接收者上进行发派,而是可以考虑所有调用时的参数,即支持方法的多派发。
4.在调用之前,可以对参数进行各种不同的处理,包括类型转换、添加和删除参数、收集和分发可变长度参数等。

如果将上面动态方法调用的例子执行javap命令后,得到如下结果(getMethodHandle与main方法):
public static java.lang.invoke.MethodHandle getMethodHandle(java.lang.Object) throws java.lang.Throwable;
    flags: ACC_PUBLIC, ACC_STATIC

    Exceptions:
      throws java.lang.Throwable
    Code:
      stack=4, locals=3, args_size=1
         0: invokestatic  #48                 // Method java/lang/invoke/MethodHandles.lookup:()Ljava/lang/invoke/MethodHandles$Lookup;
         3: astore_1      
         4: getstatic     #54                 // Field java/lang/Void.TYPE:Ljava/lang/Class;
         7: ldc           #3                  // class java/lang/Object
         9: invokestatic  #60                 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
        12: astore_2      
        13: aload_1       
        14: aload_0       
        15: invokevirtual #66                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
        18: ldc           #70                 // String println
        20: aload_2       
        21: invokevirtual #71                 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
        24: aload_0       
        25: invokevirtual #77                 // Method java/lang/invoke/MethodHandle.bindTo:(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
        28: areturn       
      LineNumberTable:
        line 16: 0
        line 18: 4
        line 20: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      29     0 receiver   Ljava/lang/Object;
               4      25     1 lookup   Ljava/lang/invoke/MethodHandles$Lookup;
              13      16     2 methodType   Ljava/lang/invoke/MethodType;

  public static void main(java.lang.String[]) throws java.lang.Throwable;
    flags: ACC_PUBLIC, ACC_STATIC

    Exceptions:
      throws java.lang.Throwable
    Code:
      stack=2, locals=2, args_size=1
         0: new           #87                 // class java/util/Random
         3: dup           
         4: invokespecial #89                 // Method java/util/Random."<init>":()V
         7: sipush        1000
        10: invokevirtual #90                 // Method java/util/Random.nextInt:(I)I
        13: iconst_2      
        14: irem          
        15: ifne          24
        18: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        21: goto          31
        24: new           #1                  // class com/xtayfjpk/asm/test/dynamicinvoke/demo/Output
        27: dup           
        28: invokespecial #94                 // Method "<init>":()V
        31: astore_1      
        32: aload_1       
        33: invokestatic  #95                 // Method getMethodHandle:(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle;
        36: ldc           #97                 // String Hello Dynamic Invoke
        38: invokevirtual #99                 // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/String;)V
        41: return        
      LineNumberTable:
        line 26: 0
        line 28: 32
        line 29: 41
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      42     0  args   [Ljava/lang/String;
              32      10     1 receiver   Ljava/lang/Object;
      StackMapTable: number_of_entries = 2
           frame_type = 24 /* same */
           frame_type = 70 /* same_locals_1_stack_item */
          stack = [ class java/lang/Object ]


   你会发现,在Code属性中,完全找不到invokedynamic指令的影子,这是因为invokedynamic指令是提供给动态编译器使用的,而我们编译时用的是javac编译器,javac它不会生成invokedynamic指令。作为程序员,更多的还是使用java.lang.invoke包中的类来完全方法的动态调用,如果你实现了某种动态编译器在Code属性中生成了invokedynamic指令,虚拟机照样是可以正常执行的。

   在字节代码中每个出现的invokedynamic指令都成为一个动态调用点(dynamic call site)。每个动态调用点在初始化的时候,都处于未链接的状态。在这个时候,这个动态调用点并没有被指定要调用的实际方法。当虚拟机要执行dynamic指令时,首先要链接到动态调用点,而动态调用点是由一个被称为启动方法(bootstrap)的方法确定的,启动方法的返回值就是CallSite。CallSite上会绑定一个MethodHandle,称为CallSite的目标,通过MethodHandle方法句柄就可以定位到真正在执行的方法。也就是说,对invokedynamic指令的调用实际上就等价于对方法句柄的调用,具体来说是被转换成对方法句柄的invoke方法的调用。
   在JDk7中一共提供了三种CallSite,分别是ConstantCallSite,MutableCallSite与VolatileCallSite,这三个类都是CallSite的子类,ConstantCallSite的特点是其目标绑定是永久的,一但绑定就不能再进行更改,也就是一条invokedynamic指令链接上了一个ConstantCallSite后,其MethodHandle方法句柄不能再改变。MutableCallSite与ConstantCallSite是相对的,其目标绑定后可以进行更改。VolatileCallSite的目标有类似volatile变量的特点,当invokedynamic链接到一个VolatileCallSite调用点时,调用点的目标的更改invokedynamic指令立即就可以观察得到,即使这个更改是在其它线程中完成的。

   下面我们就手动生成invokedynamic指令,看看虚拟机是否可以正常执行:
由于javac编译生成的字节码中不包含invokedynamic指令,所以我们无法看到。尽管如此,我们可以使用字节码生成工具(如ASM)来手动生成。

import static org.objectweb.asm.Opcodes.*;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.invoke.MethodHandles.Lookup;
import java.nio.file.Files;
import java.nio.file.Paths;

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

public class DynamicInvokeInstructionGenerator {
	
	//启动方法定义
	public static CallSite bootstrap(Lookup lookup, String name, MethodType type, String value) throws Exception {
		MethodHandle handle = lookup.findVirtual(StringBuilder.class, name, MethodType.methodType(StringBuilder.class)).bindTo(new StringBuilder(value));
		return new ConstantCallSite(handle);
	}
	
	//ASM中定义的方法句柄
	private static final Handle BSM = new Handle(
		H_INVOKESTATIC, 
		DynamicInvokeInstructionGenerator.class.getName().replace('.', '/'), 
		"bootstrap", 
		MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class, String.class).toMethodDescriptorString());
	
	public static void main(String[] args) throws Exception {
		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
		cw.visit(V1_7, ACC_PUBLIC|ACC_SUPER, "StringReverser", null, "java/lang/Object", null);
		//生成main方法
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
		mv.visitCode();
		mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
		//调用StringBuilder的reverse方法
		mv.visitInvokeDynamicInsn("reverse", "()Ljava/lang/StringBuilder;", BSM, "Hello Dynamic Invoke");//生成invokedynamic指令
		//调用System.out.println(Object x)
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
		cw.visitEnd();
		
		
		Files.write(Paths.get("StringReverser.class"), cw.toByteArray());
		
	}
}

只是为了书写方便,就把启动方法,定义方法句柄,生成字节码的代码写在一个类中了,对生成的StringReverser类执行javap命令后得到如果结果:

{
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokedynamic #25,  0             // InvokeDynamic #0:reverse:()Ljava/lang/StringBuilder;
         8: invokevirtual #31                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        11: return
}

可以看到,invokedynamic指令的确生成了,执行该类得到输出:ekovnI cimanyD olleH,正确地执行了字符串反序操作。

至此手动生成字节码成功并顺序执行。

关于启动方法签名,java.lang.invoke包中类的更详细信息可参看:http://docs.oracle.com/javase/7/docs/api/java/lang/invoke/package-summary.html

JDK7动态方法调用