David Moles
David Moles

Reputation: 51113

Chain/transform method calls with ByteBuddy

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:

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

Upvotes: 3

Views: 1572

Answers (2)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

There is not currently a good way in Byte Buddy but this would be an easy feature to add. You can track the progress on GitHub. I will add it once I find some time.

If you want to implement such chained calls today, you can implement them in Java code and inline this code using the Advice component. Alternatively, you can write the byte code more explicitly by creating your own ByteCodeAppender based on MethodInvocation instances where you have to load the arguments manually however.

Upvotes: 2

k5_
k5_

Reputation: 5558

You use an instance as interceptor, that means instance methods are prefered (maybe static method are not accepted at all). There is an instance method that matches the signature of your int baz() method, it is int hashCode(). The number you are getting is the hashcode of the new BazInterceptor() instance.

Options i am aware of:

  • Remove static from barLength that way it will actually be used for interception.
  • Add the class as interceptor .intercept(MethodDelegation.to(BazInterceptor.class))

I would prefer the second option as you are not using any fields/state of the BazInterceptor instance.

Upvotes: 1

Related Questions