Chain/transform method calls with ByteBuddy

Viewed 1172

Using ByteBuddy, can I implement one instance method by calling another and transforming the result?

For instance (toy example):

public abstract class Foo {
  public String bar() {
    return "bar";
  }

  public abstract int baz();
}

Given the above, can I implement baz such that it calls bar() and returns the length of the returned string? I.e., as if it were:

public int baz() {
  return bar().length();
}

Naively, I tried the following:

Method bar = Foo.class.getDeclaredMethod("bar");
Method baz = Foo.class.getDeclaredMethod("baz");

Method length = String.class.getDeclaredMethod("length");

Foo foo = new ByteBuddy()
  .subclass(Foo.class)
  .method(ElementMatchers.is(baz))
  .intercept(
    MethodCall.invoke(bar)                 // call bar()...
      .andThen(MethodCall.invoke(length))  // ... .length()?
  ).make()
  .load(Foo.class.getClassLoader())
  .getLoaded()
  .newInstance();

System.out.println(foo.baz());

However, it looks like I was wrong in thinking andThen() is invoked on the return value of the first invocation; it looks like it's invoked on the generated instance.

Exception in thread "main" java.lang.IllegalStateException: 
  Cannot invoke public int java.lang.String.length() on class Foo$ByteBuddy$sVgjXXp9
    at net.bytebuddy.implementation.MethodCall$MethodInvoker$ForContextualInvocation
    .invoke(MethodCall.java:1667)

I also tried an interceptor:

class BazInterceptor {
  public static int barLength(@This Foo foo) {
    String bar = foo.bar();
    return bar.length();
  }
}

with:

Foo foo = new ByteBuddy()
  .subclass(Foo.class)
  .method(ElementMatchers.is(baz))
  .intercept(MethodDelegation.to(new BazInterceptor()))
  // ...etc.

This ran, but produced the nonsensical result 870698190, and setting breakpoints and/or adding print statements in barLength() suggested it's never getting called; so clearly I'm not understanding interceptors or @This properly, either.

How can I get ByteBuddy to invoke one method and then invoke another on its return value?


Per k5_'s answer: BazInterceptor works if either:

  • we delegate to new BazInterceptor(), as above, but make barLength() an instance method, or:
  • we leave barLength() a class method, but delegate to BazInterceptor.class instead of to an instance.

I suspect the 870698190 was delegating to hashCode() of the BazInterceptor instance, though I didn't actually check.

2 Answers
Related