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

问题描述 投票:0回答:1

我拥有无法修改的庞大的第三方代码库,但是我需要在许多不同的地方进行小而重要的更改。我希望使用基于ByteBuddy的代理,但是我不知道如何做。我需要替换的呼叫的格式为:

SomeSystemClass.someMethod("foo")

并且我需要替换为

SomeSystemClass.someMethod("bar")

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

SomeSystemClass.someMethod("ignore me")

由于SomeSystemClass是JDK类,所以我不想建议它,而只建议包含对其调用的类。如何做到这一点?

注意:

  1. someMethod是静态的,
  2. 这些调用(至少其中一些)在静态初始化程序块内
java byte-buddy javaagents
1个回答
0
投票

Byte Buddy有两种解决方法:

  1. 您使用所涉及的呼叫站点转换了所有类:

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

    在这种情况下,我建议您将类MyAlternativeDispatcher实现到类路径(它也可以作为代理的一部分提供,除非您具有更复杂的类加载器设置,例如OSGi,您可以在其中实现条件逻辑:

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

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

  2. 指示系统类本身并使其对调用者敏感:

    new AgentBuilder.Default()
      .with(RetransformationStrategy.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(...);
    

    在这种情况下,您需要反思调用方类,以确保仅更改要对其应用此更改的类的行为。这在JDK中并不罕见,并且由于Advice将建议类的代码内联(“复制粘贴”)到系统类中,因此,如果您不能使用JDK内部API(Java 8及更低版本),则可以不受限制地使用JDK内部API。堆栈浏览器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";
        }
      }
    }
    

您应选择哪种方式?

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

使用第二种方法,您只需要(重新)转换一个方法。这样做非常便宜,但是您每次调用该方法都会增加(最小)开销。因此,如果在关键执行路径上多次调用此方法,那么如果JIT找不到优化模式来避免它,则每次调用都要付出一定的代价。我认为,在大多数情况下,我更喜欢这种方法,一次转换通常更可靠,更高效。

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

  • 不添加条件语句
  • 重新计算该类的堆栈映射框架

如果添加条件语句,并且Byte Buddy(或任何人)无法在方法上对其进行优化,则后者是必需的。堆栈映射框架的重新计算非常昂贵,经常失败,并且可能需要将类加载锁定为死锁。 Byte Buddy优化了ASM的默认重新计算,通过避免类加载来尝试避免死锁,但也不保证,因此您应牢记这一点。

© www.soinside.com 2019 - 2024. All rights reserved.