JVM 动态调用句柄 MethodHandle

一、MethodHandle概述

Q:MethodHandle是什么?
A:方法句柄类似于反射框架中的Method,只不过其功能更为强大、效率也更高
Q:MethodHandle比起反射框架中的对应类有什么不同?
A:(1)表示范围更广。MethodHandle不仅仅只是能够代表Method,它更像是反射框架中Method、Field、Constructor三个类的抽象整合。(2)效率更高。MethodHandle在方法的执行过程中将不会和反射一样会去检查访问权限之类的东西。(3)更加安全。方法查找阶段将会严格按照访问控制权限来执行,并不像反射一样可以访问控制权限之外的东西。(4)扩展性强。方法句柄能够对执行时候的参数、返回值、异常等部分进行扩展,比如目标方法需要2个参数,但是我想在实际使用时只想让使用者输入1个参数(也就是说我会默认提供一个参数),这就可以通过方法句柄的转换来完成。(5)可以和反射框架协同使用。

二、创建MethodHandle的三个基本步骤

  1. 方法类型的确定
  2. 获取方法查找的客户端
  3. 使用方法查找的客户端查找指定方法类型、名字、访问权限等等的方法,成功后返回代表对应的方法的方法句柄(MethodHandle)

以创建Math#max方法的句柄为例:

//创建一个参数类型为`int,int`,返回值类型为`int`的方法类型实例
MethodType type = MethodType.fromMethodDescriptorString("(II)I",null);
//获取方法查找的客户端
MethodHandles.Lookup lookup = MethodHandles.lookup();
//通过该客户端查找`Math`类中名字为`max`方法类型为type的静态方法
MethodHandle maxHandle = lookup .findStatic(Math.class , "max" , type);

三、MethodType详解

  • 创建MethodType
    • MethodType#genericMethodType
      该方法会创建一个参数值及返回值类型都是Object的特殊方法类型,使用时只需要指定参数个数,以及最后一个参数是否是Object[]的参数两个部分就可以了
    • MethodType#methodType
      该方法显示指定参数类型及返回值类型,第一个参数用于指定方法的返回值类型,如果是表示静态方法的,第一个可以不填。后面的参数用于指定表示方法的参数类型,接受不定长参数
    • MethodType#fromMethodDescriptorString
      该方法接受一个代表方法返回值类型和参数类型的字符串,将会解析这个字符串并生成对应的方法类型实例,第二个参数可以为null
  • 修改MethodType
    • 修改参数列表
    • 增加参数
      • appendParameterTypes在参数列表的末尾再添加一系列参数
      • insertParameterTypes在指定位置处添加一系列参数
    • 修改参数类型
    • changeParameterType修改指定位置处的参数类型
    • 删除参数
      • dropParameterTypes删除指定位置到指定位置处的参数
    • 修改返回值类型
    • changeReturnType
    • 类型擦除
      • erase将所有引用类型都转换为Object类型
      • generic将所有引用类型和基本类型都转换为Object类型
    • 装箱类型的变换
      • wrap将所有基础类型转换为对应的装箱类型
      • unwrap将所有的装箱类型转换为对应的基础类型
    • 其他方法
      • parameterArray/parameterList/parameterType将参数类型列表用数组返回 / 将参数类型列表用List返回 / 返回指定位置处的参数类型
      • parameterCount参数个数
      • returnType返回值类型
      • toMethodDescriptorString返回对应的字符串表示方式
      • hasPrimitives/hasWrappers是否有基本类型 / 是否有装箱类型

四、MethodHandles.Lookup详解

  • 查找构造方法
    • findConstructor找寻指定类中符合指定类型(返回值设置为void.class)的构造方法
  • 查找域(不是真的查找对应域Setter和Getter方法,是直接对域进行赋值的,查找域时直接填入域的类型就可以了,不用MethodType)
    • findGetter/findSetter
    • findStaticGetter/findStaticSetter
  • 查找方法
    • findVirtual查找非静态方法
    • bind在findVirtual上事先还绑定了方法的执行者
    • findSpecial查找非公开类型的非静态方法,参数的最后需要指定调用者的类型,该调用者的类型应当是使用该方法返回的句柄进行调用的那个类,并且该类有权限能够访问指定的方法,否则前者将在执行时报出调用者与实际不符的错误,后者将在查找时报出没有指定方法的错误
    • findStatic 与前三个不同,这是专门用来查找静态方法的
  • 反射转换为句柄
    • unreflectConstructor将反射的Constructor转换为句柄
    • unreflectGetter将反射的Field转换为句柄
    • unreflectSetter将反射的Field转换为句柄
    • unreflect将反射的Method转换为句柄
    • unreflectSpecial将反射的特殊访问权限的Method转换为句柄

