ByteBuddy 代理将一种方法参数替换为另一种方法参数

ByteBuddy agent to replace one method param with another

我有一个无法修改的大型第 3 方代码库,但我需要在许多不同的地方进行微小但重要的更改。我希望使用基于 ByteBuddy 的代理,但我不知道如何使用。我需要替换的调用是以下形式:

SomeSystemClass.someMethod("foo")

我需要将其替换为

SomeSystemClass.someMethod("bar")

同时保持对同一方法的所有其他调用不变

SomeSystemClass.someMethod("ignore me")

因为 SomeSystemClass 是 JDK class,我不想建议它,但只建议包含对它的调用的 classes。如何做到这一点?

注意:

  1. someMethod 是静态的并且
  2. 调用(至少其中一些)在静态初始化块中

Byte Buddy 有两种方法:

  1. 您将所有 classes 转换为有问题的调用站点:

     new AgentBuilder.Default()
       .type(nameStartsWith("my.lib.pkg."))
       .transform((builder, type, loader, module) -> builder.visit(MemberSubstitution.relaxed()
          .method(SomeSystemClass.class.getMethod("someMethod", String.class))
          .replaceWith(MyAlternativeDispatcher.class.getMethod("substitution", String.class)
          .on(any()))
        .installOn(...);
    

    在这种情况下,我建议您在 class 路径中实现一个 class MyAlternativeDispatcher(它也可以作为代理的一部分发送,除非您有更复杂的class 加载程序设置,例如您在其中实现条件逻辑的 OSGi:

     public class MyAlternativeDispatcher {
       public static void substitution(String argument) {
         if ("foo".equals(argument)) {
           argument = "bar";
         }
         SomeSystemClass.someMethod(argument);
       }
     } 
    

    这样做,您可以设置断点并实现任何复杂的逻辑,而无需在设置代理后考虑太多字节代码。您甚至可以按照建议独立于代理发送替换方法。

  2. 检测系统 class 本身并使其对调用者敏感:

     new AgentBuilder.Default()
       .with(RedefinitionStrategy.RETRANSFORMATION)
        .disableClassFormatChanges()
       .type(is(SomeSystemClass.class))
       .transform((builder, type, loader, module) -> builder.visit(Advice.to(MyAdvice.class).on(named("someMethod").and(takesArguments(String.class)))))
        .installOn(...);
    

    在这种情况下,您需要反思调用者 class 以确保您只更改要为其应用此更改的 classes 的行为。这在 JDK 中并不少见,因为 Advice 将您的建议代码 class 内联(“复制粘贴”)到系统 class 中,您可以使用 JDK 内部 APIs 无限制(Java 8 及之前)如果你不能使用堆栈 walker API(Java 9 及更高版本):

     class MyAdvice {
       @Advice.OnMethodEnter
       static void enter(@Advice.Argument(0) String argument) {
         Class<?> caller = sun.reflect.Reflection.getCallerClass(1); // or stack walker
         if (caller.getName().startsWith("my.lib.pkg.") && "foo".equals(argument)) {
           argument = "bar";
         }
       }
     }
    

您应该选择哪种方法?

第一种方法可能更可靠,但成本相当高,因为您必须处理一个包或子包中的所有 classes。如果此包中有许多 classes,您将付出相当大的代价来处理所有这些 classes 以检查相关调用站点,从而延迟应用程序启动。一旦加载了所有 classes,您就已经付出了代价并且一切都准备就绪,而无需更改系统 class。但是,您确实需要照顾 class 加载程序,以确保每个人都可以看到您的替换方法。在最简单的情况下,您可以使用 Instrumentation API 将带有此 class 的 jar 附加到引导加载程序,使其全局可见。

使用第二种方法,您只需要(重新)转换一个方法。这样做的成本非常低,但是每次调用该方法都会增加(最小的)开销。因此,如果此方法在关键执行路径上被多次调用,如果 JIT 未发现避免它的优化模式,您将为每次调用付出代价。在大多数情况下,我更喜欢这种方法,我认为,单个转换通常更可靠、性能更好。

作为第三个选项,您还可以使用 MemberSubstitution 并添加您自己的字节码作为替换(Byte Buddy 在 replaceWith 步骤中公开 ASM,您可以在其中定义自定义字节码而不是委派)。这样,您就可以避免添加替换方法的要求,而只需就地添加替换代码。但是,这确实需要您严格要求:

  • 不添加条件语句
  • 重新计算 class
  • 的堆栈映射帧

如果您添加条件语句并且 Byte Buddy(或任何人)无法在方法内优化它,则后者是必需的。堆栈映射框架重新计算非常昂贵,经常失败,并且可能需要 class 加载锁才能死锁。 Byte Buddy 优化了 ASM 的默认重计算,试图通过避免 class 加载来避免死锁,但也不能保证,所以你应该记住这一点。