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=" + value);
	}

	public static MethodHandle getMethodHandle(Object receiver) throws Throwable {
		//如果Lookup对象
		MethodHandles.Lookup lookup = MethodHandles.lookup();
		//MethodType代表方法的类型(不包含方法名称),其实MethodType是为了确定方法的描述符,例如此方法描述符为:(Ljava/lang/Object;)V
		MethodType methodType = MethodType.methodType(void.class, Object.class);
		//在接收者类中查找一个名为println,指定方法类型的虚方法
		return lookup.findVirtual(receiver.getClass(), "println", methodType).bindTo(receiver);
	}

	public static void main(String[] args) throws Throwable {
		Object receiver = new Random().nextInt(1000)%2==0 ? System.out : new Output();
		// 无论receiver最终是什么类型,只要有println方法,方法就可以正常调用。
		getMethodHandle(receiver).invoke("Hello Dynamic Invoke");
	}
}

你会发现这时候,无论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

时间: 2024-12-27 09:31:22

JDK7动态方法调用的相关文章

Struts2学习第七课 动态方法调用

动态方法调用:通过url动态调用Action中的方法. action声明: <package name="struts-app2" namespace="/" extends="struts-default"> <action name="Product" class="org.simpleit.app.Product"> </package> URI: --/strut

动态方法调用

1.先建立一个项目 2.在此项目中需要建立两个jsp 1)在第一个jsp中写入一句话 <body> User Add Success! </body> 2)在第二个jsp中写入链接 <body> Action执行的时候并不一定要执行execute方法<br /> 可以在配置文件中配置Action的时候用method=来指定执行哪个方法 也可以在url地址中动态指定(动态方法调用DMI)(推荐)<br /> <a href="<

struts2中通配符和DMI(动态方法调用)

在struts2中不建议使用Dynamic Method Invocation,具体原因见官方文档: http://struts.apache.org/docs/action-configuration.html#ActionConfiguration-WildcardMethod; 刚刚接触这块,所以两种方法各自实现一下: 1)动态方法调用: struts.xml文件: <package name="default" namespace="/yin" ext

Struts2动态方法调用(DMI)

在Struts2中动态方法调用有三种方式,动态方法调用就是为了解决一个Action对应多个请求的处理,以免Action太多 第一种方式:指定method属性这种方式我们前面已经用到过,类似下面的配置就可以实现 <action name="chainAction" class="chapter2.action.Chapter2Action" method="chainAction"> <result name="chai

C#动态方法调用

此篇将介绍C#如何在运行时动态调用方法.当某些类型是运行时动态确定时,编译时的静态编码是无法解决这些动态对象或类的方法调用的.此篇则给你一把利剑,让动态对象的方法调用成为可能. 1.动态调用dll里的方法: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

struts2.5动态方法调用和默认Action

在动态方法调用中,使用通配符方法出现问题,参考了http://www.cnblogs.com/jasonlixuetao/p/5933671.html 这篇博客,问题解决了. 这个是helloworld.xml: 1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE struts PUBLIC 3 "-//Apache Software Foundation//DTD Struts Co

ActionMethod_DMI_动态方法调用

Action执行的时候并不一定要执行execute方法可以在配置文件中配置Action的时候用method=来指定执行那个方法,也可以在url地址中动态指定(动态方法调用DMI)(推荐) 动态方法调用的配置要先打开: 1 <constant name="struts.enable.DynamicMethodInvocation" value="true"/> index.jsp 1 <%@ page language="java"

Struts2系列:(7)通配符和动态方法调用

当前存在的问题:在struts.xml配置文件中,每个action标签对应一个类中的方法.但是,在实际JavaWeb项目开发中,有许多继承自ActionSupport类的类(其中也包括很多方法),如果每个方法对应一个action标签,那么就会造成struts.xml非常庞大. 本节中介绍的通配符 和 动态方法调用 就是为了解决这一问题.本节分成2个部分:(1)通配符映射 和 (2)动态调用 概念:Struts应用可能有很多action 声明,可把多个相似的映射关系简化为一个(通用)映射关系的机制

struts.enable.DynamicMethodInvocation = true 动态方法调用

default.properties 在Struts 2的核心jar包-struts2-core中,有一个default.properties的默认配置文件.里面配置了一些全局的信息,比如: struts.enable.DynamicMethodInvocation = true,-动态方法调用,为true时,就可以在struts.xml配置“*”的通配符,来调用action里的方法(下面介绍) struts.action.extension=action,-action 的后缀名,可以改成.t