Java8:如何动态地获取一个 MethodReference 所引用的 Method

简介:Method References

Java8 的 lambda 表达式 可以很方便的用来创建一个匿名方法。在更多的情况下,可以通过一种新的 方法引用 的语法来基于一个现有的方法创建一个新的 lambda 表达式。

例如:某个方法需要传入一个 java.util.function.Supplier

setTimeStampSupplier(Supplier supplier);

假如我们打算为这个方法提供一个总是返回系统当前时间戳的 supplier,那么在按照传统的方法书写:

obj.setTimeStampSupplier(new Supplier() {
    @Override
    public Long get() {
        return System.currentTimeMillis();
    }
});

由于 java.util.function.Supplier 是一个 FunctionalInterface,所以可以使用 Lambda 表达式的方式来简化书写:

obj.setTimeStampSupplier(() -> System.currentTimeMillis());

借由 Method References 语法的帮助,这种写法可以更加简化为:

obj.setTimeStampSupplier(System::currentTimeMillis);

上文中的 System::currentTimeMillis 就是一个方法引用。

获取 Method Reference 所引用的 Method 实例

理论分析

那么是否能在运行时获取某个给定的 Method Reference 所引用的 java.lang.reflect.Method 实例呢?

答案是:没有可靠的、完美的方法(来自:Mike Strobel 的回答)

Java 实现 Lambda Expression 的方式并不是引入了一个新的数据类型,而可以理解成 JVM 在运行时动态生成匿名类(实际上是通过 invokedynamic 指令实现的,具体可以参阅 Java 8 Lambdas - A Peek Under the Hood
这篇文章,这样效率比匿名类要高很多,不过不妨碍理解),因此在这一机制下,在语言层面上 Method Reference 和 Method 并没有一一对应的关系,因此也没有可靠的方法能够完美获取 Method Reference 所引用的 Method。

“没有可靠的方法” 并不等于 “没有方法”

如题,没有可靠的完美的方法 并不等于 没有方法。在满足某些特定的条件下,是可以的通过 Method Reference 获取到其引用的方法的。

Method Reference 可以分成下面四种类型:

种类 例子
引用给定类型上的静态方法 ContainingClass::staticMethodName,例如:java.lang.Thread::currentThread
引用给定对象上的实例方法 containingObject::instanceMethodName,例如:java.lang.Thread.currentThread::getName
引用给定类型上的实例方法 ContainingType::methodName,例如:java.lang.Thread::getId
引用构造函数 ClassName::new,例如:java.lang.Object::new

上表中第一类和第三类的区别在于(以例子中的方法为例):

  • 第一类 Method Reference 将匹配形如 Supplier 的函数式接口
  • 第三类 Method Reference 将匹配形如 Function 或者 Consumer 的函数式接口

如果给定的 MethodReference 是上表中的第二类或者第三类,并且满足:

  1. 给定的类或者给定对象的类不是 final 类,并且最好需要有一个有效的无参数构造函数,或者给定的类为接口
  2. 指定的实例方法不是 final 方法

在这种情况下,可以借助 cglib 来创建一个给定类的实例,并拦截给定的方法,然后在此实例上调用给定的 MethodReference,于是就可以在拦截器中获取 Method 实例了。(这也就解释了为什么会有上面的限制,因为 cglib 的 Enhancer 无法创建一个 final 类的子类,也无法拦截一个 final 方法)

代码实例

假定我们要获取 Thread::getId 引用的 Method,那么可以使用如下的代码:

// 定义一个 MethodReference
Function methodRef = Thread::getId;

// 创建一个 Enhancer,并配置拦截器
AtomicReference ref = new AtomicReference();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Thread.class);
enhancer.setCallback(new MethodInterceptor() {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        ref.set(method);
        return null;
    }
});

// 创建一个实例
Thread phantom = (Thread) enhancer.create();

// 在实例上调用 MethodReference
methodRef.apply(phantom);

Method method = ref.get();
System.out.println(method);

运行结果:

public long java.lang.Thread.getId()

优化

重构为库函数

有了上面的原型以后,我们可以重构出一个获取 带返回值但是无参数的实例方法 的 Method Reference 的 Method 的库函数:

