9

In Java it is possible to create dynamic proxies using an implementation of InvocationHandler. Despite JVM optimizations, using reflection will always have some overhead invoking a method.

To try to solve this problem, I tried to use ByteBuddy to create the proxy classes at runtime, but the documentation didn't seem clear enough on this aspect.

How do I create a MethodCallProxy in order to forward a method invocation to some class instance?

Edit:

To better clarify my problem, I am providing an example of what I want to achieve:

I am building an RPC system. On each side of a method invocation, I have an interface defining the contract (when both caller/callee are running under the JVM).

@Contract
interface ISomeService {
    fun someMethod(arg0: String, arg1: SomePojo): PojoResult
}

At the call site, I inject a proxy that intercepts all method calls and forwards them to the callee.

ByteBuddy()
    .subclass(Any::class.java)
    .implement(serviceClass)

    // Service contract method delegation
    .method(isDeclaredBy(serviceClass)).intercept(
      MethodDelegation
          .to(ServiceProxyInterceptor())
          .filter(not(isDeclaredBy(Any::class.java)))
    )

    .make()
    .load(this)
    .loaded as Class<T>

And, finally, at the callee, I have several handlers, one for each service method, responsible for unmarshalling the invocation parameters and forwarding them to the service implementation.

@Service
class SomeServiceImpl {
    fun someMethod(arg0: String, arg1: SomePojo): PojoResult {
        // ...
    }
}

I could solve this problem using code generation, but the resulting jar file can become very big. Thus, I want to create a generic version of these handlers and, in each instance, attach a proxy that intercepts every method call to ISomeService and forwards them to SomeServiceImpl.

Carlos Melo
  • 3,052
  • 3
  • 37
  • 45

1 Answers1

11

There are many ways of creating proxy classes in Byte Buddy. The exact way depends on your use-case. The easiest way might be to use the InvocationHandlerAdapter. Given that you want to create a proxy for SomeClass, you can create one using:

Class<? extends SomeClass> proxy = new ByteBuddy()
  .subclass(SomeClass.class)
  .method(ElementMatchers.any())
  .intercept(InvocationHandlerAdapter.of(invocationHandler))
  .make()
  .load(SomeClass.class.getClassLoader());

If you want to create a proxy with a delegate to different instance, you would additionally define a field. This can be done by the following instructions:

Class<? extends SomeClass> proxy = new ByteBuddy()
  .subclass(SomeClass.class)
  .defineField("handler", InvocationHandler.class, Visibility.PUBLIC)
  .method(ElementMatchers.any())
  .intercept(InvocationHandlerAdapter.toField("handler"))
  .make()
  .load(SomeClass.class.getClassLoader());

You would set the above field via reflection or by implementing a setter interface such as for example:

interface HandlerSetter {
  InvocationHandler getHandler();
  void setHandler(InvocationHandler handler);
}

Class<? extends SomeClass> proxy = new ByteBuddy()
  .subclass(SomeClass.class)
  .defineField("handler", InvocationHandler.class, Visibility.PUBLIC)
  .implement(HandlerSetter.class)
  .intercept(FieldAccessor.ofField("handler"))
  .method(ElementMatchers.any())
  .intercept(InvocationHandlerAdapter.toField("handler"))
  .make()
  .load(SomeClass.class.getClassLoader());

You can now instantiate the class and cast the class to the interface for setting the handler.

