Introduction
Every method in Java class corresponds to Method
object, that can be retrieved in the runtime. This object has a plenty of useful features: it describes the properties of the method, can invoke method itself, retrieves method’s annotations etc.
To get Method
object we need its name and parameter types.
Class clazz = object.getClass();
String methodName = ...
Class [] methodParams = new Class[] {};
Method method = clazz.getDeclaredMethod(methodName, methodParams);
Unfortunately, the method does not relate to its String
name, so if programmer changes the method name, the program fails in the runtime.
In .NET the excerption of MethodInfo
(.NET name for java's Method
) not via method’s name is quite simple [1]:
- Create delegate with the same signature
- Call the delegate’s
GetMethodInfo()
To solve this problem in Java the recorder-technique [2] is used:
- Create the proxy
- Call the method of proxy
- Record the method name and parameter types
- Excerpt the
Method
object from the record’s data
The famous Mockito unit-testing [3] framework uses such technique.
There is almost impossible to proxy static and final methods using recorder-technique.
The unit-testing framework PowerMock [4] solves the problem of static and final methods via custom class loaders. However, PowerMock often conflicts with frameworks like Spring that use their own class loaders.
This article introduces another technique without any limitations on static and final methods and without any usage of custom class loaders.
Anonymous Classes (Old JDK)
If we have to use old JDK, then anonymous classes are the only option:
Method method = new ReflectionUtils() {
public void marker() {
obj.testMethod(null);
}
}.method();
The implementation of abstract marker()
method plays the same role as in recorder-technique. The marker()
method is never called, the compiled bytecode is inserted instead. Then the bytecode is analyzed using ASM and the appropriate Method
is extracted.
For Method
extraction the following steps should be performed:
- Find
marker
method in compiled bytecode - Get bytecode of
marker
method - Find
AbstractInsnNode.METHOD_INSN
instruction in bytecode - Extract
MethodInsnNode
- Transform
MethodInsnNode
to Method
Then the different things could be done:
- Build dynamic proxy around extracted
Method
- Transform it to
MethodHandle
- Bind
Method
's parameters to their predefined values etc.
In order to find marker
method we are looking for the single abstract
method in the ReflectUtils.getAbstractMethod()
:
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);
}
To extract appropriate Method
from class we use AsmUtils.getMethod()
.
Firstly we need to obtain InputStream
for given Class
object:
class AsmUtils {
...
private static InputStream getClassStream(Class<?> clazz) throws IOException {
String className = clazz.getName();
String classResource = "/"+className.replaceAll("\\.", "/")+".class";
return clazz.getResourceAsStream(classResource);
}
...
}
Then, to extract Method
from this InputStream
some code manipulation with ASM framework should be done:
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);
}
}
...
}
Ultimately, the getMethodFromNode
is used for extraction Method
from MethodNode
:
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);
}
}
Additional details could be found in attached source code reflect-utils-jdk-6.zip
Extraction of Method
Information from Lambda (New JDK)
Suppose we have a class TestClass
with method testMethod
class TestClass {
public void testMethod(String name) {
...
}
}
The goal is to define some API to get Method
object related to testMethod
Let RS
be the interface:
@FunctionalInterface
public interface RS extends Runnable, Serializable {
default Method method() {
return LambdaUtils.fromLambda(this);
}
}
Interface RS
allows to extract Method
from appropriate lambda-object. The method is called inside lambda-object to record its name and parameter types:
TestClass obj = new TestClass();
Method method = ((RS)()->{obj.testMethod(null);}).method();
The lambda is never called, it is used only for recording the method.
RS
interface extends the marking interface Serializable
and makes the lambda a serializable object.
Every serializable lambda has writeReplace
method called by the java-runtime during serialization process.
As the result of writeReplace
invocation, we get SerializedLambda
object.
Below we call this method using reflection:
public class SerializedLambdaHelper {
...
public static SerializedLambda getSerializedLambda(Serializable lambda){
Method method = ClassUtils.getMetod(lambda.getClass(), "writeReplace");
return (SerializedLambda) ClassUtils.invokeMethod(lambda, method);
}
}
For Method
extraction from SerializedLambda
the following steps should be performed:
- Find the
MethodNode
containing this lambda - Find
MethodInsnNode
inside this MethodNode
- Get the
Method
from MethodInsnNode
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);
}
...
}
The low-level implementation details could be found in the class AsmUtils
from attached code.
Running Examples
The Maven should be installed.
I tested the examples using JDK-8 and JDK-9. You could run examples using IDE of your choice, or from the command line. If you use IDE, just import Maven project.
Alternatively, you could run the project from command line. Just compile and test project using mvn test
in the pom.xml
directory.
References
- How to get the MethodInfo of a Java 8 method reference?
- Typesafe database interaction with Java 8
- Mockito framework
- PowerMock
- Can you inspect the byte code of a Java 8 lambda at runtime?
- Reflection type inference on Java 8 Lambdas