五、MethodHandle详解

  • 句柄的调用
    • invokeExact严格检查实际参数类型和返回值类型是否和句柄描述一致的调用,比如句柄描述中有一个参数类型为Object,则调用时必须传入一个Object类型的参数。又如System.out.println接受Object类型的参数,而你的句柄返回类型是String类型,你想打印这个字符串,然而实际上会报错,除非你句柄的返回类型也是Object类型的,再,如果你想直接单独使用(直接调用)也不行,因为那意味着你的句柄返回类型应当为void.class
    • invoke内嵌了自动类型转换的调用,会根据返回类型,参数类型、长度自动适配,极其方便
  • 其他方法
    • bindTo预先绑定句柄的第一个参数,对于非静态方法来说就是绑定调用者
      asType接受一个MethodType实例,尝试将句柄的方法类型改为指定类型,注意,只是尝试转换,也就是说需要符合一定规则,比如把String类型的返回值改为Object类型
MethodType methodType = MethodType.fromMethodDescriptorString("(II)Ljava/lang/String;" , null);
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class , "substring" , methodType);
//由于System.out.println只接受Object类型的参数,故使用asType进行转换
mh = mh.asType(MethodType.fromMethodDescriptorString("(Ljava/lang/String;II)Ljava/lang/Object;" , null));
System.out.println(mh.invokeExact("hello" , 1 , 3));
  • type返回句柄的方法类型,注意该方法类型对于非静态方法来说,第二个参数会是调用者的类型,与查找方法时的方法类型不同,后者只包含返回值类型、参数类型

六、MethodHandles详解

  • 特殊句柄
    操纵数组元素的句柄:MethodHandles#arrayElementSetter和MethodHandles#arrayElementGetter
int[] arr = new int[]{1,2,3,4,5,6};
MethodHandle arrHandle = MethodHandles.arrayElementSetter(int[].class);
arrHandle = arrHandle.bindTo(arr);
//将index为2的数组元素改为9
arrHandle.invoke(2,9);
for(int item : arr){
    System.out.println(item);
}
//结果:1 2 9 4 5 6
  • 输出等于输入的句柄:MethodHandles#identity该方法接受一个类型参数,生成的方法句柄的返回值和参数(唯一)也会是这个类型,调用时输入一个值,返回值同样会是这个值

  • 输出值固定的句柄:MethodHandles#constant该方法接受一个类型参数和一个输出值,该类型参数是返回值的类型,输出值将会作为每次调用的返回值

  • 句柄转换

    • 为原句柄添加无效的参数的转换:MethodHandles#dropArguments,第一个参数为待转换的句柄,第二参数为要添加参数的位置,第三个参数为要添加的参数类型列表。无效参数的意思时,在真正调用的时候,这些参数会被忽略掉,可以理解为占位符
MethodHandle dropArgHandle = MethodHandles.lookup().findVirtual(String.class , "toString" , MethodType.fromMethodDescriptorString("()Ljava/lang/String;" , null));
dropArgHandle = MethodHandles.dropArguments(dropArgHandle , 0 , String.class , int.class);
System.out.println(dropArgHandle.invoke("123" , 2 ,"hello"));
//结果:hello

为原句柄添加预先绑定的参数值的转换:第一个参数为待转换的句柄,第二个参数为要开始绑定参数的位置,第三个需要被绑定的参数值列表。相当于加强版的bindTo

MethodHandle bindArgHandle = MethodHandles.lookup().findVirtual(String.class , "concat" , MethodType.fromMethodDescriptorString("(Ljava/lang/String;)Ljava/lang/String;" , null));
bindArgHandle = MethodHandles.insertArguments(bindArgHandle , 1 , "654");
System.out.println(bindArgHandle.invoke("123"));
//结果:123654

为参数/返回值添加过滤机制的转换:MethodHandles#filterArguments/MethodHandles#filterReturnValue,前者接受三个参数,第一个参数是原句柄,第二个参数是需要添加过滤机制的位置,第三个参数是用于过滤的方法句柄。后者接受两个参数,第一个参数是原句柄,第二个参数是用于过滤的方法句柄。过滤的方法句柄应该满足这些原则:参数过滤的句柄的返回值类型应该和被过滤的参数类型一致,但是输入类型可以随意,返回值过滤的句柄的输入类型应当和被过滤的返回类型一致,但是输出类型可以随意。(参数->过滤参数句柄->过滤后的参数->原方法句柄->返回值->过滤返回值句柄->过滤后的返回值)

