Byte-buddy的转换在单次执行中运行多次。

huangapple go评论156阅读模式
英文:

Byte-buddy transform running multiple times for a single execution

问题

我写了一个如下的 javaagent 用于捕获 Apache org.apache.http.client.HttpClientexecute 方法的执行时间。它能够捕获时间,但运行了三次。

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;
    }
}

我正在使用 DefaultHttpClient 发起 HTTP 请求。
客户端代码:

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;
                }
            });
    }
}

控制台输出:

Took 512
Took 512
Took 512

Maven 依赖:

<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>

这是 implementsInterface 的实现

这是我执行应用程序的方式:

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

我不确定是什么原因导致它打印了 3 次。

更新:
感谢 @kriegaex 提供了最小可复现示例(MCVE),你可以在此 GitHub 存储库中找到它。

英文:

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(&quot;org.apache.http.client.HttpClient&quot;))
)
.transform((builder, type, classLoader, module) -&gt;
builder.method(isMethod()
.and(named(&quot;execute&quot;))
.and(not(isAbstract()))
.and(takesArguments(3))
.and(takesArgument(0, named(&quot;org.apache.http.client.methods.HttpUriRequest&quot;)))
.and(takesArgument(1, named(&quot;org.apache.http.client.ResponseHandler&quot;)))
.and(takesArgument(2, named(&quot;org.apache.http.protocol.HttpContext&quot;))))
.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&lt;?&gt; callable
) {
long start = System.currentTimeMillis();
try {
try {
return callable.call();
} catch (Exception e) {
e.printStackTrace();
}
} finally {
System.out.println(
&quot;Took &quot; + (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(&quot;http://www.google.com&quot;);
HttpResponse response = client
.execute(httpUriRequest, new ResponseHandler&lt;HttpResponse&gt;() {
public HttpResponse handleResponse(final HttpResponse response)
throws ClientProtocolException, IOException {
return response;
}
});
}
}

Console output:

Took 512
Took 512
Took 512

Maven dependency:

&lt;dependency&gt;
&lt;groupId&gt;net.bytebuddy&lt;/groupId&gt;
&lt;artifactId&gt;byte-buddy&lt;/artifactId&gt;
&lt;version&gt;1.10.10&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.apache.httpcomponents&lt;/groupId&gt;
&lt;artifactId&gt;httpasyncclient&lt;/artifactId&gt;
&lt;version&gt;4.1.4&lt;/version&gt;
&lt;scope&gt;provided&lt;/scope&gt;
&lt;/dependency&gt;

This is the implementation for implementsInterface

This is how I am executing the application:

java -javaagent:&quot;javaagent.jar&quot; -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

得分: 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=&quot;This is not a P3P policy! See g.co/p3phelp for more info.&quot;, 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:

Byte-buddy的转换在单次执行中运行多次。

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&lt;MethodDescription&gt; executeMethodDecription = isMethod()
  .and(named(&quot;execute&quot;))
  .and(not(isAbstract()))
  .and(takesArguments(3))
  .and(takesArgument(0, named(&quot;org.apache.http.client.methods.HttpUriRequest&quot;)))
  .and(takesArgument(1, named(&quot;org.apache.http.client.ResponseHandler&quot;)))
  .and(takesArgument(2, named(&quot;org.apache.http.protocol.HttpContext&quot;)));

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) -&gt; 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=&quot;This is not a P3P policy! See g.co/p3phelp for more info.&quot;, 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;
<details>
<summary>英文:</summary>
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
```java
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=&quot;This is not a P3P policy! See g.co/p3phelp for more info.&quot;, 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:

Byte-buddy的转换在单次执行中运行多次。

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&lt;MethodDescription&gt; executeMethodDecription = isMethod()
  .and(named(&quot;execute&quot;))
  .and(not(isAbstract()))
  .and(takesArguments(3))
  .and(takesArgument(0, named(&quot;org.apache.http.client.methods.HttpUriRequest&quot;)))
  .and(takesArgument(1, named(&quot;org.apache.http.client.ResponseHandler&quot;)))
  .and(takesArgument(2, named(&quot;org.apache.http.protocol.HttpContext&quot;)));

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) -&gt; 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=&quot;This is not a P3P policy! See g.co/p3phelp for more info.&quot;, 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.

huangapple
  • 本文由 发表于 2020年10月26日 10:46:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/64530922.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定