raphw / byte-buddy

Runtime code generation for the Java virtual machine.
https://bytebuddy.net
Apache License 2.0
6.27k stars 806 forks source link

Agent creation with common dependencies : LinkageError #1723

Open allemas opened 5 days ago

allemas commented 5 days ago

Hello,

I'm trying to instrument the vert.x context to manually add the span trace as OpenTelemetry does.

I'm facing a classpath error that I can't fix correctly.

Caused by: java.lang.LinkageError: loader constraint violation: when resolving method 'io.vertx.core.Handler com.byteprofile.bytecodr.agent.instrumentation.vertx.HandlerWrapper.wrap(io.vertx.core.Handler)' the class loader io.quarkus.bootstrap.runner.RunnerClassLoader @386f0da3 of the current class, io/vertx/ext/web/impl/RouteImpl, and the class loader 'app' for the method's defining class, com/byteprofile/bytecodr/agent/instrumentation/vertx/HandlerWrapper, have different Class objects for the type io/vertx/core/Handler used in the signature (io.vertx.ext.web.impl.RouteImpl is in unnamed module of loader io.quarkus.bootstrap.runner.RunnerClassLoader @386f0da3, parent loader 'app'; com.byteprofile.bytecodr.agent.instrumentation.vertx.HandlerWrapper is in unnamed module of loader 'app')

In the OTEL repository the HandlerWrapper implement Handler<T>. who's a vertx dependency.

I'm quite sure I should isolate classpath for agent and quarkus, is this really the right thing to do? Do you have some examples or pointers ?

Here my ByteBuddy agent, maybe I miss something ?


import io.vertx.core.Handler;

new AgentBuilder
                .Default()
                .ignore(ElementMatchers.nameStartsWith("net.bytebuddy."))
                .with(RETRANSFORMATION) 
                .disableClassFormatChanges()
                .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())  
                .type(is(RouteImpl.class))
                .transform(new AgentBuilder.Transformer.ForAdvice()
                        .advice(ElementMatchers.named("handler")
                                        .and(takesArgument(0, named("io.vertx.core.Handler"))),
                                HandlerVisitorCallSite.class.getName()
                        ))
                ;

public class HandlerVisitorCallSite {
    @Advice.OnMethodEnter()
    public static void onEnter(
            @Advice.Argument(value = 0, readOnly = false, typing = Assigner.Typing.DYNAMIC) Handler<?> handler) {
        System.out.print("ENTER the method: " + '\n');
        Handler<?> handler = HandlerWrapper.wrap(handler);
    }
}

import io.vertx.core.Handler;

public class HandlerWrapper<T> implements Handler<T> {
    private final Handler<T> delegate;

    private HandlerWrapper(Handler<T> delegate) {
        this.delegate = delegate;
    }

    public static <T> Handler<T> wrap(Handler<T> handler) {
        handler = new HandlerWrapper<>(handler);
        return handler;
    }

    @Override
    public void handle(T t) {
        delegate.handle(t);
    }
}

Thank you very much for your time

raphw commented 5 days ago

Instead of is(RouteImpl.class), use named("io.vertx.RouteImpl"), assuming this is the right package. Avoid loading user classes from your agent as far as possible.

allemas commented 5 days ago

Nop it doesn't work, the error probably comes from the fact that I'm using Handler<T> and that it's also used in quarkus

raphw commented 5 days ago

Yes, indeed on a second look the problem is likely that you contain vertx in your agent and that the handler class is loaded twice. What you need to do: exclude the vertx dependencies from your agent, and avoid calling the vertx classes from it. You should only depend on it for compiling the advice and defining the helper classes.

Resolving the advice is already handled by Byte Buddy, but in your transform method, you will also need to inject the helper class. You can do so by using a ClassInjector, but I just tried to make this more convenient by adding auxiliary methods to the ForAdvice class. You can build Byte Buddy from source if you wanted to use this, or wait for the next release.

allemas commented 1 day ago

I think what I'm looking for is something resembling as a dynamic invocation mechanism as described in the AssignReturned chapter: https://www.elastic.co/fr/blog/embracing-invokedynamic-to-tame-class-loaders-in-java-agents .

I already have the shared class loader part with :

new AgentBuilder.Transformer.ForAdvice().include(classLoader)

but now when I run the agent in the application, I get a java.lang.NoClassDefFoundError problem when the application tries to create a class that implements Handler

I think the answer is around net.bytebuddy.dynamic.loading.MultipleParentClassLoader and Advice.withCustomBinding.

Just to clarify, in the latest version of bytebuddy, is the Advice.withCustomBinding method replaced by Advice.withCustomMapping()?

                     new AgentBuilder.Transformer.ForAdvice()
                        .include(classLoader) 
                        .advice(ElementMatchers.named("handler")), Advice.withCustomMapping().....)

I'm afraid I'm trying to use it the wrong way.

raphw commented 12 hours ago

If you run with the latest version of Byte Buddy, you can add: .auxiliary("com.acme.HandlerWrapper") what should solve your issue.