MethodHandles

Method Hanldes是在Java 7引入的概念。全限定名是java.lang.invoke.MethodHandles。

1.介绍

Method Handles的引入是为了与已经存在的java.lang.reflect API相配合。他们分别是为了解决不同的问题而出现的。从性能角度上说,MethodHandle api要比反射快很多因为访问检查在创建的时候就已经完成了,而不是像反射一样等到运行时候才检查。但同时,Method Handles比反射更难用,因为没有列举类中成员,获取属性访问标志之类的机制。
另外,MethodHandles可以操作方法,更改方法参数的类型和他们的顺序。而反射则没有这些功能。
从以上角度看,反射更通用,但是安全性更差,因为可以在不授权的情况下使用反射对象。而method Handles遵从了分享者的能力。所以method handle是一种更低级的发现,适配和调用方法的方式,唯一的优点就是更快。所以反射更适合主流Java开发者,而method handle更适用于对编译和运行性能有要求的人。

MethodHandle是可对直接执行的方法(或域、构造方法等)的类型的引用,或者说,它是一个有能力安全调用方法的对象。换个方法来说,通过句柄我们可以直接调用该句柄所引用的底层方法。作用类似于反射中的Method类,但它比Method类要更加灵活和轻量级。通过MethodHandle进行方法调用一般需要以下几步:

(1)创建MethodType对象,指定方法的签名;

(2)在MethodHandles.Lookup中查找类型为MethodType的MethodHandle;

(3)传入方法参数并调用MethodHandle.invoke或者MethodHandle.invokeExact方法。

MethodType:是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型。它的类型完全由参数类型和方法类型来确定,而与它所引用的底层的方法的名称和所在的类没有关系。可以通过MethodHandle类的type方法查看其类型,返回值是MethodType类的对象。也可以在得到MethodType对象之后,调用MethodHandle.asType(mt)方法适配得到MethodHandle对象。可以通过调用MethodType的静态方法创建MethodType实例,有三种创建方式:

(1)methodType及其重载方法:需要指定返回值类型以及0到多个参数;

(2)genericMethodType:需要指定参数的个数,类型都为Object;

(3)fromMethodDescriptorString:通过方法描述来创建。

需注意:MethodType的ptypes需要与目标方法签名一致(不支持向上/向下转型,比如用Integer去获取入参为Number的方法是不允许的,同样用Number去获取入参为Integer的方法也不行。这个还能理解),rtype需要与目标方法的返回值一致(不允许向上/向下转型。不允许向下很容易理解,为何要不允许向上转型,有点不清楚)

创建好MethodType对象后,还可以对其进行修改,MethodType类中提供了一系列的修改方法,比如:changeParameterType、changeReturnType等。 MethodType的对象实例只能通过MethodType类中的静态工厂方法来创建,而且MethodType类的所有对象实例都是不可变的。如果修改了MethodType实例中的信息,就会生成另外一个MethodType实例。

Lookup:MethodHandle.Lookup相当于MethodHandle工厂类,通过findxxx方法可以得到相应的MethodHandle,还可以配合反射API创建MethodHandle,对应的方法有unreflect、unreflectSpecial等。

invoke:在得到MethodHandle后就可以进行方法调用了,有三种调用形式:

(1)invokeExact:调用此方法与直接调用底层方法一样,需要做到参数类型精确匹配;

(2)invoke:参数类型松散匹配,通过asType自动适配;

(3)invokeWithArguments:直接通过方法参数来调用。其实现是先通过genericMethodType方法得到MethodType,再通过MethodHandle的asType转换后得到一个新的MethodHandle,最后通过新MethodHandle的invokeExact方法来完成调用。

2.使用

1.要使用method handle,首先需要得到Lookup。这是创造方法,构造函数,属性的method handles的工厂类。

// public方法的Lookup
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
// 所有方法的Lookup
MethodHandles.Lookup lookup = MethodHandles.lookup();

2.要创建MethodHandle,lookup需要一个定义了它的类型的MethodType对象。这里的类型包括了传入参数的类型,和最后返回的类型,要一一对应。第一个是返回类型,如果没有返回值就是Void.class, 后面是可变的传入参数的类型。

a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller.

例如

// 接收数组,返回一个List对象
MethodType mt = MethodType.methodType(List.class, Object[].class);

3.查找MethodHandle
Lookup之所以叫Lookup自然是因为他们有查找MethodHandle的能力。先看看他的方法。

img

4.接下来就可以进行查找并调用了

MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);

String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

5.方法调用细则:
有三种方法可以调用方法invoke(), invokeWithArugments()和invokeExact(),当我们使用invoke时,我们必须固定arguments的数目。

