MethodGraph incorrectly prefers abstract method from base class to default method from interface
See original GitHub issueGiven a class hierarchy where some method’s implementation exists strictly as a default method in a Java 8 interface:
public interface Interface {
String getType();
}
public static abstract class BaseClass implements Interface {
}
public interface SubInterfaceWithType extends Interface {
default String getType() {
return "SubInterfaceWithType";
}
}
public static class ConcreteClass extends BaseClass implements SubInterfaceWithType {
}
ByteBuddy incorrectly passes the abstract method rather than the default method when delegating to the implementation. This issue appears to stem from the fact that the MethodGraph compiler incorrectly the abstract method from the superclass over the default method implementation inherited from the interface.
Using the class hierarchy above, and the test code below, I get the following result (where this issue is denoted by INCORRECTLY
):
For class example.Test$ConcreteClass:
public abstract java.lang.String example.Test$Interface.getType()
returns:
SubInterfaceWithType
For class example.Test$ConcreteClass$ByteBuddy$2K32eamD:
public java.lang.String example.Test$ConcreteClass$ByteBuddy$2K32eamD.getType()
returns:
INCORRECTLY intercepted ABSTRACT method: public abstract java.lang.String example.Test$Interface.getType()
----------------------------------------
For interface example.Test$Interface:
net.bytebuddy.dynamic.scaffold.MethodGraph$Linked$Delegation@8a6fb820
correctly contains:
public abstract java.lang.String example.Test$Interface.getType()
For class example.Test$BaseClass:
net.bytebuddy.dynamic.scaffold.MethodGraph$Linked$Delegation@863996eb
correctly contains:
public abstract java.lang.String example.Test$Interface.getType()
For interface example.Test$SubInterfaceWithType:
net.bytebuddy.dynamic.scaffold.MethodGraph$Linked$Delegation@bfa0171d
correctly contains:
public java.lang.String example.Test$SubInterfaceWithType.getType()
For class example.Test$ConcreteClass:
net.bytebuddy.dynamic.scaffold.MethodGraph$Linked$Delegation@28abaec0
INCORRECTLY contains:
public abstract java.lang.String example.Test$Interface.getType()
Here the full example code:
package example;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.BindingPriority;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.StubValue;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;
import static net.bytebuddy.implementation.MethodDelegation.to;
public class Test {
public interface Interface {
String getType();
}
public static abstract class BaseClass implements Interface {
}
public interface SubInterfaceWithType extends Interface {
default String getType() {
return "SubInterfaceWithType";
}
}
public static class ConcreteClass extends BaseClass implements SubInterfaceWithType {
}
/**
* Mimicks Mockito's own <a href="https://github.com/mockito/mockito/blob/893e2f476445ce273d82ec73a93ced713610df86/src/main/java/org/mockito/internal/creation/bytebuddy/MockMethodInterceptor.java#L126">DispatcherDefaultingToRealMethod</a>
*/
public static class DispatcherDefaultingToRealMethod {
@SuppressWarnings("unused")
@RuntimeType
@BindingPriority(BindingPriority.DEFAULT * 2)
public static Object interceptSuperCallable(
@This Object mock,
@Origin Method invokedMethod,
@AllArguments Object[] arguments,
@SuperCall(serializableProxy = true) Callable<?> superCall)
throws Throwable {
return "correctly intercepted actual method: " + invokedMethod;
}
@SuppressWarnings("unused")
@RuntimeType
public static Object interceptAbstract(
@This Object mock,
@StubValue Object stubValue,
@Origin Method invokedMethod,
@AllArguments Object[] arguments)
throws Throwable {
return "INCORRECTLY intercepted ABSTRACT method: " + invokedMethod;
}
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException {
Class<? extends ConcreteClass> original = ConcreteClass.class;
Class<? extends ConcreteClass> intercepted = new ByteBuddy(ClassFileVersion.JAVA_V8)
.subclass(original)
.method(any -> true)
.intercept(to(DispatcherDefaultingToRealMethod.class))
.make()
.load(original.getClassLoader())
.getLoaded();
for (Class<? extends ConcreteClass> type : Arrays.asList(original, intercepted)) {
ConcreteClass instance = type.newInstance();
System.out.println("For " + type + ":");
System.out.println(" " + type.getMethod("getType"));
System.out.println("returns:");
System.out.println(" " + instance.getType());
System.out.println();
}
System.out.println("----------------------------------------\n");
for (Class<?> type : Arrays.asList(Interface.class, BaseClass.class, SubInterfaceWithType.class, ConcreteClass.class)) {
MethodGraph.Linked graph = MethodGraph.Compiler.DEFAULT.compile(TypeDescription.ForLoadedType.of(type));
MethodDescription methodDescription = graph.listNodes().asMethodList().stream()
.filter(candidate -> candidate.getName().equals("getType"))
.findFirst().orElseThrow(IllegalStateException::new);
System.out.println("For " + type + ":");
System.out.println(" " + graph);
if (SubInterfaceWithType.class.isAssignableFrom(type) && methodDescription.isAbstract()) {
System.out.print("INCORRECTLY ");
} else {
System.out.print("correctly ");
}
System.out.println("contains:");
System.out.println(" " + methodDescription);
System.out.println();
}
System.out.println();
}
}
Among other potential impact, this issue appears to causes Mockito spies to return null
from such methods rather invoking the actual implementation.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:5 (2 by maintainers)
Top Results From Across the Web
Interface With Default Methods vs Abstract Class - Baeldung
In this tutorial, we'll take a closer look at both the interface and abstract class to see how they differ. 2. Why Use...
Read more >When to use: Java 8+ interface default method, vs. abstract ...
In interfaces only have public methods. So one reason you would use an abstract base class is if your classes have a property...
Read more >Why can't abstract classes have default methods (even though ...
All non-abstract methods in an abstract class are inherited by its sub-classes. So they are already kind of “default” methods if that's what...
Read more >What are the Difference Between Interface and Abstract Class?
Java 8 Features Tutorials | What are the Difference Between Interface and Abstract Class | Java Tutorials | by Mr.Ramachandra** For Online ...
Read more >The definitive guide to JProfiler - ej-technologies
Adding the VM parameter is the preferred way to profile and is ... By default, the JProfiler agent binds the communication socket to...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
This is finally solved in Byte Buddy and will be fixed with the next release!
By the way, I just want to say that the ByteBuddy library is extremely impressive – in its capabilities, its relative ease of use, and its impact in the JVM ecosystem – so thank you for the work you have put into building this!
Please consider this issue simply as a “heads up” – I have no expectation that this will be fixed on any particular timeline, but I simply wanted to make you and others aware of this problem, and I wanted to document the issue for myself related to my own workaround.