Beyond the InvocationHandler, there are many other ways to create a proxy. One way would be using MethodDelegation which is more flexible, often faster and allows you to invoke a super method on demand. A forwarding insrumentation can also be applied using a MethodCall or a Forwarding instrumentation. You can find detailed information in the respective classes javadoc.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Of course, to address that specific part of the question, there is no reason to assume that a ByteBuddy generated proxy calling an `InvocationHandler` is more efficient that a JRE generated `java.lang.reflect.Proxy` calling an `InvocationHandler`… – Holger Oct 28 '16 at 14:42
  • The difference is mostly that the JVM only supports interfaces whereas Byte Buddy can also instrument classes. I think the performance aspect indicated the benefits of code generation over reflection. The latter is however doubtful. For the most, I see reflection inflation to suffice. – Rafael Winterhalter Oct 28 '16 at 15:48
  • I know the difference between ByteBuddy’s and Reflection’s capabilities, but the question does not indicate that this is an issue at all. The only mentioned motivation is the claimed overhead of Reflection. There might be scenarios where performance matters, but when sticking to `InvocationHandler`, the code generation is addressing the wrong end, as e.g., the boxing of primitive types and stuffing the arguments into an array still happens… – Holger Oct 28 '16 at 16:33
  • @Rafael, I tried to implement a proxy using `MethodCall` and forwarding the vararg parameter of an interface via `withAllArguments()`. The problem is that ByteBuddy recognizes the vararg parameter as an array, as opposed to the behaviour of `with(Object ... args)`. – Carlos Melo Oct 28 '16 at 18:52
  • 1
    At runtime, there is no vararg. Simply provide the above method with a single array as an argument but cast it to Object. I could overload the method, but even then, the call would be ambiguous. Let me think how I can improve this! – Rafael Winterhalter Oct 28 '16 at 21:26
  • 1
    Maybe, I got it wrong, but I read the OP’s description as having an issue with `withAllArguments()`, not with the method `with(Object...)`. Further, the varargs nature of a method is detectable at runtime… – Holger Oct 31 '16 at 12:50
  • In the meaning of "varargs are handled by javac". – Rafael Winterhalter Oct 31 '16 at 13:19
  • @EdMelo Are you trying to read an object array containing all arguments in a boxed form? Look into `MethodDelegation` and `@AllArguments`. – Rafael Winterhalter Oct 31 '16 at 13:20
  • @RafaelWinterhalter actually I'm trying the opposite. I have a concrete class which is the target impl. In this RPC system, I detect the target class at runtime and forward the call to it, that's why `InvocationHandler` is such a good fit for the problem. I didn't know beforehand about reflection inflation. I do, however, use `MethodDelegation` with `@AllArguments` at the calling side. – Carlos Melo Oct 31 '16 at 13:30
  • @RafaelWinterhalter I don't know if I made my problem clear enough. If not, I can update the question with further details. – Carlos Melo Oct 31 '16 at 13:31
  • @EdMelo Maybe you can add an example to your question? – Rafael Winterhalter Oct 31 '16 at 14:49
  • @RafaelWinterhalter I added it. Thanks for taking the time to verify this. – Carlos Melo Oct 31 '16 at 19:01
  • @EdMelo: Do I understand it correctly that you want to use Byte Buddy to unroll an array into its individual arguments, similar to the reflection API's `invoke` method? If so, this is not supported, just use the reflection API for this. The overhead is more or less non-existant. – Rafael Winterhalter Nov 01 '16 at 16:48
  • @RafaelWinterhalter that's exactly it. Thanks. – Carlos Melo Nov 01 '16 at 16:52
  • @EdMelo The reflection API has a minimal overhead that comes from people comparing it to the unreflected invocation. The real overhead does however come from boxing and unboxing the arguments. As this is not possible to avoid in this case, I always recommend people to use reflection instead. Reflection used to be slower in older Java versions but with any recent VMs, this is no longer a problem. – Rafael Winterhalter Nov 01 '16 at 17:00
  • 1
    @RafaelWinterhalter I see. I didn't know about reflection inflation either. Also, I wasn't aware boxing and unboxing had a sensible overhead. If it ever becomes a problem, I can change the impl to generate the handler bytecode in order to avoid it. – Carlos Melo Nov 01 '16 at 17:04
  • Great example - i just replacement of CatchExceptions library. It was causing conflicts due to usage of Mockito < 2.* and as a result it was using cglib which is replaced by ByteBuddy in Mockito 2. – Antoniossss Dec 19 '18 at 10:09
  • 1
    This SO question https://stackoverflow.com/questions/60317919/byte-buddy-method-interception-invocationhandler-vs-methoddelegation-to-general covers the difference between proxy via Reflection InvocationHandler and Byte Buddy MethodDelegation – FlyingSheep Feb 24 '20 at 13:29