Isaac
Isaac

Reputation: 16736

Using Byte Buddy to expand Spring Boot's classpath

The AWS SDK can locate API call interceptors via classpath scanning (looking for software/amazon/awssdk/global/handlers/execution.interceptors and instantiating classes specified there).

I'm writing a Java Agent with the intention of causing my interceptors to be locatable by the AWS SDK.

For regular standalone applications, this is a no-brainer, as the Java Agent is automatically added to the runtime classpath of the application. The AWS SDK finds my interceptors with no problem.

However, this approach completely breaks with Spring Boot applications where the AWS SDK is bundled as a dependency under BOOT-INF/lib. The reason boils down to Spring Boot's classloading hierarchy. My interceptor class can be found, but its loading fails due to inability to find AWS's ExecutionInterceptor, as it is loaded in a "lower" classloader in the hierarchy.

So I figured that my approach should be to somehow modify Spring Boot's classloader search. However, I'm facing these issues:

I've read of Byte Buddy being able to help in such "interesting" circumstances but haven't found a way to make this work yet. Any ideas?

(EDIT: I'm looking for a solution that doesn't require code/packaging changes, hence the Java Agent approach)

(EDIT: Things I've tried)

Following Rafael's answer: The method in the SDK that resolves all interceptors is in the class SdkDefaultClientBuilder, and is called resolveExecutionInterceptors.

The following, then, works for standalone JARs which are not SpringBoot applications:

    public static void installAgent(Instrumentation inst) {
        new AgentBuilder.Default()
            .with(RedefinitionStrategy.DISABLED)
            .type(ElementMatchers.nameEndsWith("SdkDefaultClientBuilder"))
            .transform(
                    new Transformer() {
                @Override
                public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription,
                        ClassLoader classLoader, JavaModule module) {
                    return builder.visit(Advice.to(MyAdvice.class).on(ElementMatchers.named("resolveExecutionInterceptors")));
                }
            }
                    ).installOn(inst);
    }

For SpringBoot applications, however, it looks like the advice isn't applied at all. I am guessing that this is because the SdkDefaultClientBuilder type isn't even available at the time when the agent starts. It is available during SpringBoot's runtime, in a different classloader.

Upvotes: 1

Views: 651

Answers (1)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44007

Byte Buddy allows you to inject code in any method of any class, so the first and only major thing you would need to find out would be where your interceptor is instantiated. This can typically be done by setting a breakpoint in the constructor of the interceptor in the working scenario and investigating the methods in the stack. Find out where the classes are discovered, for example the method where software/amazon/awssdk/global/handlers/execution.interceptors is read.

Once you have identified this method, you would need to find a way to manually extract the interceptors defined by your agent and to manually add them. For example, if the file-extracted interceptors are added to an argument of type List<Interceptor>, you could use Byte Buddy to modify this method to also add those of your agent.

Normally, you use Byte Buddy's AgentBuilder in conjunction with Advice to do so. Advice let's you inline code into another method as for example, assuming you find a method with an argument of type List<Interceptor>:

class MyAdvice {
  @Advice.OnMethodEnter
  static void enter(@Advice.Argument(0) List<Interceptor> interceptors) {
    interceptors.addAll(MyAgent.loadMyInterceptors());
  }
}

You can now inline this code into the method in question by:

class MyAgent {
  public static void premain(String arg, Instrumentation inst) {
    new AgentBuilder.Default().type(...).transform((builder, ...) -> builder
      .visit(Advice.to(MyAdvice.class).on(...))).install(inst);
  }
}

You might need to use AgentBuilder.Transformer.ForAdvice if the classes in question are not available on the agent's class loader where Byte Buddy resolves the advice using both the target and the agent class loader.

Upvotes: 1

Related Questions