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
}
}
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.