2

I'm trying to annotate all public methods in my class annotated with my custom annotation using Byte Buddy.

I've already tried to use the code from the comment here: Add method annotation at runtime with Byte Buddy

Java version: 1.8. The app is for testing microservices. Application is running via Spring Boot. I try to annotate all needed methods in my app with annotation with value depending on method name.

            <dependency>
                <groupId>org.reflections</groupId>
                <artifactId>reflections</artifactId>
                <version>0.9.11</version>
            </dependency>
            <dependency>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy</artifactId>
                <version>1.10.1</version>
            </dependency>
            <dependency>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy-agent</artifactId>
                <version>1.10.1</version>
            </dependency>  

Working method:


import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.qameta.allure.Step;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.asm.MemberAttributeExtension;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.matcher.ElementMatchers;
import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;

public class StackOverflowExample {

    private static final String REGEX = "some-package";

    public void configureAnnotation() {
        Reflections reflections = getReflections();
        Set<Class<?>> allClasses = reflections.getSubTypesOf(Object.class);
        allClasses.forEach(clazz -> {
            if (clazz.isAnnotationPresent(ConfigureSteps.class)) {
                List<Method> publicMethods = Arrays.stream(clazz.getDeclaredMethods())
                                                   .filter(method -> Modifier.isPublic(method.getModifiers()))
                                                   .collect(Collectors.toList());
                AnnotationDescription annotationDescription = AnnotationDescription.Builder.ofType(Step.class)
                                                                                           .define("value", "new annotation")
                                                                                           .build();
                publicMethods.forEach(method -> new ByteBuddy().redefine(clazz)
                                                               .visit(new MemberAttributeExtension.ForMethod()
                                                                              .annotateMethod(annotationDescription)
                                                                              .on(ElementMatchers.anyOf(method)))
                                                               .make());
            }
        });
    }

    private Reflections getReflections() {
        return new Reflections(new ConfigurationBuilder().setScanners(new SubTypesScanner(false), new ResourcesScanner())
                                                         .addUrls(ClasspathHelper.forJavaClassPath())
                                                         .filterInputsBy(new FilterBuilder().include(REGEX)));
    }
}

I call configureAnnotation method before all the tests using JUnit @BeforeAll annotation. Method is invoked without issues but methods in my class with ConfigureSteps annotation aren't annotated with Step annotation. What is the problem? Or may be I should build Agent like it is in tutorial here: http://bytebuddy.net/#/tutorial And in this case what way should I override transform method?

UPDATED: Added load method in chain; added

ByteBuddyAgent.install()
public class StackOverflowExample {

    private static final String REGEX = "example-path";

    public void configureAnnotation() {
        Reflections reflections = getReflections();
        Set<Class<?>> allClasses = reflections.getTypesAnnotatedWith(ConfigureSteps.class);
        ByteBuddyAgent.install();
        allClasses.forEach(clazz -> {
            List<Method> publicMethods = Arrays.stream(clazz.getDeclaredMethods())
                                               .filter(method -> Modifier.isPublic(method.getModifiers()))
                                               .collect(Collectors.toList());
            AnnotationDescription annotationDescription = AnnotationDescription.Builder.ofType(Step.class)
                                                                                       .define("value", "new annotation")
                                                                                       .build();
            publicMethods.forEach(method -> new ByteBuddy().redefine(clazz)
                                                           .visit(new MemberAttributeExtension.ForMethod()
                                                                          .annotateMethod(annotationDescription)
                                                                          .on(ElementMatchers.anyOf(method)))
                                                           .make()
                                                           .load(clazz.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent(
                                                                   ClassReloadingStrategy.Strategy.REDEFINITION)));
        });
    }

    private Reflections getReflections() {
        return new Reflections(new ConfigurationBuilder().setScanners(new TypeAnnotationsScanner(), new SubTypesScanner(false), new ResourcesScanner())
                                                         .addUrls(ClasspathHelper.forJavaClassPath())
                                                         .filterInputsBy(new FilterBuilder().include(REGEX)));
    }
}

Also I defined new class for agent, don't really understand if it is needed because as I see it doesn't work with loaded classes and I have loaded one. Example was taken partly from: Redefine java.lang classes with ByteBuddy. Trying to add breakpoint on this method, but application didn't stop there

import java.lang.instrument.Instrumentation;

import net.bytebuddy.agent.builder.AgentBuilder;

public class ExampleAgent {
    public static void premain(String arguments,
                               Instrumentation instrumentation) {
        new AgentBuilder.Default()
                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
                .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
                .installOn(instrumentation);
    }
}

Now the following problem is presented: java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)

  • note: don't just use `reflections.getSubTypesOf(Object.class);` there is method to get only classes with given annotation, then you will not load all class path classes and it will be much faster – GotoFinal Aug 21 '19 at 09:41
  • @GotoFinal yep, will use it, thanks. But the main problem is still remaining – youcancallmeyourmajesty Aug 21 '19 at 11:47

1 Answers1

0

By calling make(), you will generate a class file and throw it out immediately after.

Instead, you need to redefine the class by applying a ClassRedefinitionStrategy in a load step onto the class's class loader. This is only possible if you have an instance of Instrumentation available which you can access via ByteBuddyAgent.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • I finally add `ByteBuddyAgent.install()` before forEach method and `.load(clazz.getClassLoader(),ClassReloadingStrategy.fromInstalledAgent( ClassReloadingStrategy.Strategy.REDEFINITION)))` to my method above and I face the exception `java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)`. Trying to read bytebuddy tutorial but didn't find the answer. BTW should I define Agent in separate class with `premain` method? – youcancallmeyourmajesty Aug 21 '19 at 08:56
  • Try adding `byteBuddy.with(Implementation.Context.Disabled.Factory.INSTANCE)` - I am not entirely sure why the class format would change for your code but this should prevent it or give you a better exception. – Rafael Winterhalter Aug 21 '19 at 13:00
  • still face the same issue – youcancallmeyourmajesty Aug 21 '19 at 13:32
  • I've also tried to make it via javassist library. And it helps me to write a new file with created annotation and I do see only 2 main differences: 1) public methods are annotated with @Step annotation 2) new import of annotation is added – youcancallmeyourmajesty Aug 21 '19 at 13:45
  • Are you running the latest version? You could set the system property `-Dnet.bytebuddy.dump=/some/folder` to get the class files before and after transformation. Open a ticket at Byte Buddy's GitHub including these files and I can tell you what the problem is. – Rafael Winterhalter Aug 22 '19 at 07:27
  • I've found what it generates a number of files according to number of public methods inside target class. as I see this is because of for loop. Can I prevent this situation by making class once after adding all annotations? I've tried to do make and load operations outside the loop but this didn't help. – youcancallmeyourmajesty Aug 22 '19 at 08:48