8.4.5 实战:掌控方法分派规则
8.4.5 实战:掌控方法分派规则
invokedynamic指令与此前4条传统的“invoke*”指令的最大区别就是它的分派逻辑不是由虚拟机决 定的,而是由程序员决定。在介绍Java虚拟机动态语言支持的最后一节中,笔者希望通过一个简单例 子(如代码清单8-15所示),帮助读者理解程序员可以掌控方法分派规则之后,我们能做什么以前无 法做到的事情。
1 | class GrandFather { |
在Java程序中,可以通过“super”关键字很方便地调用到父类中的方法,但如果要访问祖类的方法呢?读者在往下阅读本书提供的解决方案之前,不妨自己思考一下,在JDK 7之前有没有办法解决这个问题。
在拥有invokedynamic和java.lang.invoke包之前,使用纯粹的Java语言很难处理这个问题(使用ASM 等字节码工具直接生成字节码当然还是可以处理的,但这已经是在字节码而不是Java语言层面来解决问题了),原因是在Son类的thinking()方法中根本无法获取到一个实际类型是GrandFather的对象引用, 而invokevirtual指令的分派逻辑是固定的,只能按照方法接收者的实际类型进行分派,这个逻辑完全固化在虚拟机中,程序员无法改变。如果是JDK 7 Update 9之前,使用代码清单8-16中的程序就可以直接解决该问题。
1 | import static java.lang.invoke.MethodHandles.lookup; |
使用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 | void thinking() { |
运行以上代码,在目前所有JDK版本中均可获得如下结果:
1 | i am grandfather |