土球球
本帖最后由 土球球 于 2020-6-29 11:47 编辑

引言

Forge 在高版本(Minecraft 1.13+)为事件总线添加了使用 Functional Interface 监听事件的方式。以下是接口声明:

  1. <T extends Event> void addListener(Consumer<T> consumer);
复制代码

我们可以注意到,我们不需要向 addListener 方法传入一个额外的 Class 代表事件的类型,也就是说事件类型只能通过我们传入的 Consumer<T> 拿到。换言之,我们需要从 Consumer<T> 中推断出具体的 T 是什么类型,考虑到 Java 的泛型擦除机制,这不得不说是一个困难。

本文将以 Consumer<T> 类型为例,阐述将类型参数从泛型类型的实例(尤其是 Lambda 表达式)中捕获的方式。读者应能相对容易地将其推广到其他类型。

本文将使用 Java 11(实际上相较基于 Java 8 的等价代码,只是单纯地多了一些 var 而已),以证明该解决方案不单纯限于 Java 8 或更低版本。

常规解决方案

有的读者可能会说,这很简单啊:我们只需要通过一点反射的小技巧,就可以拿到具体的类型。

类似的实现有很多,比如 Guava 在 TypeToken 中的实现。这里我们也随手写一个:

  1. public static Class<?> getErased(Type type)
  2. {
  3.     if (type instanceof ParameterizedType)
  4.     {
  5.         return getErased(((ParameterizedType) type).getRawType());
  6.     }
  7.     if (type instanceof GenericArrayType)
  8.     {
  9.         return Array.newInstance(getErased(((GenericArrayType) type).getGenericComponentType()), 0).getClass();
  10.     }
  11.     if (type instanceof TypeVariable<?>)
  12.     {
  13.         var bounds = ((TypeVariable<?>) type).getBounds();
  14.         return bounds.length > 0 ? getErased(bounds[0]) : Object.class;
  15.     }
  16.     if (type instanceof WildcardType)
  17.     {
  18.         var bounds = ((WildcardType) type).getUpperBounds();
  19.         return bounds.length > 0 ? getErased(bounds[0]) : Object.class;
  20.     }
  21.     if (type instanceof Class<?>)
  22.     {
  23.         return (Class<?>) type;
  24.     }
  25.     return Object.class;
  26. }

  27. public static Class<?> getConsumerParameterType(Consumer<?> consumer) throws ReflectiveOperationException
  28. {
  29.     for (var type : consumer.getClass().getGenericInterfaces())
  30.     {
  31.         if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType() == Consumer.class)
  32.         {
  33.             return getErased(((ParameterizedType) type).getActualTypeArguments()[0]);
  34.         }
  35.     }
  36.     if (consumer.getClass().isSynthetic())
  37.     {
  38.         return getConsumerLambdaParameterType(consumer);
  39.     }
  40.     throw new NoSuchMethodException();
  41. }
复制代码

getConsumerParameterType 方法的实现对于匿名内部类非常完美,换句话说,下面这一实例中的具体参数类型将会很容易地被捕获:

  1. Consumer<String> a = new Consumer<String>
  2. {
  3.     @Override
  4.     public void accept(String s)
  5.     {
  6.         System.out.println(s);
  7.     }
  8. }
复制代码

但对于 Lambda 表达式(也包括方法引用)呢?

  1. Consumer<String> b = s -> System.out.println(s);
  2. Consumer<String> c = System.out::println;
复制代码

实际上,我们从 Lambda 表达式中,根本拿不到一个 ParameterizedType,更不要说从里面提取参数类型了。我们需要其他的方法,当然了,也是更为 dirty hack 的方法。

探究 Lambda 表达式常量池

每个 .class 后缀的文件中都有一段二进制存放的是该类或接口的常量池(Constant Pool),其中包含着描述每个字段和方法的符号引用。

我们知道,对于每个 Lambda 表达式,JVM 都会为其生成对应的类型,而它们也包含了常量池。我们需要做的,就是把 Lambda 表达式对应的常量池提取出来,并寻找对我们有用的方法的符号引用。

我们需要排除这些符号引用:

  • 构造方法的符号引用
  • 覆盖 Object 类的方法的符号引用

我们可以通过调用 Class 类的 getConstantPool 方法(非公开)拿到常量池,也就是 ConstantPool 类的实例。但是,从 Java 9 开始,常量池的实现被移动到了 jdk.internal.reflect 包下,换言之,如果想要调用常量池的一些非公开方法,我们需要一些更激进的策略。

