Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to remove/replace existing annotations? #917

Open
initshdb opened this issue Sep 1, 2020 · 15 comments
Open

How to remove/replace existing annotations? #917

initshdb opened this issue Sep 1, 2020 · 15 comments
Assignees
Milestone

Comments

@initshdb
Copy link

initshdb commented Sep 1, 2020

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,

  1. Remove the existing annotation @OneToOne (fetch = FetchType.LAZY)
  2. 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.

@raphw
Copy link
Owner

raphw commented Sep 1, 2020

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.

@raphw raphw self-assigned this Sep 1, 2020
@raphw raphw added the question label Sep 1, 2020
@raphw raphw added this to the 1.10.14 milestone Sep 1, 2020
@initshdb
Copy link
Author

initshdb commented Sep 1, 2020

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);
                                    }
                                };
                            }
                     }));

@raphw
Copy link
Owner

raphw commented Sep 2, 2020

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.

@initshdb
Copy link
Author

initshdb commented Sep 3, 2020

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);
                            }
                        };
                    }
               }));

@raphw
Copy link
Owner

raphw commented Sep 3, 2020

Yes, absolutely. If your replacement is that simple, then using ASM is the best option!

@initshdb
Copy link
Author

initshdb commented Sep 4, 2020

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.

@raphw
Copy link
Owner

raphw commented Sep 6, 2020

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.

@chaudharydeepak
Copy link

chaudharydeepak commented Dec 21, 2021

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.

@raphw
Copy link
Owner

raphw commented Dec 21, 2021

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.

@chaudharydeepak
Copy link

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

@raphw
Copy link
Owner

raphw commented Dec 21, 2021

Indeed, the problem happens in validation which is not aware of the visitor. Configure ByteBuddy with TypeValidation.DISABLED to avoid this error.

@chaudharydeepak
Copy link

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.

@raphw
Copy link
Owner

raphw commented Dec 22, 2021

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.

@chaudharydeepak
Copy link

thank you @raphw. I was able to make some progress with your inputs.

@drekbour
Copy link

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);
                            }
                        };
                    }
                };
            }
        };
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants