8.4.5 实战:掌控方法分派规则

8.4.5 实战:掌控方法分派规则

invokedynamic指令与此前4条传统的“invoke*”指令的最大区别就是它的分派逻辑不是由虚拟机决 定的,而是由程序员决定。在介绍Java虚拟机动态语言支持的最后一节中,笔者希望通过一个简单例 子(如代码清单8-15所示),帮助读者理解程序员可以掌控方法分派规则之后,我们能做什么以前无 法做到的事情。

代码清单8-15 方法调用问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
// 请读者在这里填入适当的代码(不能修改其他地方的代码)
// 实现调用祖父类的thinking()方法,打印"i am grandfather"
}
}

在Java程序中,可以通过“super”关键字很方便地调用到父类中的方法,但如果要访问祖类的方法呢?读者在往下阅读本书提供的解决方案之前,不妨自己思考一下,在JDK 7之前有没有办法解决这个问题。

在拥有invokedynamic和java.lang.invoke包之前,使用纯粹的Java语言很难处理这个问题(使用ASM 等字节码工具直接生成字节码当然还是可以处理的,但这已经是在字节码而不是Java语言层面来解决问题了),原因是在Son类的thinking()方法中根本无法获取到一个实际类型是GrandFather的对象引用, 而invokevirtual指令的分派逻辑是固定的,只能按照方法接收者的实际类型进行分派,这个逻辑完全固化在虚拟机中,程序员无法改变。如果是JDK 7 Update 9之前,使用代码清单8-16中的程序就可以直接解决该问题。

代码清单8-16 使用MethodHandle来解决问题
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
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
class Test {
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}

class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
mh.invoke(this);
}
catch (Throwable e) {
}
}
}
public static void main(String[] args) {
(new Test().new Son()).thinking();
}
}

使用JDK 7 Update 9之前的HotSpot虚拟机运行,会得到如下运行结果:

1
i am grandfather

但是这个逻辑在JDK 7 Update 9之后被视作一个潜在的安全性缺陷修正了,原因是必须保证findSpecial()查找方法版本时受到的访问约束(譬如对访问控制的限制、对参数类型的限制)应与使用invokespecial指令一样,两者必须保持精确对等,包括在上面的场景中它只能访问到其直接父类中的方法版本。所以在JDK 7 Update 10修正之后,运行以上代码只能得到如下结果:

1
i am father

由于本书的第2版是基于早期版本的JDK 7撰写的,所以印刷之后才发布的JDK更新就很难再及时地同步修正了,这导致不少读者重现这段代码的运行结果时产生了疑惑,也收到了很多热心读者的邮件,在此一并感谢。

那在新版本的JDK中,上面的问题是否能够得到解决呢?答案是可以的,如果读者去查看MethodHandles.Lookup类的代码,将会发现需要进行哪些访问保护,在该API实现时是预留了后门的。访问保护是通过一个allowedModes的参数来控制,而且这个参数可以被设置成“TRUSTED”来绕开所有的保护措施。尽管这个参数只是在Java类库本身使用,没有开放给外部设置,但我们通过反射可以轻易打破这种限制。由此,我们可以把代码清单8-16中子类的thinking()方法修改为如下所示的代码来解决问题:

1
2
3
4
5
6
7
8
9
10
11
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class,"thinking", mt, GrandFather.class);
mh.invoke(this);
}
catch (Throwable e) {
}
}

运行以上代码,在目前所有JDK版本中均可获得如下结果:

1
i am grandfather