byte-buddy

Unable to delegate interface method using Byte Buddy


I am working on an add-on to a unit testing tool that creates mocks, and for one feature I want to be able to intercept the call to the mock and cause a call to the super class (i.e. call the real method instead of the mocked one).

To do this I am attempting to create a proxy that intercepts calls to the mock and does this:

      if (someCondition == true) { // condition not important to this q
        // do some stuff then...
        return superMethod.invoke(self, args);
      } else {
        return method.invoke(this.originalMock, args); // original mock is field on interceptor
      }

Since I know the field to which the mock is assigned, I use the type of the field to create the proxy and this is working great for mocks of classes, but fails on mocks of interfaces.

The problem appears to be that ByteBuddy doesn't intercept interface methods, but rather applies a default implementation and then ignores them for interception. I have tried this with 1.15.10 (from EasyMock transitive) and 1.17.2 (forced to latest). Below is an MWE for the root problem. If I can get this main method to complete without error I can likely solve my problem.

MWE:

import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static org.junit.Assert.assertEquals;

import java.lang.reflect.Method;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperMethod;
import net.bytebuddy.implementation.bind.annotation.This;

public class InterceptInterfaceMWE {

  private static Class<?> dynamicSubclass(Class<?> type1, Interceptor target) {
    ByteBuddy byteBuddy = new ByteBuddy();
    return byteBuddy
        .subclass(type1)
        .method(isMethod())
        .intercept(MethodDelegation.to(target))
        .make()
        .load(type1.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
        .getLoaded();
  }

  private static Class<?> dynamicImpl(Class<?> type1, Interceptor target) {
    ByteBuddy byteBuddy = new ByteBuddy();
    return byteBuddy
        .subclass(Object.class)
        .implement(type1)
        .method(isAbstract()) // have also tried isMethod() and any() here, no difference
        .intercept(MethodDelegation.to(target))
        .make()
        .load(type1.getClassLoader())
        .getLoaded();
  }

  public static class Interceptor {

    private String foo;

    public Interceptor(String foo) {
      this.foo = foo;
    }

    @RuntimeType
    public Object intercept(@This Object self, @Origin Method method, @AllArguments Object[] args, @SuperMethod Method superMethod) throws Throwable {
      System.out.println(foo + " intercepted");
      return foo;
    }
  }

  public interface IFace {
    String bar();
  }

  public static class IFaceImpl implements IFace {
    @Override
    public String bar() {
      return "foo";
    }
  }

  public static IFace bbImpl;

  public static void main(String[] args) throws InstantiationException, IllegalAccessException {
    Class<?> test = dynamicSubclass(IFaceImpl.class, new Interceptor("test"));
    bbImpl = (IFace) test.newInstance();
    assertEquals("test", bbImpl.bar()); // passes
    // test = dynamicSubclass(IFace.class, new Interceptor("test")); // also fails assert below
    test = dynamicImpl(IFace.class, new Interceptor("test"));
    bbImpl = (IFace) test.newInstance();
    assertEquals("test", bbImpl.bar()); // fails
  }
}

The main method fails to complete throwing an ComparisonFailure with the message

expected:<[test]> but was:<[com.needhamsoftware.easiermock.InterceptInterfaceMWE$Interceptor@6f3b5d16]>

I found one ugly solution to the MWE that isn't good enough. I did make the MWE pass by repeating "ByteBuddying" things in the event of an interface type, so that the interface is implemented, and the resulting type is then subclassed again by ByteBuddy. That applied the interception to the (now concrete) implementations made by ByteBuddy and output the proper string, but then in the actual code the mock isn't an implementation of the method's class (I think this is because the method now comes from the first round of byte-buddy) and it fails with an exception.

I believe need to accomplish this with only one round of byte-buddy implementation/subclassing.


Solution

  • Your interface will not have an invokable super method, as there is no default implementation. You can however declare

    @SuperMethod(nullIfImpossible = true)