How to remove/replace existing annotations?
Thank you for this wonderful library. However, I am stuck at one point. I am able to add annotations to a field/method, but not able to replace or remove existing annotation.
I have the following requirement, on a method,
@OneToOne (fetch = FetchType.LAZY) // I want to remove / replace annotation values
public Employee getEmployee()
I would like to,
- Remove the existing annotation
@OneToOne (fetch = FetchType.LAZY) - Replace the existing annotation's values with
@OneToOne (fetch = FetchType.EAGER)
I am using the bytebuddy maven plugin, using the DynamicType.Builder to visit the method to add new annotation using MemberAttributeExtension.ForMethod().annotateMethod(), but not sure how to remove or replace existing annotations.
I tried using MemberSubstitution, but did not get that to work for removing / replacing annotations
For your help I will be immensely grateful.
Thanks, glad you like it. Member removal is unfortunately not supported overly well. You'd need to register a AsmClassVisitorWrapper and override the visitAnnotation method to return null if such an annotation is discovered.
With your suggestion and the below code, using AsmVisitorWrapper I am able to remove an existing annotation from a method, however, I also want to replace the removed annotation with a new annotation or just replace the value of the annotation from @OneToOne(fetch = "oldValue") to @OneToOne(fetch = "newValue") instead of removing the annotation completely.
How can I do that?
// Registered a new AsmVisitorWrapper
builder.visit(new AsmVisitorWrapper.ForDeclaredMethods()
.method(ElementMatchers.nameStartsWith("getEmployee"), // METHOD TO MATCH
new AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper() {
@Override
public MethodVisitor wrap(TypeDescription typeDescription, MethodDescription methodDescription,
MethodVisitor methodVisitor, Implementation.Context context,
TypePool typePool, int i, int i1) {
return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
// THE ANNOTATION TO MATCH
if(Type.getDescriptor(OneToOne.class).equals(descriptor)) {
return null; // REMOVES THE ANNOTATION
// HOW CAN I REPLACE ANNOTATION VALUE ???
}
return super.visitAnnotation(descriptor, visible);
}
};
}
}));
So, if you wanted to avoid the visitor, you can set AnnotationRetention.DISABLED in ByteBuddy and apply a Transformer in the method to change the method in question by replacing the entire method object.
Alternatively, you'd need to drop the first annotation of that type being visited. Byte Buddy always plays the preexisting annotations down the visitor chain first.
I was able to replace the annotation value of the existing annotation on a method by using an AnnotationVisitor.
@OneToOne (fetch = FetchType.LAZY)
Replaced to
@OneToOne (fetch = FetchType.EAGER)
The below works, but is it the right way to do using an AnnotationVisitor? Is it the same as what you suggested? or how does it differ from using a transformer?
new AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper() {
@Override
public MethodVisitor wrap(TypeDescription typeDescription, MethodDescription methodDescription,
MethodVisitor methodVisitor, Implementation.Context context, TypePool typePool, int i, int i1) {
return new MethodVisitor(OpenedClassReader.ASM_API, methodVisitor) {
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
// Match the annotation
if (Type.getDescriptor(OneToOne.class).equals(descriptor)) {
return new AnnotationVisitor(OpenedClassReader.ASM_API, visitor) {
// In my case, it is an Enum Value I want to replace
@Override
public void visitEnum(String name, String descriptor, String value) {
// If matches the annotation value to replace, the replace by visiting
// and returning with a new value
if("fetch".equals(name) &&
Type.getDescriptor(FetchType.class).equals(descriptor)) {
super.visitEnum(name, descriptor, FetchType.EAGER.toString());
return;
}
// Else return existing annotation
super.visitEnum(name, descriptor, value);
}
};
}
return super.visitAnnotation(descriptor, visible);
}
};
}
}));
Yes, absolutely. If your replacement is that simple, then using ASM is the best option!
Thank you so much for your help.
Can you point or tell me how instead of ASM, could I use the Transformer? I'd like to try using the Transformer as well to remove/replace annotations.
The transformer can be added in the .method(...)....transform(...) and .field(...).transform(...) steps. The idea is to return an alternative representation of the method or field that you'd like Byte Buddy to materialize. This does however require you to set AnnotationRetention.DISABLED.
Byte Buddy should really offer a convenience DSL here to make these removals simple. I'll leave this ticket open as an enhancement for a future version.
Hi, checking in to see if this feature is since available in bytebuddy? Basically i am trying to replace class level annotation - and I wrote custom AsmVisitorWrapper following notes above -
.transform((builder, typeDescription, classLoader, module) ->
{
return builder.visit(new AsmVisitorWrapper() {
@Override
public int mergeWriter(int flags) {
return 0;
}
@Override
public int mergeReader(int flags) {
return 0;
}
@Override
public ClassVisitor wrap(TypeDescription instrumentedType, ClassVisitor classVisitor, Implementation.Context implementationContext, TypePool typePool, FieldList<FieldDescription.InDefinedShape> fields, MethodList<?> methods, int writerFlags, int readerFlags) {
return new ClassVisitor(ASM4, classVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals("Lio/cucumber/junit/CucumberOptions;")) {
return null;
}
return super.visitAnnotation(descriptor, visible);
}
};
}
}).annotateType(
AnnotationDescription.Builder.ofType(CucumberOptions.class)
.defineArray("plugin", "org.deployd.agent.ListenerPlugin")
.build());
I was expecting this would remove initial annotation at class level and then create new one - however this complains of duplicate annotation - possibly the initial annotation isn't getting removed. Secondly, I tried to change the annotation inside visitAnnotation method but could'nt find API to do so. any pointers are much appreciated. Thank you.
Try disabling annotation retention: https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/implementation/attribute/AnnotationRetention.java#L34
Otherwise, Byte Buddy copies the original annotation to retain the definition of default values compared to explicitly set defaults.
i tried using that as well - I hope this is the way to do it. thank you
new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(AgentBuilder.TypeStrategy.Default.REDEFINE)
.with(AgentBuilder.TypeStrategy.Default.REBASE)
.with(new ByteBuddy().with(AnnotationRetention.DISABLED))
.type(isAnnotatedWith(CucumberOptions.class))
............
Logs:
[Byte Buddy] ERROR com.automatedtest.sample.SearchTest [jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7, unnamed module @f2ff811, loaded=false]
java.lang.IllegalStateException: Duplicate annotation @io.cucumber.junit.CucumberOptions(name={}, strict=false, tags={}, stepNotifications=false, plugin={"org.deployd.agent.ListenerPlugin"}.....
Indeed, the problem happens in validation which is not aware of the visitor. Configure ByteBuddy with TypeValidation.DISABLED to avoid this error.
Thank you @raphw . TypeValidation.DISABLED option is working - though from logs I can that the visitor removes both the annotation - the previously created as well as newly created.
@Override
public ClassVisitor wrap(TypeDescription instrumentedType, ClassVisitor classVisitor, Implementation.Context implementationContext, TypePool typePool, FieldList<FieldDescription.InDefinedShape> fields, MethodList<?> methods, int writerFlags, int readerFlags) {
logger.info("inside visitAnnotation: descriptor - {} visible - {}", classVisitor, instrumentedType);
return new ClassVisitor(ASM4, classVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
logger.info("inside visitAnnotation: descriptor - {} visible - {}", descriptor, visible);
if (descriptor.equals("Lio/cucumber/junit/CucumberOptions;")) {
logger.info("returning null");
return null;
}
return super.visitAnnotation(descriptor, visible);
}
};
}
}).annotateType(
AnnotationDescription.Builder.ofType(CucumberOptions.class)
.defineArray("plugin", "org.deployd.agent.ListenerPlugin")
.build());
and in logs -
443 [main] INFO org.deployd.agent.CucumberAgent - ***** found ***** class com.automatedtest.sample.HomePageTest
445 [main] INFO org.deployd.agent.CucumberAgent - inside visitAnnotation: descriptor - net.bytebuddy.dynamic.scaffold.ClassWriterStrategy$FrameComputingClassWriter@14fc5f04 visible - class com.automatedtest.sample.HomePageTest
445 [main] INFO org.deployd.agent.CucumberAgent - inside visitAnnotation: descriptor - Lorg/junit/runner/RunWith; visible - true
446 [main] INFO org.deployd.agent.CucumberAgent - inside visitAnnotation: descriptor - Lio/cucumber/junit/CucumberOptions; visible - true
446 [main] INFO org.deployd.agent.CucumberAgent - returning null
446 [main] INFO org.deployd.agent.CucumberAgent - inside visitAnnotation: descriptor - Lio/cucumber/junit/CucumberOptions; visible - true
446 [main] INFO org.deployd.agent.CucumberAgent - returning null
I will spend some more time on it and share findings. Thanks for your help as always.
Yes, the visitor you implemented removes all annotations of the given type, if you added them or if they were added previously. The preexisting annotations will be visisted first, you could therefore add a counter to avoid this.
thank you @raphw. I was able to make some progress with your inputs.
Since this issue provided much of the inspiration, here is the visitor I needed to write to parse "most" annotations in a class, possibly amending them with a Remapper. It's not very nice to look at.
private AsmVisitorWrapper rewriteAnnotations(Remapper remapper) {
// Complicated looking code constructs a hierarchical Visitor that finds @annotations and checks their values
return new AsmVisitorWrapper() {
@Override
public int mergeWriter(int flags) {
return flags;
}
@Override
public int mergeReader(int flags) {
return flags;
}
@Override
public ClassVisitor wrap(TypeDescription instrumentedType, ClassVisitor classVisitor,
Context implementationContext, TypePool typePool, FieldList<InDefinedShape> fields,
MethodList<?> methods, int writerFlags, int readerFlags) {
return new ClassVisitor(ASM9, classVisitor) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return new AnnotationRemapper( // class annotations
descriptor,
super.visitAnnotation(descriptor, visible),
remapper);
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor,
String signature, Object value) {
FieldVisitor delegate = super.visitField(access, name, descriptor, signature,
value);
return new FieldVisitor(ASM9, delegate) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return new AnnotationRemapper( // field annotations
descriptor,
super.visitAnnotation(descriptor, visible),
remapper);
}
};
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor delegate = super.visitMethod(access, name, descriptor, signature,
exceptions);
return new MethodVisitor(ASM9, delegate) {
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return new AnnotationRemapper( // method annotations
descriptor,
super.visitAnnotation(descriptor, visible),
remapper);
}
@Override
public AnnotationVisitor visitParameterAnnotation(int parameter,
String descriptor, boolean visible) { // method-parameter annotations
return new AnnotationRemapper(
descriptor,
super.visitParameterAnnotation(parameter, descriptor, visible),
remapper);
}
};
}
};
}
};
}