question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

MethodGraph incorrectly prefers abstract method from base class to default method from interface

See original GitHub issue

Given 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:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

2reactions
raphwcommented, Nov 2, 2021

This is finally solved in Byte Buddy and will be fixed with the next release!

1reaction
robbytxcommented, Oct 12, 2020

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.

Read more comments on GitHub >

github_iconTop 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 >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found