// invoke使用
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

// invokeWithArguments使用
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);
List<Integer> list = (List<Integer>) asList.invokeWithArguments(1,2);

// invokeExact
MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);
int sum = (int) sumMH.invokeExact(1, 11);

具体的区别是:

与invokeExact方法不同,invoke方法允许更加松散的调用方式。它会尝试在调用的时候进行返回值和参数类型的转换工作。这是通过MethodHandle类的asType方法来完成的,asType方法的作用是把当前方法句柄适配到新的MethodType上面,并产生一个新的方法句柄。当方法句柄在调用时的类型与其声明的类型完全一致的时候,调用invoke方法等于调用invokeExact方法;否则,invoke方法会先调用asType方法来尝试适配到调用时的类型。如果适配成功,则可以继续调用。否则会抛出相关的异常。这种灵活的适配机制,使invoke方法成为在绝大多数情况下都应该使用的方法句柄调用方式。

进行类型匹配的基本规则是对比返回值类型和每个参数的类型是否都可以相互匹配。假设源类型为S,目标类型为T,则基本规则如下:

​ 1、可以通过java的类型转换来完成,一般从子类转成父类,比如从String到Object类型;

​ 2、可以通过基本类型的转换来完成,只能将类型范围的扩大,比如从int切换到long;

​ 3、可以通过基本类型的自动装箱和拆箱机制来完成,例如从int到Integer;

​ 4、如果S有返回值类型,而T的返回值类型为void,则S的返回值会被丢弃。

​ 5、如果S的返回值是void,而T的返回值是引用类型,T的返回值会是null;

​ 6、如果S的返回值是void,而T的返回值是基本类型,T的返回值会是0;

有一点需要注意⚠️,虽然invoke的入参是一个(Object … args),但对于可变参数,不能简单的传个数组过去,会出现类型转换异常 Object[] -> SpecifiedType,所以暂时无法对这个封装,因为无法正确的传递可变参数部分,中间方法里这个会变成一个数组,调用invoke时,会将其视为一个参数,而非可变参数,从而无法成功调用invoke系列方法 see


补充:

Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
IMPL_LOOKUP.setAccessible(true);// 可用IMPL_LOOKUP.trySetAccessible()替换
MethodHandles.Lookup lkp = (MethodHandles.Lookup) IMPL_LOOKUP.get(null);
MethodHandle methodHandle= lkp.findSpecial(t.getClass(), getMethodName, MethodType.methodType(String.class), t.getClass());
value = methodHandle.bindTo(t).invoke();

//IMPL_LOOKUP 是用来判断私有方法是否被信任的标识,用来控制访问权限的.默认是false,
// 默认情况下findSpecial方法中的最后一个参数Class<?> specialCaller 有一个校验 checkSpecialCaller,
// 如果当前的lookup的类和specialCaller不一致的话就会检测不通过,
// IMPL_LOOKUP.setAccessible(true);设置为true之后,(MethodHandles.Lookup) IMPL_LOOKUP.get(null)这是获取一个Lookup,
// 这种方式返回的allowedModes为-1 这样的话就可以绕过检查,从而执行传入specialCaller类中的方法,
// 当然也有风险,舍弃了强校验,很容易抛出NoSuchMethodError.
// 一些情况下,通过get(null)获取不到,要用下面的方式
MethodHandles.Lookup lkp  = (MethodHandles.Lookup) unsafe.getObject(unsafe.staticFieldBase(IMPL_LOOKUP), unsafe.staticFieldOffset(IMPL_LOOKUP));

findSpecial方法用来查找类中的特殊方法,主要是类中的私有方法。findSpecial方法比之前的findVirtual和findStatic等方法多了一个参数。这个额外的参数用来指定私有方法被调用时所使用的类。提供这个类的原因是为了满足对私有方法的访问控制的要求。当方法句柄被调用时,指定的调用类必须具备访问私有方法的权限,否则会出现无法访问的错误。

除了直接在某个类中进行查找之外,还可以从通过反射API得到的Constructor、Field和Method等对象中获得方法句柄。   

首先通过反射API得到表示构造方法的Constructor对象,接着

  • 通过unreflectConstructor方法就可以得到其对应的一个方法句柄;
  • 通过unreflect方法可以将Method类对象转换成方法句柄。
  • 对于私有方法,则需要使用unreflectSpecial来进行转换,同样也需要提供一个作用与findSpecial中参数相同的额外参数;
  • 对于Field类的对象来说,通过unreflectGetter和unreflectSetter就可以得到获取和设置其值的方法句柄。

Leave a Reply

Your email address will not be published. Required fields are marked *

lWoHvYe 无悔,专一