1

I'd like to add the following code to existing classes using ByteBuddy. Given an existing class SomeSample I want to turn this into the follwoing:

class SomeSample {

  private @Transient boolean isNew = true;

  public boolean isNew() {
    return isNew;
  }

  @PrePersist
  @PostLoad
  void markNotNew() {
    this.isNew = false;
  }
}

Original attempt

I can get the field and methods added properly. What I cannot really get to work is the assignment of the value. I've learned that I need to augment all existing constructors of the class I want to augment as technically an assignment declared like this is compiled into the constructors.

I've created the following helper:

public class Helper {

  @OnMethodExit
  public static void initField(@FieldValue(value = "isNew", readOnly = false) boolean value) {
    value = !value;
  }
}

and tried to assign this as follows:

builder.constructor(ElementMatchers.any())
  .intercept(Advice.to(Helper.class));

I would've expected the advice to be added at the end of the original declarations but during the build I receive the following error:

Failed to transform class files in …: Cannot call super (or default) method for public ….SomeSample()

Alternative approach

Instead of flipping the value of the field I thought I could also stay with the default (false) and negate the value returned from the generated isNew() method. If I change my helper to this:

public class Helper {

  public static boolean isNew(@FieldValue(value = "isNew") boolean value) {
    return !value;
  }
}

When I change my method generating code for isNew() to the following:

builder = builder.defineMethod("isNew", boolean.class, Visibility.PUBLIC)
  .intercept(MethodDelegation.to(Helper.class));

I get:

None of [public static boolean ….Helper.isNew(boolean)] allows for delegation from public boolean SomeSample.isNew()

Any idea?

2
  • 1
    @FieldValue(value = "isNew"), are you using the Advice version of the annotation here by accident? Commented Mar 3, 2021 at 10:06
  • 🤦🏼‍♂️ That was indeed the case and seems to solve the delegation error message. Commented Mar 3, 2021 at 13:37

2 Answers 2

1

That was maybe an unfortunate API choice but you can use Advice both as decorator and interceptor. What you likely would want to would be to set:

builder = builder.visit(Advice.to(Helper.class).on(isConstructor()))

This adds the code around the existing code. With the suggested approach, you replace the method around a call to the original implementation. If you define a new method, such an implementation does not exist and the error you are seeing is yielded.

Sign up to request clarification or add additional context in comments.

3 Comments

That already gets me further, thanks. I am now stuck with ….SomeSample() does not define an index 0. Full stack trace in this gist.
that indicates you are using the incorrect annotation and advice falls back to the default of mapping the argunent with the same parameter index.
Once in untangled my code trying to get Helper work with both the constructor and the isNew() method using the correct annotation, everything works as you describe. Once again, a big thank you for helping out! 🙇‍♂️
1
builder = builder
  .defineField("isNew", boolean.class, Visibility.PRIVATE)
  .annotateField(yourTransientAnnotationLiteral);

builder = builder
  .defineConstructor(Visibility.PUBLIC)
  .intercept(MethodCall.invokeSuper()
               .andThen(FieldAccessor.of("isNew")
                          .setsValue(Boolean.TRUE)));

builder = builder
  .defineMethod("isNew", boolean.class, Visibility.PUBLIC)
  .intercept(FieldAccessor.of("isNew"));

builder = builder
  .defineMethod("markNotNew", TypeDescription.VOID, Visibility.PACKAGE_PRIVATE)
  .intercept(FieldAccessor.of("isNew")
               .setsValue(Boolean.FALSE));

This is untested (I'm particularly not sure about the implementation of the isNew() method) but I hope gives you an idea of one general way to do this programmatically without using @Advice or other higher-level mechanisms.

2 Comments

That's helpful, too, thanks. The issue I have with that approach is that I need to add the setting of the field to every constructor already present in the class.
So in that case instead of defineConstructor in my example, use constructor (javadoc.io/static/net.bytebuddy/byte-buddy/1.10.19/net/…) and you should be good.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.