byte-buddy icon indicating copy to clipboard operation
byte-buddy copied to clipboard

java.lang.NoSuchMethodError when using the agent for method instrumentation

Open kavehshahedi opened this issue 1 year ago • 2 comments

Hello,

As shown in the following code, I have created an agent in order to instrument the methods of a project with a specific class name and capture their execution times:

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;

public class FunctionEntryAgent {

    public static void premain(String args, Instrumentation inst) {
        new AgentBuilder.Default()
                .type(ElementMatchers.nameContains("org.HdrHistogram"))
                .transform(
                        (builder, type, classLoader, module, protectionDomain) -> 
                        builder.method(ElementMatchers.any())
                                .intercept(MethodDelegation.to(MethodExecutionTime.class)))
                .installOn(inst);
    }

    public static class MethodExecutionTime {
        private static final String LOG_FILE = "method_logs.log";

        @RuntimeType
        public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception{
            long start = System.nanoTime();
            try {
                return callable.call();
            } finally {
                String message = "[" + (System.nanoTime() - start) + "] "
                        + method.getDeclaringClass().getName() + "." + method.getName() + " "
                        + "(" + Arrays.toString(method.getParameterTypes()) + ") "
                        + method.getReturnType().getName();

                CompletableFuture.runAsync(() -> logMethodExecutionTime(message));
            }
        }

        private static void logMethodExecutionTime(String message) {
            try (Writer writer = new BufferedWriter(new FileWriter(LOG_FILE, true))) {
                writer.write(message + "\n");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

The agent works in most cases, but in some projects, it throws java.lang.NoSuchMethodError error during the execution. For instance, the following error comes from this line of the code return callable.call();:

java.lang.NoSuchMethodError: 'boolean org.HdrHistogram.RecordedValuesIterator.hasNext$original$NZclujSK()'

Based on one of the suggestions in this this issue, I formatted the code in way that it uses Advice instead of MethodDelegation, as follows:

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;

public class FunctionEntryAgent {

    public static void premain(String args, Instrumentation inst) {
        new AgentBuilder.Default()
                .type(ElementMatchers.nameStartsWith("org.HdrHistogram"))
                .transform(new AgentBuilder.Transformer.ForAdvice()
                        .advice(ElementMatchers.isMethod(),
                                MethodExecutionTime.class.getName()))
                .installOn(inst);
    }

    public static class MethodExecutionTime {
        private static final String LOG_FILE = "method_logs.log";

        @Advice.OnMethodEnter
        static long enter(@Advice.Origin Method method) {
            return System.nanoTime();
        }

        @Advice.OnMethodExit
        static void exit(@Advice.Origin Method method, @Advice.Enter long start) {
            long duration = System.nanoTime() - start;
            String message = "[" + duration + "] "
                    + method.getDeclaringClass().getName() + "." + method.getName() + " "
                    + "(" + Arrays.toString(method.getParameterTypes()) + ") "
                    + method.getReturnType().getName();

            CompletableFuture.supplyAsync(new LoggerSupplier(message));
        }

        public static class LoggerSupplier implements Supplier<Void> {
            private final String message;

            public LoggerSupplier(String message) {
                this.message = message;
            }

            @Override
            public Void get() {
                try (Writer writer = new BufferedWriter(new FileWriter(LOG_FILE, true))) {
                    writer.write(message + "\n");
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return null;
            }
        }
    }
}

These modifications fix the error, but the other problem is that the agent is not able to instrument all the classes and subclasses under the target package name. For instance, the agent attaches to a jar, which is a JMH benchmark that is benchmarking a baseline project (both of them have the same package names). The agent instruments the methods within the jmh project correctly (i.e., methods that are targeting the baseline project), but it can't capture the methods from the baseline project. However, when using MethodDelegation instead of Advice, the agent instruments all the methods (but as said, it throws the mentioned error).

I would be glad if you could inform me how to overcome this issue. Thanks!

kavehshahedi avatar May 10 '24 19:05 kavehshahedi

This does not quite make sense to me. Could you investigate how the methods get instrumented? Register an AgentBuilder.Listener, possibly some of the instrumentations are failing

raphw avatar May 10 '24 21:05 raphw

Thanks for the suggestion. After attaching a listener to the agent, this error popped up: Cannot write invoke dynamic instruction for class file version Java 6 (50)

As I investigated the previous issues, it turned out that the lambda dynamic invocation was causing the problem. I re-compiled the agent with Java 7, and the problem seems to be fixed.

Thanks again!

kavehshahedi avatar May 10 '24 21:05 kavehshahedi