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






3.72/5 (10投票s)
本文允许使用匿名类或 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
编译和测试项目即可。