65.9K
CodeProject 正在变化。 阅读更多。
Home

从成员调用中提取方法对象

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.72/5 (10投票s)

2017年12月11日

CPOL

3分钟阅读

viewsIcon

7984

downloadIcon

15

本文允许使用匿名类或 lambda 表达式提取 java.lang.reflect.Method

引言

Java 类中的每个方法都对应于一个 Method 对象,该对象可以在运行时检索。此对象具有许多有用的功能:它描述了方法的属性,可以调用方法本身,检索方法的注解等。

要获取 Method 对象,我们需要它的名称和参数类型。

    Class clazz = object.getClass();
    String methodName = ...//The <code>String</code> name of the method
    Class [] methodParams = new Class[] {/*The types of the parameters*/};
    Method method = clazz.getDeclaredMethod(methodName, methodParams); 

不幸的是,该方法与其 String 名称无关,因此如果程序员更改方法名称,则程序在运行时会失败。

在 .NET 中,提取 MethodInfo (java 的 Method 的 .NET 名称) 而不是通过方法名称非常简单 [1]

  • 创建具有相同签名的委托
  • 调用委托的 GetMethodInfo()

 

为了解决 Java 中的这个问题,使用了记录器技术 [2]

  • 创建代理
  • 调用代理的方法
  • 记录方法名称和参数类型
  • 从记录的数据中提取 Method 对象

著名的 Mockito 单元测试 [3] 框架使用了这种技术。

 

使用记录器技术几乎不可能代理静态方法和 final 方法。

单元测试框架 PowerMock [4] 通过自定义类加载器解决了静态方法和 final 方法的问题。但是,PowerMock 经常与像 Spring 这样使用自己类加载器的框架冲突。

本文介绍了一种无需限制静态方法和 final 方法且不使用任何自定义类加载器的技术。

匿名类 (旧 JDK)

如果我们必须使用旧的 JDK,那么匿名类是唯一的选择

    Method method = new ReflectionUtils() {
        public void marker() {
            obj.testMethod(null);
        }
    }.method();

抽象 marker() 方法的实现起着与记录器技术中相同的作用。 marker() 方法永远不会被调用,而是插入编译后的字节码。然后,使用 ASM 分析字节码并提取相应的 Method

 

对于 Method 提取,应执行以下步骤

  • 在编译后的字节码中查找 marker 方法
  • 获取 marker 方法的字节码
  • 在字节码中查找 AbstractInsnNode.METHOD_INSN 指令
  • 提取 MethodInsnNode
  • MethodInsnNode 转换为 Method

然后可以做不同的事情

  • 围绕提取的 Method 构建动态代理
  • 将其转换为 MethodHandle
  • Method 的参数绑定到其预定义的值等。

 

为了找到 marker 方法,我们在 ReflectUtils.getAbstractMethod() 中寻找单个 abstract 方法

    public static Method getAbstractMethod(Class<?> clazz) {
        List<method> methodList = new ArrayList<>();
        getAbstractMethods(clazz, methodList);
        if(methodList.size() == 0) return null;
        if(methodList.size() != 1) throw new ReflectUtilsException("The class have more then one abstract method");
        return methodList.get(0);
    }

要从类中提取相应的 Method,我们使用 AsmUtils.getMethod()
首先,我们需要获取给定 Class 对象的 InputStream

class AsmUtils {
...
    private static InputStream getClassStream(Class<?> clazz) throws IOException {
        String className = clazz.getName();
        String classResource = "/"+className.replaceAll("\\.", "/")+".class";
        return clazz.getResourceAsStream(classResource);
    }
...
}

然后,要从此 InputStream 中提取 Method,应使用 ASM 框架进行一些代码操作

class AsmUtils {
...
    public static java.lang.reflect.Method getMethod(Class<?> clazz, String methodName) {
        try {
            InputStream is = getClassStream(clazz);
            ClassReader cr = new ClassReader(is);
            ClassNode cn = new ClassNode();
            cr.accept(cn, 0);
            for(MethodNode mn: (List<methodnode>) cn.methods) {
                if(mn.name.equals(methodName)) {
                    return getMethodFromNode(mn);
                }
            }
            throw new ReflectionUtilsException("Class does not have 'consume' method");
            
        } catch (IOException e) {
            throw new ReflectionUtilsException(e);
        }
    }

...
}

