Search code examples
javareflectionbytecodebyte-buddy

ByteBuddy Proxy Interface


I am trying to convert Cglib proxy to ByteBuddy. Cglib has net.sf.cglib.proxy.Proxy interface to intercept all method calls. I check the documentation of ByteBuddy but couldnt find such an example. Without such interface for every object that i instantiate with ByteBuddy i am repeating same thing again and agin. Is there a better way to do this with ByteBuddy?

Here is my example code snippet:

Service:

public class MyService {

    public void sayFoo() {
        System.out.println("foo");
    }

    public void sayBar() {
        System.out.println("bar");
    }
}

Interceptor:

public class MyServiceInterceptor {

    public void sayFoo(@SuperCall Callable<Void> zuper) {
        try {
            zuper.call();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sayBar(@SuperCall Callable<Void> zuper) {
        try {
            zuper.call();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Test:

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.dynamic.ClassLoadingStrategy;
import net.bytebuddy.instrumentation.MethodDelegation;
import net.bytebuddy.instrumentation.method.matcher.MethodMatchers;

public class Main {

    public static void main(String[] args) throws Exception {
        ByteBuddy buddy = new ByteBuddy(ClassFileVersion.forCurrentJavaVersion());
        Class<? extends MyService> serviceClass =
                buddy.subclass(MyService.class)
                .method(MethodMatchers.named("sayFoo").or(MethodMatchers.named("sayBar")))
                .intercept(MethodDelegation.to(new MyServiceInterceptor()))
                .make()
                .load(Main.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                .getLoaded();

        MyService service = serviceClass.newInstance();

        service.sayFoo();
        service.sayBar();
    }
}

Solution

  • Byte Buddy will look at any possible target method and bind it, if this is possible. If there is more than one possible target method, it will bind the most specific one or throw an exception if it is ambiguous. In your example, the bindings would be ambiguous but as you named the interceptor methods (in MyServiceInterceptor) identically to the intercepted methods (in Service), Byte Buddy figured that intercepting each method with the interceptor method of identical name is probably what you wanted to do. As mentioned in the javadoc of the MethodInterceptor Byte Buddy will:

    1. Find any method that it can bind and discard the others.
    2. Check if a method is annotated with @BindingPriority and choose those with highest priority.
    3. Intercept with a method that has an identical name to the intercepted method if there is at least one.
    4. Choose the method with the most specific argument types if the @Argument annotation is used and resolve the most specific binding analogously to the Java compiler identifying the target of an overloaded method call.
    5. Takes the method with the most parameters.

    You can furthermore alter this default behavior by adding / removing AmbiguityResolvers.

    If you want to specify a single interceptor method which is capable of intercepting any method that has a super method than you can write the following interceptor class:

    public class MyServiceInterceptor {
      @RuntimeType
      public static Object intercept(@SuperCall Callable<?> zuper) throws Exception {
        return zuper.call();
      }
    }
    

    The name of the method does not matter, Byte Buddy will bind the interceptor as it is the only possible target. You need to add the @RuntimeType annotation as the @SuperCall proxy returns an Object and Byte Buddy needs to cast (and possibly unbox) the value inside the intercepted method.

    With this interceptor (note that the method is also static, this way, Byte Buddy does not need to add a field for holding an instance of MyServiceInterceptor) you can simply write:

    public class Main {
      public static void main(String[] args) throws Exception {
        Class<? extends MyService> serviceClass = new ByteBuddy()
          .subclass(MyService.class)
          .method(ElementMatchers.named("sayFoo").or(ElementMatchers.named("sayBar")))
          .intercept(MethodDelegation.to(MyServiceInterceptor.class))
          .make()
          .load(Main.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
          .getLoaded();
    
        MyService service = serviceClass.newInstance();
    
        service.sayFoo();
        service.sayBar();
      }
    }
    

    and you get the desired results. As shown in the example, you can write

    new ByteBuddy();
    

    instead of

    new ByteBuddy(ClassFileVersion.forCurrentJavaVersion());
    

    It is the same thing.

    Byte Buddy does not use any interfaces because it wants to avoid a dependency of the generated class to any of the Byte Buddy classes. Doing so, you can reuse the classes you generate even when loading them with a ClassLoader that is unaware of the Byte Buddy dependency.