Search code examples
javabyte-buddy

How to correctly add a method to an existing class with Byte Buddy


I am facing the same issue as the one described here redefining method with Byte Buddy, however I am not sure how to adapt the solution to my use case:

I am trying to implement the active record pattern by delegating the method implementations to an interceptor. The ActiveRecord base class is defined as follows:

public class ActiveRecord {

   private Long id;

   public Long getId() {
       return id;
   }

   public void setId(Long id) {
       this.id = id;
   }

   private static IllegalStateException implementationMissing() {
      return new IllegalStateException(
            "This method must be overridden in subclasses");
  }

  public static Long count(){
      throw implementationMissing();
  }

  public void save(){
    throw implementationMissing();
  }
// extra methods omitted
}

A child class would then extend active record as follows:

class MapText extends ActiveRecord{
    private String text;
    private String description;

    private double wgs84Latitude;
    private double wgs84Longitude;
    // getters and setters omitted

}

Using Byte Buddy, I am trying to delegate the count and save methods to an interceptor class as follows:

@Test
void testRedefine(){
    ByteBuddyAgent.install();
    new ByteBuddy().redefine(MapText.class)
            .defineMethod("save", void.class, Visibility.PUBLIC)
            .intercept(MethodDelegation.to(ActiveRecordInterceptor.class))
            .defineMethod("count", Long.class, Visibility.PUBLIC)
            .intercept(MethodDelegation.to(ActiveRecordInterceptor.class))
            .make()
            .load(MapText.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
    
    MapText mapText = new MapText();
    // set properties
    mapText.save();
    MapText.count();
}

Which generates the following exception:

java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

If I add empty "placeholder" methods for save() and count() in MapText, then everything works fine.

How should I adapt my code to make the delegation work without requiring empty placeholder methods in the subclass?

Edit: changed the code to use the AgentBuilder API according to feedback

 @Test
 void testRedefine(){
 ByteBuddyAgent.install();
    new AgentBuilder.Default()
            .disableClassFormatChanges()
            .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
            .type(ElementMatchers.named("pkg.MapText"))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                    return builder.defineMethod("save", void.class, Visibility.PUBLIC)
                            .intercept(MethodDelegation.to(ActiveRecordInterceptor.class));
                }
            }).with(new ListenerImpl()).installOnByteBuddyAgent();
    CallTextSave callTextSave =  new CallTextSave();
    callTextSave.save();
    }    

CallTextSave encapsulates the MapText class and calls it save method. Unfortunately MapText.save() is not intercepted.

public class CallTextSave {

   public void save(){
       MapText text = new MapText();
       text.save(); // Method not intercepted

   }
}

Solution

  • If you want to alter code this way, you would need to do this before it is loaded for the first time. You can do so by defining a Java agent using the AgentBuilder API. You must avoid referring to the loaded class in the agent code, rather use named for a matcher that takes the string name as an argument.

    Alternatively, you can redefine the class in your main method by resolving the class using a TypePool.Default. Again, resolve the TypeDescription by the name and avoid loading it. Also, move the actual code to a different class as the JVM validator will otherwise load the class in question.

    This latter approach is only possible if you control the life cycle of your application.