public class Test {
    private int fieldInt = 2;
    public Test(int fieldInt){
        this.fieldInt = fieldInt;
    }
    public int getSum(int otherInt){
        return fieldInt + otherInt;
    }
}
//................................
MethodHandle getSumHandle = MethodHandles.lookup().findVirtual(Test.class , "getSum" , MethodType.fromMethodDescriptorString("(I)I" , null));
getSumHandle = getSumHandle.bindTo(new Test(2));
MethodHandle filterLengthHandle = MethodHandles.lookup().findVirtual(String.class , "length" , MethodType.fromMethodDescriptorString("()I" , null));
getSumHandle = MethodHandles.filterArguments(getSumHandle , 0 , filterLengthHandle);
MethodHandle valueOfHandle = MethodHandles.lookup().findStatic(String.class , "valueOf" , MethodType.fromMethodDescriptorString("(I)Ljava/lang/String;" , null));
getSumHandle = MethodHandles.filterReturnValue(getSumHandle , valueOfHandle);
String result = (String) getSumHandle.invoke("123456789");
//(参数类型String->过滤参数句柄(String->int)->过滤后的参数int->原方法句柄(int->int)->返回值int->过滤返回值句柄(int->String)->过滤后的返回值String
System.out.println(result);
//结果:11

将多个参数整合成一个新参数的句柄转换:MethodHandles#foldArguments,该方法第一个参数为待转换的句柄,第二个参数是从哪个位置开始取多个参数(至于具体取几个参数,由第三个参数方法句柄的参数列表长度决定),第三个参数是来执行该转换的句柄,该句柄的输出值将会添加在第二个参数指定的位置。(参数列表【参数类型1,参数类型2,参数类型3】-> 转换句柄【(参数类型1,参数类型2)-> 参数类型4】-> 转换后参数列表【参数类型4,参数类型1,参数类型2,参数类型3】->原句柄执行)

public class Test {
    private int fieldInt = 2;
    public Test(int fieldInt){
        this.fieldInt = fieldInt;
    }
    public int getSumFromTwoArgs(int first , int second){
        return first + second + fieldInt;
    }
}
//.............................
MethodHandle getSumPlusHandle = MethodHandles.lookup().findVirtual(Test.class , "getSumFromTwoArgs" , MethodType.fromMethodDescriptorString("(II)I",null));
getSumPlusHandle = getSumPlusHandle.bindTo(new Test(2));
MethodHandle filterMaxlengthHandle = MethodHandles.lookup().findStatic(Main.class , "maxLength" , MethodType.fromMethodDescriptorString("(Ljava/lang/String;Ljava/lang/String;)I" , null));
getSumPlusHandle = MethodHandles.dropArguments(getSumPlusHandle , 1 , String.class , String.class);
//参数列表(int,String,String,int)  
getSumPlusHandle = MethodHandles.foldArguments(getSumPlusHandle , 0 ,filterMaxlengthHandle);
System.out.println(getSumPlusHandle.invoke("12332112" , "2358646" , 6));
//结果:16

将参数顺序重新排列的句柄转换:MethodHandles#permuteArguments,第一个参数是待转换的句柄,第二个参数是顺序变换后的方法类型,第三个参数接受句柄参数多个的整型值,该值表示其所代表的参数在重新排列后应当处于什么位置。详情看示例:

MethodType oldType = MethodType.fromMethodDescriptorString("(Ljava/lang/String;I)I" , null);
MethodHandle indexOfHandle = MethodHandles.lookup().findVirtual(String.class , "indexOf" , oldType);
MethodType newType = MethodType.fromMethodDescriptorString("(Ljava/lang/String;ILjava/lang/String;)I" , null);
indexOfHandle = MethodHandles.permuteArguments(indexOfHandle , newType , 0 , 2 , 1);
//(String,String,int)->(String,int,String)
System.out.println(indexOfHandle.invoke("hello ele" , 2 , "e"));
//结果:6

添加对异常进行处理的句柄转换:Methodhandles#catchException,第一个参数是待处理的句柄,第二个参数是要处理的异常类型,第三个参数是处理异常的句柄,该处理异常的句柄的参数应当为(Exception,String),返回值应当和原句柄的返回值的类型一致。也就是说当异常发生后,该异常会被异常处理句柄处理,然后异常处理句柄的返回值将作为原句柄的返回值被返回

