网上看过太多关于MethodHandle方法句柄的文章,但是基本上没有人能把其中的findspecial方法讲清楚,特别是findspecial的第四个参数specialCaller, 相信大家都不明白是干嘛用的,网上给出的水文是很多都是说: 执行到specialCaller的父类所对应的方法。我可以很负责的告知各位:这个结论绝对是错误的!
包括网上很知名的书籍《深入理解java虚拟机》也并没有把这个问题讲清楚,这本书废话太多,对于关键问题讲不到重点,完全是浪费读者时间。
首先我给出一个结论:findspecial的第四个参数specialCaller的作用:是为了限定方法查找的范围。
public MethodHandle findSpecial(Class<?> refc,
String name,
MethodType type,
Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException
后面会通过走读源码具体说明这一点。
首先我们需要了解,MethodHandle方法句柄中lookup到底是起什么作用的,因为包括findstatic,findvirtual,findspecial这些都是从lookup中创建出来。
官网的解释如下:
A lookup object is a factory for creating method handles, when the creation requires access checking. Method handles do not perform access checks when they are called, but rather when they are created. Therefore, method handle access restrictions must be enforced when a method handle is created. The caller class against which those restrictions are enforced is known as the lookup class.
A lookup class which needs to create method handles will call MethodHandles.lookup to create a factory for itself. When the Lookup
factory object is created, the identity of the lookup class is determined, and securely stored in the Lookup
object. The lookup class (or its delegates) may then use factory methods on the Lookup
object to create method handles for access-checked members. This includes all methods, constructors, and fields which are allowed to the lookup class, even private ones.
这里面说了很多,但是关键点只有三点:
1. lookup是用来创建方法句柄的工厂对象
2. 方法句柄创建时就必须做权限检查,而不是运行时做权限检查
3. 受该权限约束的class对象就是lookup class, 而且一旦lookup工厂对象创建,就能确定具体的lookup class.
第三点说的非常抽象,不太好理解。关于这一点可以通过断点调试搞明白:
通过网上著名的son/father/grandfather这段代码,做一个断点调试如下:
这里的方法调用是从Son的实例发起的,而根据上面断点调试结果,lookup class也正好是类Son,
那么可以看到:方法句柄所说的lookup对象,实际是指调用lookup方法所在的类,也就是:哪个类调用了lookup方法,那么生成的lookup对象就指向这个类
你可以简单理解为:lookup就是当前方法调用所处的一个上下文环境,在这个上下文里面,你所有的权限都不能超过这个范围!
换句话说:findSpecial方法的权限也在这个范围里面,这就是为什么会出现私有权限越界的原因:no private access for invokespecial ,如下所示:
那么回到开头:findspecial的第四个参数specialCaller到底是干嘛用的呢?
官网是这样解释的:
Produces an early-bound method handle for a virtual method. It will bypass checks for overriding methods on the receiver, as if called from an invokespecial
instruction from within the explicitly specified specialCaller
. The type of the method handle will be that of the method, with a suitably restricted receiver type prepended. (The receiver type will be specialCaller
or a subtype.) The method and all its argument types must be accessible to the lookup object.
Before method resolution, if the explicitly specified caller class is not identical with the lookup class, or if this lookup object does not have private access privileges, the access fails.
The returned method handle will have variable arity if and only if the method's variable arity modifier bit (0x0080
) is set.
说实话,这段话非常不好理解,基本上等同于狗屁不通,说了半天不知所云,果然是官网出品,必属废品!
只有倒数第二部分,稍微有点意思:
"在方法解析之前,如果显式指定的调用者类与lookup不同,或者如果该Lookup不具有私有访问权限,则访问失败(即方法句柄创建失败)。"
现在通过源码走读来揭开其中的秘密:
跟进findSpecial方法首先关注到checkSpecialCaller方法:
继续跟进checkSpecialCaller方法:
发现:如果specialCaller跟lookupClass对象不一致时,就会抛出:无私有权限异常
这确实印证了官网刚才的倒数第二段解释。
但是这还不够,难道specialCaller仅仅只是为了简单做这样一个校验的吗,那岂不是很多此一举,这跟findVirtual也没多大区别,还要自找麻烦传入第四个参数,有意义吗?!因为前面已经说过:
findSpecial方法的权限本身被封存在lookup class对象的范围内,它的所有子对象和操作都不能超过这个范围,所以specialCaller这里显得很多余!所以,一定还有别的什么原因。
回到findSpecial主代码,继续跟进:
注意有一个this.in方法(实际是Lookup.in):重新生成了specialLookup
查看一下官网对这个Api的解释:
public MethodHandles.Lookup in(Class<?> requestedLookupClass)
Creates a lookup on the specified new lookup class. The resulting object will report the specified class as its own lookupClass.
However, the resulting
Lookup
object is guaranteed to have no more access capabilities than the original. In particular, access capabilities can be lost as follows:
重点在第二段:这个新生成的Lookup对象的权限不会超过原有对象的范围,而this.in方法是把specialCaller作为参数传进去,生成了一个新的Lookup对象,所以:这个新的Lookup对象的权限也不会超过specialCaller的范围,记住这个结论!
然后继续跟进getDirectMethod方法:
跟进getDirectMethodCommon方法:
注意:这里的入参refc就是findSpecial的第一个参数refc
重点看下面的while循环这段:
Class<?> refcAsSuper = lookupClass();
MemberName m2;
do {
refcAsSuper = refcAsSuper.getSuperclass();
m2 = new MemberName(refcAsSuper,
method.getName(),
method.getMethodType(),
REF_invokeSpecial);
m2 = IMPL_NAMES.resolveOrNull(refKind, m2, lookupClassOrNull());
} while (m2 == null && // no method is found yet
refc != refcAsSuper); // search up to refc
注意这里的refcAsSuper实际就是specalCaller,因为上面整个方法调用链条的发起人就是specialLookup,而且specialLookup是通过specalCaller创建的。
在这个while循环里,refcAsSuper不断的通过getSuperclass()方法向其父级搜寻指定的method方法,如果找到方法就跳出循环;否则就一直往上找,直到抵达refc类的层级.
为什么这么说呢,因为while条件是这么写的,仔细想想就能明白:
while (m2 == null && refc != refcAsSuper);
所以,到现在为止,一切都很明朗了:findspecial并不是执行到specialCaller的父类为止所对应的方法!
而是从specialCaller开始一直向其父类寻找指定方法,一旦找到就返回并执行该方法;如果找不到就一直往上找,最后终止于findspecial的第一个参数refc所对应的类。
这里其实隐含了一层意思:就是specialCaller一定是findspecial的第一个参数refc的子类!
下面我们来验证上面的结论。
由于前面已经说过当specialCaller跟lookUP class对象不一致时,是无法通过权限检查的。
但是我们可以通过修改allowedModes的值,让它等于TRUSTED(也就是-1),绕过这个检查:
加入下面两行代码:
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
然后通过lookupImpl.get(null)方法可以获取到一个allowedModes=-1 的lookup
这样就跳过了checkSpecial的权限检查。
下面来试验specialCaller的方法查找范围:
当specialCaller指定为Son.class时,最后的确执行了它的父级Father的think方法。
但是如果把父级Father的think方法注释掉,会发生什么呢!
看到了吗,因为specialCaller(Son.class)因为在它的父级Father找不到指定的thinking方法,所以会继续向上查找:从而找到Grandfather的thinking方法,并执行该方法。
所以最后总结:findspecial的第四个参数specialCaller的作用:是为了限定方法查找的范围。
具体来说:findSpecial执行时:会从specialCaller开始一直向其父类寻找指定方法,一旦找到就立即返回并执行该方法;如果找不到就一直往上找,最后终止于findspecial的第一个参数refc所对应的类。如果最终找不到指定的方法,就会抛NoSuchMethodException异常
另外:specialCaller一定是findspecial的第一个参数refc的子类!