Byte-buddy transform running multiple times for a single execution

心不动则不痛 提交于 2020-12-14 23:54:38

问题


I wrote a javaagent as below to capture the execution time of the execute method of the apache org.apache.http.client.HttpClient. It is capturing the time, but it's running three times.

import java.lang.instrument.Instrumentation;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.implementation.MethodDelegation;

import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAbstract;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.not;

public class TimerAgent {

    public static void premain(
        String arguments,
        Instrumentation instrumentation
    ) {
        new AgentBuilder.Default()
            .type(
                implementsInterface(named("org.apache.http.client.HttpClient"))
            )
            .transform((builder, type, classLoader, module) ->
                           builder.method(isMethod()
                                              .and(named("execute"))
                                              .and(not(isAbstract()))
                                              .and(takesArguments(3))
                                              .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest")))
                                              .and(takesArgument(1, named("org.apache.http.client.ResponseHandler")))
                                              .and(takesArgument(2, named("org.apache.http.protocol.HttpContext"))))
                               .intercept(MethodDelegation
                                              .to(TimingInterceptor.class))
            ).installOn(instrumentation);
    }
}


import java.lang.reflect.Method;
import java.util.concurrent.Callable;

import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;

public class TimingInterceptor {

    @RuntimeType()
    public static Object intercept(
        @Origin Method method,
        @SuperCall Callable<?> callable
    ) {
        long start = System.currentTimeMillis();
        try {
            try {
                return callable.call();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } finally {
            System.out.println(
                "Took " + (System.currentTimeMillis() - start));
        }
        return 0;
    }
}

I am using DefaultHttpClient to make a HTTP request. Client code:

import java.io.IOException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.DefaultHttpClient;

public class Main {