public class Main {
    public static void main(String[] args) throws Throwable {
        MethodHandle parseIntHandle =   MethodHandles.lookup().findStatic(Integer.class , "parseInt" , MethodType.fromMethodDescriptorString("(Ljava/lang/String;)I" , null));
        MethodHandle exceptionHandle = MethodHandles.lookup().findStatic(Main.class , "exceptionHandle" , MethodType.fromMethodDescriptorString("(Ljava/lang/Exception;Ljava/lang/String;)I" , null));
        parseIntHandle = MethodHandles.catchException(parseIntHandle , Exception.class , exceptionHandle);
        parseIntHandle.invoke("0sad");
    }
    
    public static int exceptionHandle(Exception e , String str){
        System.out.println(e.getMessage());
        return 0;
    }
}
//结果:For input string: "0sad"

为句柄添加条件执行能力:MethodHandles#guardWithTest,第一个参数是条件句柄,该句柄参数列表应当为空,返回值类型应当为boolean,第二个参数是当条件为真时执行的句柄,第三个参数是当条件为假时执行的句柄,第二、三个句柄应当具有相同的方法类型。句柄执行时,会先执行条件句柄,然后根据结果选择对应的执行句柄去真正执行。

MethodHandle boolHandle = MethodHandles.constant(boolean.class , true);
MethodType type = MethodType.fromMethodDescriptorString("(II)I" , null);
MethodHandle maxHandle = MethodHandles.lookup().findStatic(Integer.class , "max" , type);
MethodHandle minHandle = MethodHandles.lookup().findStatic(Integer.class , "min" , type);
boolHandle =MethodHandles.guardWithTest(boolHandle , maxHandle , minHandle);
System.out.println(boolHandle.invoke(7,9));
//结果:9

句柄模板
MethodHandles#invoker,句柄模板用于解决这么一种问题:假设现在有10个MethodType一样的方法句柄,我想对这10个方法句柄都添加一个返回值过滤的机制,那么我得转换10次。而有了模板句柄之后,我们可以这么做,创建一个模板句柄,MethodType与待转换的句柄们一致,使用该模板句柄添加一个返回值过滤的机制,然后在使用时指定哪个句柄进行执行就可以了。

MethodType type = MethodType.fromMethodDescriptorString("(II)I",null);
MethodHandle maxHandle = MethodHandles.lookup().findStatic(Math.class , "max" , type);
MethodHandle minHandle = MethodHandles.lookup().findStatic(Math.class , "min" , type);
MethodHandle templateInvoker = MethodHandles.invoker(type);
MethodHandle intToStringFilter = MethodHandles.lookup().findStatic(String.class , "valueOf" , MethodType.methodType(String.class , int.class));
templateInvoker = MethodHandles.filterReturnValue(templateInvoker , intToStringFilter);
String result = (String) templateInvoker.invoke(maxHandle , 56 ,78);
System.out.println(result);
//结果:78

七、其他

  • 接口代理
    MethodHandleProxies#asInterfaceInstance,接口代理就是使用指定句柄来对某个接口的方法进行实现,限制较多,该接口必须只含有一个方法,且用于实现该方法的句柄的参数类型与返回值类型应当与被实现的方法一致
MethodHandle handle = MethodHandles.lookup().findStatic(Main.class , "printOk" , MethodType.fromMethodDescriptorString("()V" , null));
        Callable runnableProxy = MethodHandleProxies.asInterfaceInstance(Callable.class , handle);
        runnableProxy.call();
//结果:ok
  • 交换点SwitchPoint
    就是使用了SwitchPoint的状态来进行判断的guardWithTest
MethodType type = MethodType.fromMethodDescriptorString("(II)I",null);
MethodHandle maxHandle = MethodHandles.lookup().findStatic(Math.class , "max" , type);
MethodHandle minHandle = MethodHandles.lookup().findStatic(Math.class , "min" , type);
SwitchPoint switchPoint = new SwitchPoint();
MethodHandle ultimateHandle = switchPoint.guardWithTest(maxHandle , minHandle);
System.out.println(ultimateHandle.invoke(1,10));
SwitchPoint.invalidateAll(new SwitchPoint[]{switchPoint});
System.out.println(ultimateHandle.invoke(1,10));
//结果:10 1
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容