public static  Method getReferencedMethod(Class clazz, Function methodRef) {
    // 创建一个 Enhancer,并配置拦截器
    AtomicReference ref = new AtomicReference();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            ref.set(method);
            return null;
        }
    });
    
    // 创建一个实例
    @SuppressWarnings("unchecked")
    T phantom = (T) enhancer.create();
    
    // 在实例上调用 MethodReference
    methodRef.apply(phantom);
    
    Method method = ref.get();
    if (method == null) {
        // 如果传入的不是方法引用,而是直接 new 出来的 Function 实例,那么 method 就会是 null
        throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
    }
    return method;
}

只需要如此调用即可:getReferencedMethod(Thread.class, Thread::getId)

再进一步,适配满足条件的任何情况

上述的库函数只能用于获取带返回值但是无参数的实例方法,如果遇到其它情况的实例方法的时候应该怎么办呢?

这个时候我们需要再将上面的库函数进一步抽象,然后通过为目标实例方法来定义函数式接口来实现。

首先抽象 getReferencedMethod 函数:

public static  Method getReferencedMethod(Class clazz, Consumer invoker) {
    // 创建一个 Enhancer,并配置拦截器
    AtomicReference ref = new AtomicReference();
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            ref.set(method);
            return null;
        }
    });
    
    // 创建一个实例
    @SuppressWarnings("unchecked")
    T phantom = (T) enhancer.create();
    
    // invoker 需要在实例上调用 MethodReference
    invoker.accept(phantom);
    
    Method method = ref.get();
    if (method == null) {
        // 如果传入的不是方法引用,而是直接 new 出来的 Function 实例,那么 method 就会是 null
        throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
    }
    return method;
}

接下来,我们需要为特定的目标实例方法创建一个函数式接口,使其能够接纳这个实例方法的 MethodReference。假定我们需要适配 NavigableMap::subMap(K fromKey, K toKey),那么我们可以定义如下的函数式接口:

/**
 * 一个可以匹配带有两个参数的实例方法引用的函数式接口
 */
@FunctionalInterface
public interface MethodRefWith2Args {
    void accept(T instance, A1 arg1, A2 arg2) throws Exception;
}

然后我们创建一个 Consumer 用来调用 Method Reference,最后将他们封装在一起,一个崭新的、用来匹配带两个参数的方法的 MethodReference 的 getReferencedMethod 诞生了:

public static  Method getReferencedMethod(
        Class clazz, 
        MethodRefWith2Args methodRef) {
    return getReferencedMethod(clazz, phantom -> {
        try {
            // 后面参数传 null 没关系,因为实际被调用的是我们自己的 MethodInterceptor
            // 不会去处理参数
            // 不过如果参数类型是 primitive 的话,这里会抛出 NullPointerException
            methodRef.accept(phantom, null, null);
        } catch (Exception e) {
            // 正常情况下,不会跑到这里来
        }
    });
}

举一反三

如果工程中需要用此方法获取方法实例的目标实例方法并没有太多参数,那么其实可以预先定义好一堆的适配用的函数式接口,譬如:MethodRefWith3ArgsMethodRefWith4ArgsMethodRefWith5ArgsMethodRefWith6Args……,在大多数情况下就够用了。

不过仍然不能大意。如果目标实例方法的某个参数是 primitive 类型(例如 int 而非 Integer),在撰写用来实际调用 methodReference 的 invoker (即基础的 getReferencedMethod 的第二个参数)的时候需要特殊处理。不能一股脑儿传 null 了。

例如需要匹配的实例方法是 NavigableMap.headMap(K toKey, boolean inclusive),那么用前面的例子就不行,需要修改成:

public static  Method getReferencedMethod(
        Class clazz, 
        MethodRefWith2Args methodRef) {
    return getReferencedMethod(clazz, phantom -> {
        try {
            // 注意第三个参数,必须是一个非 null 的值
            // 否则在 unboxing 的时候会抛出 NullPointerException
            methodRef.accept(phantom, null, new Boolean());
        } catch (Exception e) {
            // 正常情况下,不会跑到这里来
        }
    });
}

后记

有兴趣的可以看看 StackOverflow 上的问题:How to get the MethodInfo of a Java 8 method reference? 在这个问题中我也有简略作答。

你可能感兴趣的:(Java8:如何动态地获取一个 MethodReference 所引用的 Method)