Is there any way to call advice() on a loaded class’s methods before the ByteBuddy agent?

Tags: ,



I have implemented a simple agent as below. It works for my custom Foo.class, but I couldn’t assign advice for the java.net.URL class.

Example test code;

    public class AgentTest {

    @Test
    public void advice() throws IOException {
        Foo foo = new Foo();
        File temp = Files.createTempDirectory("tmp").toFile();
        Map<TypeDescription, byte[]> map = new HashMap<>();
        map.put(new TypeDescription.ForLoadedType(URL.class), ClassFileLocator.ForClassLoader.read(URL.class));
        ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, ByteBuddyAgent.install()).inject(map);

        new AgentBuilder.Default()
                .disableClassFormatChanges()
                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                .with(AgentBuilder.TypeStrategy.Default.REBASE)
                .type(is(URL.class).or(is(Foo.class)))
                .transform(
                        new AgentBuilder.Transformer.ForAdvice()
                                .advice(
                                        isMethod().and(isPublic()).and(named("openConnection")).or(named("myMethod")),
                                        FooAdvice.class.getName()
                                )
                )
                .installOnByteBuddyAgent();

        foo.myMethod();
    }

    public static class FooAdvice {

        @Advice.OnMethodEnter
        public static void enter() {
            System.out.println("1- method entered !");
        }

        @Advice.OnMethodExit
        public static void exit() {
            System.out.println("2- method exited");
        }
    }
}

Is there any specific approach to binding advice‘s java.net.URL class methods so that the class is loaded before the ByteBuddy agent?

Answer

Rafael is right, as usual. I want to elaborate on your question a bit more, though: You are having a bootstrapping problem here:

  • The advice needs to be on the bootstrap class path, otherwise it cannot be used with the bootstrap class URL.
  • The advice needs ByteBuddy because of the annotations. So BB also has to be on the bootstrap class path.
  • If you put your sample code into a single class and also use the BB agent library from there, that one also needs to be on the bootstrap class path. I.e. if you want to run this code
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;

import static net.bytebuddy.matcher.ElementMatchers.*;

class AgentTest {
  public static void main(String[] args) throws IOException {
    ByteBuddyAgent.install();
    new AgentBuilder.Default()
      .disableClassFormatChanges()
      .ignore(none())
      .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
      .with(AgentBuilder.RedefinitionStrategy.Listener.StreamWriting.toSystemError())
      .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
      .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
      .type(is(URL.class).or(is(Foo.class)))
      .transform(
        new AgentBuilder.Transformer.ForAdvice()
          .advice(
            isMethod().and(isPublic()).and(named("openConnection")).or(named("myMethod")),
            FooAdvice.class.getName()
          )
      )
      .installOnByteBuddyAgent();

    new Foo().myMethod();
  }

  public static class Foo {
    public void myMethod() throws IOException {
      new URL("https://google.de").openConnection();
    }
  }

  public static class FooAdvice {
    @Advice.OnMethodEnter
    public static void enter(@Advice.Origin Method method) {
      System.out.println("Entering " + method);
    }

    @Advice.OnMethodExit
    public static void exit(@Advice.Origin Method method) {
      System.out.println("Exiting " + method);
    }
  }

}

you need to add something like this on the Java command line:

java -Xbootclasspath/a:/path/to/my-classes;/path/to/byte-buddy-1.10.13.jar;/path/to/byte-buddy-agent-1.10.13.jar ... AgentTest

Then you get this output:

[Byte Buddy] BEFORE_INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer@27bc2616 on sun.instrument.InstrumentationImpl@3941a79c
[Byte Buddy] REDEFINE BATCH #0 [2 of 2 type(s)]
[Byte Buddy] TRANSFORM AgentTest$Foo [null, null, loaded=true]
[Byte Buddy] TRANSFORM java.net.URL [null, null, loaded=true]
[Byte Buddy] REDEFINE COMPLETE 1 batch(es) containing 2 types [0 failed batch(es)]
[Byte Buddy] INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer@27bc2616 on sun.instrument.InstrumentationImpl@3941a79c
Entering public void AgentTest$Foo.myMethod() throws java.io.IOException
Entering public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Entering public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Exiting public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Entering public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Entering public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Exiting public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Exiting public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Exiting public java.net.URLConnection java.net.URL.openConnection() throws java.io.IOException
Exiting public void AgentTest$Foo.myMethod() throws java.io.IOException

As an alternative, you can use a spring-board Java agent which dynamically puts BB and your transformer onto the bootstrap class path without directly referring to any of their classes. You can subsequently kick off transformation via reflection.



Source: stackoverflow