    public static void main(String[] args) throws IOException {

        HttpClient client = new DefaultHttpClient();

        HttpUriRequest httpUriRequest = new HttpGet("http://www.google.com");

        HttpResponse response = client
            .execute(httpUriRequest, new ResponseHandler<HttpResponse>() {
                public HttpResponse handleResponse(final HttpResponse response)
                    throws ClientProtocolException, IOException {
                    return response;
                }
            });
    }
}

Console output:

Took 512
Took 512
Took 512

Maven dependency:

<dependency>
  <groupId>net.bytebuddy</groupId>
  <artifactId>byte-buddy</artifactId>
  <version>1.10.10</version>
</dependency>

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpasyncclient</artifactId>
  <version>4.1.4</version>
  <scope>provided</scope>
</dependency>

This is the implementation for implementsInterface

This is how I am executing the application:

java -javaagent:"javaagent.jar" -jar application.jar

I am not sure what is causing it to print 3 times.

Update: Thanks to @kriegaex for the MCVE, it can be found in this GitHub repository.


回答1:


First of all, where do you get the implementsInterface element matcher from? It is not part of ByteBuddy, at least not in the current version. I replaced it by isSubTypeOf in order to make the code compile. Anyway, if you activate logging like this

new AgentBuilder.Default()
.with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
  .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
  .type(isSubTypeOf(HttpClient.class))
  // ...

you will see something like this on the console:

[Byte Buddy] BEFORE_INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@1eb5174b on sun.instrument.InstrumentationImpl@67080771
[Byte Buddy] INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@1eb5174b on sun.instrument.InstrumentationImpl@67080771
[Byte Buddy] TRANSFORM org.apache.http.impl.client.DefaultHttpClient [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4d518b32, loaded=false]
[Byte Buddy] TRANSFORM org.apache.http.impl.client.AbstractHttpClient [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4d518b32, loaded=false]
[Byte Buddy] TRANSFORM org.apache.http.impl.client.CloseableHttpClient [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4d518b32, loaded=false]
Handling response: HTTP/1.1 200 OK [Date: Mon, 26 Oct 2020 04:46:18 GMT, Expires: -1, Cache-Control: private, max-age=0, Content-Type: text/html; charset=ISO-8859-1, P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info.", Server: gws, X-XSS-Protection: 0, X-Frame-Options: SAMEORIGIN, Set-Cookie: 1P_JAR=2020-10-26-04; expires=Wed, 25-Nov-2020 04:46:18 GMT; path=/; domain=.google.com; Secure, Set-Cookie: NID=204=qecfPmmSAIwXyTGneon07twIXIIw4EyPiArm67OK5nHyUowAq3_8QJZ7gw9k8Nz5ZuGuHoyadCK-nsAKGkZ8TGaD5mdPlAXeVenWwzQrmFkNNDiEpxCj-naf4V6SKDhDUgA18I-2z36ornlEUN7xinrHwWfR0pc4lvlAUx3ssJk; expires=Tue, 27-Apr-2021 04:46:18 GMT; path=/; domain=.google.com; HttpOnly, Accept-Ranges: none, Vary: Accept-Encoding, Transfer-Encoding: chunked] org.apache.http.conn.BasicManagedEntity@6928f576
Took 1870
Took 1871
Took 1871

See? You have installed your interceptor in the whole class hierarchy, three classes all in all:

Hence, you either need to limit your element matcher or have to live with multiple log lines.

Disclaimer: I am not a ByteBuddy expert and maybe Rafael Winterhalter will write a much better answer later, maybe my solution is not canonical.

It looks as if the interceptor matches all classes, even though technically the method is only defined in CloseableHttpClient and not overridden in AbstractHttpClient or DefaultHttpClient. One way to limit type matching is to check if the target class actually contains the method you wish to instrument:

ElementMatcher.Junction<MethodDescription> executeMethodDecription = isMethod()
  .and(named("execute"))
  .and(not(isAbstract()))
  .and(takesArguments(3))
  .and(takesArgument(0, named("org.apache.http.client.methods.HttpUriRequest")))
  .and(takesArgument(1, named("org.apache.http.client.ResponseHandler")))
  .and(takesArgument(2, named("org.apache.http.protocol.HttpContext")));

new AgentBuilder.Default()
  .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
  .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
  .type(
    isSubTypeOf(HttpClient.class)
      .and(declaresMethod(executeMethodDecription))
  )
  .transform((builder, type, classLoader, module) -> builder
    .method(executeMethodDecription)
    .intercept(MethodDelegation.to(TimingInterceptor.class))
  )
  .installOn(instrumentation);

Now the console log should turn into:

[Byte Buddy] BEFORE_INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@67080771 on sun.instrument.InstrumentationImpl@72cde7cc
[Byte Buddy] INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@67080771 on sun.instrument.InstrumentationImpl@72cde7cc
[Byte Buddy] TRANSFORM org.apache.http.impl.client.CloseableHttpClient [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @6ea2bc93, loaded=false]
Handling response: HTTP/1.1 200 OK [Date: Mon, 26 Oct 2020 05:21:25 GMT, Expires: -1, Cache-Control: private, max-age=0, Content-Type: text/html; charset=ISO-8859-1, P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info.", Server: gws, X-XSS-Protection: 0, X-Frame-Options: SAMEORIGIN, Set-Cookie: 1P_JAR=2020-10-26-05; expires=Wed, 25-Nov-2020 05:21:25 GMT; path=/; domain=.google.com; Secure, Set-Cookie: NID=204=N7U6SBBW5jjM1hynbowChQ2TyMSbiLHHwioKYusPVHzJPkiRaSvpmeIlHipo34BAq5QqlJnD7GDD1iv6GhIZlEEl7k3MclOxNY9WGn9c6elHikj6MPUhXsAapYz9pOVFl_DjAInWv5pI00FfUZ6i5mK14kq3JIXu-AV84WKDxdc; expires=Tue, 27-Apr-2021 05:21:25 GMT; path=/; domain=.google.com; HttpOnly, Accept-Ranges: none, Vary: Accept-Encoding, Transfer-Encoding: chunked] org.apache.http.conn.BasicManagedEntity@5471388b
Took 1274

Update: Actually the behaviour of intercept() is described in its javadoc:

Implements the previously defined or matched method by the supplied implementation. A method interception is typically implemented in one of the following ways:

  1. If a method is declared by the instrumented type and the type builder creates a subclass or redefinition, any preexisting method is replaced by the given implementation. Any previously defined implementation is lost.
  2. If a method is declared by the instrumented type and the type builder creates a rebased version of the instrumented type, the original method is preserved within a private, synthetic method within the instrumented type. The original method therefore remains invokeable and is treated as the direct super method of the new method. When rebasing a type, it therefore becomes possible to invoke a non-virtual method's super method when a preexisting method body is replaced.
  3. If a virtual method is inherited from a super type, it is overridden. The overridden method is available for super method invocation.

Update 2: My full MCVE is available for everyone's convenience in this GitHub repository.



来源:https://stackoverflow.com/questions/64530922/byte-buddy-transform-running-multiple-times-for-a-single-execution

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!