使用 Byte-Buddy 拦截 java.lang.Object wait(long) 本机方法

Intercepting java.lang.Object wait(long) native method with Byte-Buddy

我正在尝试记录“Object.wait”方法调用的次数。拦截“public final native void wait(long timeout)”对 ByteBuddy 无效。我做了一些测试:

test1、test2 打印错误:java.lang.IllegalStateException:无法调用 public 的超级(或默认)方法 final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException; java.lang.IllegalStateException: 无法为 public 调用超级(或默认)方法 final native void java.lang.Object.wait(long) throws java.lang.InterruptedException

test3 无效,不打印任何内容。

test4 成功。

这是我的测试代码:

Java代理:


final public class AgentBootstrap {

     public static class TestAdvice {
        @Advice.OnMethodEnter
        public static void before(@Advice.Origin String methodIns) {
            System.out.println("Byte-Buddy enter:" + methodIns);
        }

        @Advice.OnMethodExit
        public static void after(@Advice.Origin String methodIns) {
            System.out.println("Byte-Buddy after:" + methodIns);
        }
      }
    
     public static void premain(String agentArgs, Instrumentation inst) throws Exception{
         AgentBuilder agentBuilder = new AgentBuilder.Default()
                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
                .with(AgentBuilder.TypeStrategy.Default.REBASE)
                .enableNativeMethodPrefix("$$mynative_")
                .with(AgentBuilder.Listener.StreamWriting.toSystemError().withErrorsOnly())
                .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
                .ignore(nameStartsWith("net.bytebuddy."));
        // test1(agentBuilder, inst);
        // test2(agentBuilder, inst);
        // test3(agentBuilder, inst);
        // test4(agentBuilder, inst);
        
    }
     
    /**
     * intercept method: public final native void wait(long timeout)
     * print error: java.lang.IllegalStateException: Cannot call super (or default) method for public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
     */
    private static void test1(AgentBuilder agentBuilder, Instrumentation inst) throws Exception {
        agentBuilder.disableClassFormatChanges()
        .type(is(Object.class))
        .transform(new Transformer() {
            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,JavaModule module) {
              return builder.method(named("wait").and(takesArguments(1))).intercept(Advice.to(TestAdvice.class));
            }
        })
        .installOn(inst);
        inst.retransformClasses(Object.class);
    }
    /**
     * intercept method: public final void wait(long timeout, int nanos)
     * print error: java.lang.IllegalStateException: Cannot call super (or default) method for public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
     */
    private static void test2(AgentBuilder agentBuilder, Instrumentation inst) throws Exception {
        agentBuilder.disableClassFormatChanges()
        .type(is(Object.class))
        .transform(new Transformer() {
            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,JavaModule module) {
              return builder.method(named("wait").and(takesArguments(2))).intercept(Advice.to(TestAdvice.class));
            }
        })
        .installOn(inst);
        inst.retransformClasses(Object.class);
    }
    
    /**
     * intercept method: public final native void wait(long timeout)
     * invalid, print nothing
     */
    private static void test3(AgentBuilder agentBuilder, Instrumentation inst) throws Exception {
        agentBuilder.disableClassFormatChanges()
        .type(is(Object.class))
        .transform(new Transformer() {
            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,JavaModule module) {
                return builder.visit(Advice.to(TestAdvice.class).on(named("wait").and(takesArguments(1))));
            }
        })
        .installOn(inst);
        inst.retransformClasses(Object.class);
    }
    /**
     * intercept method: public final void wait(long timeout, int nanos)
     * success
     */
    private static void test4(AgentBuilder agentBuilder, Instrumentation inst) throws Exception {
        agentBuilder.type(is(Object.class))
        .transform(new Transformer() {
            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,JavaModule module) {
                return builder.visit(Advice.to(TestAdvice.class).on(named("wait").and(takesArguments(2))));
            }
        })
        .installOn(inst);
    }
     
}

测试:

public static void main(String[] args) throws Exception {
    new Thread(() -> {
        Object obj = new Object();
        while (true){
            try {
                synchronized (obj) {
                    obj.wait(1000);
                    obj.wait(1000, 1);
                }
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

:( Test4 代码用于“public final void wait()”方法,它有效,但不适用于“public final native void wait()”方法。我想知道如何拦截“public final native void wait()”方法。

免责声明:这不是最终答案,只是讨论需要的初步版本。

其实我们说的是BB的一部分,我也不是专家,所以不知道。但我很好奇并尝试了类似

的东西
.transform((builder, typeDescription, classLoader, module) ->
  builder
    .method(named("wait").and(takesArguments(long.class)))
    .intercept(MethodDelegation.to(WaitAdvice.class))
)

结合

public class WaitAdvice {
  public static long COUNT = 0;

  public static void waitX(long millis) throws InterruptedException {
    System.out.println("Before wait -> " + millis);
    COUNT++;
  }
}

如果我将 WaitAdvice 放入单独的 JAR 并将其放在 JVM 的 bootstrap 类路径中,这将起作用。这里的缺点是我不知道如何从覆盖方法调用真正的目标方法,即 wait(long) 不会实际执行,只会被替换,这可能不是你想要的。我将不得不挖掘大量 BB 源代码、单元和集成测试(因为该教程不是很有帮助而且也已过时)、GitHub 问题和 Whosebug 问题,以便找到解决方案,但到目前为止,我没有发现任何不会立即导致 follow-up 问题的有用信息。尽管我可以回答您之前的(更简单的)问题,但我仍然是一个 BB 菜鸟。

也许 Rafael Winterhalter(BB 维护者)对您有想法。

查了一下,确实是字节好友1.10.14版本存在bug。 rebased 方法必须有修饰符 private final 而不仅仅是 private,否则 JVM 将不接受 rebased 方法。

修复已经在 master 上,如果您自己构建 Byte Buddy,您现在应该可以 运行 您的代理。下一个版本 1.10.15 将包含此修复。

但是请注意,从版本 13 开始,JVM 不再支持添加私有(最终)方法。可以使用 -XX:+AllowRedefinitionToAddDeleteMethods 重置旧行为。然而,此选项已被弃用,并且将不再存在于 JVM 的未来版本中。没有这种重命名本机方法的可能性,这种行为在 JVM 上不再可能,无论有没有 Byte Buddy。

(声明一下,我是字节好友的维护者。)