我们可以通过替换 Method 类下的 override 字段来绕过 JVM 的限制。为此,我们先编写辅助用的方法:

  1. public static Method getMethod(Class<?> objClass, String methodName) throws NoSuchMethodException
  2. {
  3.     for (var method : objClass.getDeclaredMethods())
  4.     {
  5.         if (methodName.equals(method.getName()))
  6.         {
  7.             return method;
  8.         }
  9.     }
  10.     throw new NoSuchMethodException();
  11. }

  12. public static Object invoke(Object obj, String methodName, Object... args) throws ReflectiveOperationException
  13. {
  14.     var overrideField = AccessibleObject.class.getDeclaredField("override");
  15.     overrideField.setAccessible(true);
  16.     var targetMethod = getMethod(obj.getClass(), methodName);
  17.     overrideField.set(targetMethod, true);
  18.     return targetMethod.invoke(obj, args);
  19. }
复制代码

上面的 invoke 方法将会找到特定名称的方法,并针对性的调用该方法。由于仅为演示可行性用,因此上面的方法并未考虑性能,因此如果读者需要在实际开发中用到,请自行对该实现进行优化。

从常量池中获取参数类型

我们将接下来的工作分为四步:

  • 找到 Lambda 表达式对应的 Class 的常量池
  • 依次遍历常量池中对方法的符号引用
  • 排除无关方法的符号引用
  • 从第一个满足条件的方法中取出参数类型

需要用到 ConstantPool 的两个方法:

  • getSize 方法:用于获取常量池的元素个数
  • getMethodAt 方法:用于获取常量池特定位置对方法的符号引用

注意如果 getMethodAt 寻找的位置对应的不是对方法的符号引用,调用该方法将会报错。我们需要把该报错屏蔽掉,然后尝试寻找下一个常量池中的元素。

下面的实现贯彻了上面提到的四步。需要注意的一点是,遍历常量池是倒序进行的:

  1. public static Class<?> getConsumerLambdaParameterType(Consumer<?> consumer) throws ReflectiveOperationException
  2. {
  3.     var consumerClass = consumer.getClass();
  4.     var constantPool = invoke(consumerClass, "getConstantPool");
  5.     for (var i = (int) invoke(constantPool, "getSize") - 1; i >= 0; --i)
  6.     {
  7.         try
  8.         {
  9.             var member = (Member) invoke(constantPool, "getMethodAt", i);
  10.             if (member instanceof Method && member.getDeclaringClass() != Object.class)
  11.             {
  12.                 return ((Method) member).getParameterTypes()[0];
  13.             }
  14.         }
  15.         catch (Exception ignored)
  16.         {
  17.             // ignored
  18.         }
  19.     }
  20.     throw new NoSuchMethodException();
  21. }
复制代码

我们可以把该方法的调用补充到之前编写的 getConsumerParameterType 方法下。这里使用的判别标准是该方法所对应的类是否是 Java 编译器自动生成的(isSynthetic 方法返回 true):

  1. public static Class<?> getConsumerParameterType(Consumer<?> consumer) throws ReflectiveOperationException
  2. {
  3.     for (var type : consumer.getClass().getGenericInterfaces())
  4.     {
  5.         if (type instanceof ParameterizedType && ((ParameterizedType) type).getRawType() == Consumer.class)
  6.         {
  7.             return getErased(((ParameterizedType) type).getActualTypeArguments()[0]);
  8.         }
  9.     }
  10.     // lambda start
  11.     if (consumer.getClass().isSynthetic())
  12.     {
  13.         return getConsumerLambdaParameterType(consumer);
  14.     }
  15.     // lambda end
  16.     throw new NoSuchMethodException();
  17. }
复制代码

各位可以自己试一试了:

  1. public static void main(String[] args)
  2. {
  3.     try
  4.     {
  5.         Consumer<Consumer<String>> consumerConsumer = c -> c.accept("");
  6.         System.out.println(getConsumerParameterType(consumerConsumer));

  7.         Consumer<String> stringConsumer = System.out::println;
  8.         System.out.println(getConsumerParameterType(stringConsumer));

  9.         Consumer<Long> longConsumer = l -> System.out.println(l + "L");
  10.         System.out.println(getConsumerParameterType(longConsumer));
  11.     }
  12.     catch (ReflectiveOperationException e)
  13.     {
  14.         e.printStackTrace();
  15.     }
  16. }
复制代码

鸣谢



第一页 上一页 下一页 最后一页