最终,getMethodFromNode 用于从 MethodNode 中提取 Method

    private static java.lang.reflect.Method getMethodFromNode(MethodNode mn) {
        MethodInsnNode methNode = findMethod(mn);
        String name = methNode.name;
        Method method = new Method(name, methNode.desc);
        Class<?>[] classArray = getClassArray(method);

        String className = methNode.owner.replaceAll("/", ".");
        Class<?> clazz = getClass(className);

        try {
            java.lang.reflect.Method meth = clazz.getDeclaredMethod(name, classArray);
            meth.setAccessible(true);
            return meth;
        } catch (Exception e) {
            throw new ReflectionUtilsException(e);
        }
    }

更多详细信息可以在附加的源代码 reflect-utils-jdk-6.zip 中找到

从 Lambda 表达式中提取 Method 信息 (新 JDK)

假设我们有一个带有方法 testMethod 的类 TestClass

class TestClass {
    public void testMethod(String name) {
        ...
    }
}

目标是定义一些 API 来获取与 testMethod 相关的 Method 对象

 

RS 为接口

@FunctionalInterface
public interface RS extends Runnable, Serializable {
    default Method method() {
        return LambdaUtils.fromLambda(this);
    }
}

接口 RS 允许从相应的 lambda 对象中提取 Method。该方法在 lambda 对象内部被调用,以记录其名称和参数类型

    TestClass obj = new TestClass();
    Method method = ((RS)()->{obj.testMethod(null);}).method();

lambda 表达式永远不会被调用,它仅用于记录方法。
RS 接口扩展了标记接口 Serializable,并使 lambda 表达式成为可序列化的对象。
每个可序列化的 lambda 表达式都有一个 writeReplace 方法,该方法在序列化过程中由 java 运行时调用。
作为 writeReplace 调用的结果,我们得到 SerializedLambda 对象。

 

下面我们使用反射调用此方法

public class SerializedLambdaHelper {
...
    public static SerializedLambda getSerializedLambda(Serializable lambda){
        Method method = ClassUtils.getMetod(lambda.getClass(), "writeReplace");
        return (SerializedLambda) ClassUtils.invokeMethod(lambda, method);
    }
}

对于从 SerializedLambda 提取 Method,应执行以下步骤

  • 找到包含此 lambda 表达式的 MethodNode
  • 在此 MethodNode 中找到 MethodInsnNode
  • MethodInsnNode 获取 Method
public class SerializedLambdaHelper {
    ...
    public static Method getReflectedMethod(Serializable lambda) {
        SerializedLambda serializedLambda = getSerializedLambda(lambda);
        Class<?> containingClass = ClassUtils.getClassFromStr(serializedLambda.getImplClass());
        String methodName = serializedLambda.getImplMethodName();
        MethodNode methodNode = AsmUtils.findMethodNode(containingClass, methodName);
        MethodInsnNode methNode = AsmUtils.findMethod(methodNode, false);
        return AsmUtils.getMethodFromMethodNode(methNode);
    }
    ...
}

 

底层实现细节可以在附加代码中的类 AsmUtils 中找到。

 

运行示例

应安装 Maven。
我使用 JDK-8 和 JDK-9 测试了这些示例。您可以使用您选择的 IDE 运行示例,也可以从命令行运行。如果您使用 IDE,只需导入 Maven 项目即可。
或者,您可以从命令行运行该项目。只需在 pom.xml 目录中使用 mvn test 编译和测试项目即可。

参考文献

  1. 如何获取 Java 8 方法引用的 MethodInfo?
  2. 使用 Java 8 的类型安全数据库交互
  3. Mockito 框架
  4. PowerMock
  5. 你能在运行时检查 Java 8 lambda 的字节码吗?
  6. Java 8 Lambdas 上的反射类型推断
© . All rights reserved.