jvm

虚拟机字节码执行引擎【动态类型语言支持(四)】

2021-08-18  本文已影响0人  云芈山人

实战:掌控方法分派规则

invokedynamic指令与此前4条传统的“invoke*”指令的最大区别就是它的分派逻辑不是虚拟机决定的,而是由程序员决定的。

方法调用问题,举个例子:

package com.jvm.test;

public class invoketest {

    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”关键字很方便地调用父类中的方法,但如果要访问祖类的方法呢?

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

如果是JDK7 Update9之前,可使用以下代码来解决该问题。

package com.jvm.test;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class invoketest {

    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 = MethodHandles.lookup().findSpecial(
                        GrandFather.class, 
                        "thinking", 
                        mt, 
                        getClass());
                mh.invoke(this);
            }catch(Throwable e){
            }
        }
    }
}

运行结果:

i am grandfather

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

i am father

查看MethodHandles.Lookup类的代码,在该API实现时预留了后门。访问保护是通过一个allowedModes的参数控制,而且这个参数可以被设置成“TRUSTED”来绕开所有 的保护措施。尽管这个参数只是在Java类库本身是引用,没有开放给外部设置,但我们通过反射可轻易打破这种限制。由此,我们可把代码修改成如下来解决问题:

package com.jvm.test;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

public class invoketest {

    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);
                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){
            }
        }
    }
}

《深入理解Java虚拟机》第三版 学习

上一篇下一篇

猜你喜欢

热点阅读