9

The problem is to create a dynamic enhanced version of existing objects.

I cannot modify the object's Class. Instead I have to:

  • subclass it
  • wrap the existing object in the new Class
  • delegate all the original method calls to the wrapped object
  • implement all methods that are defined by another interface

The interface to add to existing objects is:

public interface EnhancedNode {

  Node getNode();
  void setNode(Node node);

  Set getRules();
  void setRules(Set rules);

  Map getGroups();
  void setGroups(Map groups);

}

With Byte Buddy I managed to subclass and to implement my interface. The problem is the delegation to the wrapped object. The only way to do this that I found is using reflection what is too slow (I have heavy load on the application and performance is critical).

So far my code is:

Class<? extends Node> proxyType = new ByteBuddy()
     .subclass(node.getClass(), ConstructorStrategy.Default.IMITATE_SUPER_TYPE_PUBLIC)
     .method(anyOf(finalNode.getClass().getMethods())).intercept(MethodDelegation.to(NodeInterceptor.class))
     .defineField("node", Node.class, Visibility.PRIVATE)
     .implement(EnhancedNode.class).intercept(FieldAccessor.ofBeanProperty())
     .defineField("groups", Map.class, Visibility.PRIVATE)
     .implement(EnhancedNode.class).intercept(FieldAccessor.ofBeanProperty())
     .defineField("rules", Set.class, Visibility.PRIVATE)
     .implement(EnhancedNode.class).intercept(FieldAccessor.ofBeanProperty())
     .make()
     .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
     .getLoaded();
enhancedClass = (Class<N>) proxyType;
EnhancedNode enhancedNode = (EnhancedNode) enhancedClass.newInstance();
enhancedNode.setNode(node);

where Node is the object to subclass/wrap. The NodeInterceptor forwards the invoked methods to the getNode property.

Here the code of the NodeInterceptor:

public class NodeInterceptor {

  @RuntimeType
  public static Object intercept(@Origin Method method,
                               @This EnhancedNode proxy,
                               @AllArguments Object[] arguments)
        throws Exception {
      Node node = proxy.getNode();
      Object res;
      if (node != null) {
          res = method.invoke(method.getDeclaringClass().cast(node), arguments);
      } else {
          res = null;
      }
      return res;
  }
}

Everything is working but the intercept method is too slow, I'm planning to use ASM directly to add the implementation of every method of Node but I hope there is a simpler way using Byte Buddy.

Captain Man
  • 6,997
  • 6
  • 48
  • 74
Teg
  • 1,302
  • 13
  • 32
  • I don't know Byte-Buddy, but I see a chance that a new proxy class is created over and over again. Is there some kind of caching in place? – JimmyB Jan 04 '16 at 16:40
  • Btw, how do you enforce that the given node's class has a public default constructor? – JimmyB Jan 04 '16 at 16:49
  • You have an interface, you add fields and methods... Why can't you just create a wrapper class by yourself without this dynamic enhance? – AdamSkywalker Jan 04 '16 at 17:00
  • @HannoBinder The class is cached for every Node subclass, and you are right, the classes need the default constructor, this is another open problem ;) – Teg Jan 04 '16 at 22:04
  • @AdamSkywalker I need the dynamic enhancer because the Nodes are in huge collections, if I wrap them in a different class I have to cycle over the collection to extract them every time the client needs a subset of nodes, this is a problem for performances. With a subclass I can return a collection of wrappers without conversions – Teg Jan 04 '16 at 22:07
  • @Teg the ByteBuddy documentation talks about caching `Method` objects by setting cache to true in the `@Origin` annotation. Have you checked the performance implication of that? – Brett Okken Feb 04 '16 at 03:12

1 Answers1

5

You probably want to use a Pipe rather than the reflection API:

public class NodeInterceptor {

  @RuntimeType
  public static Object intercept(@Pipe Function<Node, Object> pipe,
                                 @FieldValue("node") Node proxy) throws Exception {
      return proxy != null
        ? pipe.apply(proxy);
        : null;
  }
}

In order to use a pipe, you first need to install it. If you have Java 8 available, you can use java.util.Function. Otherwise, simply define some type:

interface Function<T, S> { S apply(T t); }

yourself. The name of the type and the method are irrelevant. The install the type:

MethodDelegation.to(NodeInterceptor.class)
                .appendParameterBinder(Pipe.Binder.install(Function.class));

Are you however sure that the reflection part is the critical point of your application's performance problems? Are you caching the generated classes correctly and is your cache working efficiently? The reflection API is faster than its reputation, especially since use Byte Buddy tends to imply monomorphic call sites.

Finally, some general feedback. You are calling

.implement(EnhancedNode.class).intercept(FieldAccessor.ofBeanProperty())

multiple times. This has no effect. Also, method.getDeclaringClass().cast(node) is not necessary. The reflection API does the cast for you.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • 3
    As a performance note, on my unit test I make 1.000.000.000 calls to methods (both added and proxed methods), with the reflection I had almost 10 seconds of execution time, with your solution I had about 9 ms execution time, well done! – Teg Jan 05 '16 at 09:53
  • Great, thanks for the feedback. I wonder what Java version you are running. Reflection improved a lot over the latest versions and I run all of my performance tests on a recent Java 8 VM. – Rafael Winterhalter Jan 05 '16 at 10:00
  • The unit test runs with a Oracle JDK 1.8.0_45 on Mac OS X, the performance of the reflection API is relevant only in critical components like this, where the invocations are too many – Teg Jan 05 '16 at 10:31
  • 1
    The performance of the reflection API varies a lot with call site monomorphism. This is similar for cglib. Byte Buddy aims for call site monomorphism, I guess that is what you are experiencing. (As a note for future readers.) – Rafael Winterhalter Jan 05 '16 at 14:09
  • Another note about performances, with Oracle JDK 1.7 the Pipe solution became about 3 seconds instead of the 9 ms of the JDK 1.8 – Teg Jan 05 '16 at 15:47
  • That is most likely because of weaker escape analyzis. – Rafael Winterhalter Jan 05 '16 at 16:57