单次执行多次字节伙伴转换 运行

Byte-buddy transform running multiple times for a single execution

我如下写了一个javaagent来捕获apacheorg.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>

ThisimplementsInterface

的实现

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

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

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

更新: 感谢@kriegaex MCVE, it can be found in this GitHub repository.

首先,你从哪里得到 implementsInterface 元素匹配器?它不是 ByteBuddy 的一部分,至少在当前版本中不是。我将其替换为 isSubTypeOf 以使代码能够编译。无论如何,如果你像这样激活日志记录

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

你会在控制台上看到类似这样的东西:

[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

看到了吗?您已经在整个 class 层次结构中安装了拦截器,总共三个 class:

因此,您要么需要限制元素匹配器,要么必须忍受多行日志。

免责声明:我不是 ByteBuddy 专家,也许 Rafael Winterhalter 稍后会写出更好的答案,也许我的解决方案不规范。

看起来好像拦截器匹配所有 classes,即使从技术上讲该方法仅在 CloseableHttpClient 中定义并且未在 AbstractHttpClientDefaultHttpClient 中覆盖。限制类型匹配的一种方法是检查目标 class 是否实际包含您希望检测的方法:

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

现在控制台日志应该变成:

[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

更新: 实际上 intercept() 的行为在其 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.

更新 2: 我的完整 MCVE is available for everyone's convenience in this GitHub repository.