简体   繁体   English

ByteBuddy 代理用另一个方法参数替换一个方法参数

[英]ByteBuddy agent to replace one method param with another

I have a large 3rd party code base I can't modify, but I need to make a small but important change in many different places.我有一个无法修改的大型第 3 方代码库,但我需要在许多不同的地方进行小而重要的更改。 I was hoping to use a ByteBuddy based agent, but I can't figure out how.我希望使用基于 ByteBuddy 的代理,但我不知道如何使用。 The call I need to replace is of the form:我需要替换的调用形式为:

SomeSystemClass.someMethod("foo")

and I need to replace it with我需要用

SomeSystemClass.someMethod("bar")

while leaving all other calls to the same method untouched同时保持对同一方法的所有其他调用不变

SomeSystemClass.someMethod("ignore me")

Since SomeSystemClass is a JDK class, I do not want to advise it, but only the classes that contain calls to it.由于SomeSystemClass是一个 JDK class,我不想建议它,但只建议包含对它的调用的类。 How can this be done?如何才能做到这一点?

Note that:注意:

  1. someMethod is static and someMethod是 static 和
  2. the calls (at least some of them) are inside a static initializer block调用(至少其中一些)在 static 初始化程序块内

There are two approaches to this with Byte Buddy: Byte Buddy 有两种方法:

  1. You transform all classes with the call site in question:您使用相关调用站点转换所有类:

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

    In this case, I suggest you to implement a class MyAlternativeDispatcher to your class path (it can also be shipped as part of the agent unless you have a more complex class loader setup such as OSGi where you implement the conditional logic:在这种情况下,我建议您将 class MyAlternativeDispatcher实现到您的 class 路径(它也可以作为代理的一部分运送,除非您有更复杂的 class 安装程序,例如 OSGi

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

    Doing so, you can set break points and implement any complex logic without thinking too much of byte code after setting up the agent.这样做,您可以设置断点并实现任何复杂的逻辑,而无需在设置代理后过多考虑字节码。 You can, as suggested, even ship the substitution method independently of the agent.按照建议,您甚至可以独立于代理发送替换方法。

  2. Instrument the system class itself and make it caller sensitive:检测系统 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(...);

    In this case, you'd need to reflect on the caller class to make sure you only alter behavior for the classes you want to apply this change for.在这种情况下,您需要考虑调用者 class 以确保您只更改要应用此更改的类的行为。 This is not uncommon within the JDK and since Advice inlines ("copy pastes") the code of your advice class into the system class, you can use the JDK internal APIs without restriction (Java 8 and prior) if you cannot use the stack walker API (Java 9 and later):这在 JDK 中并不少见,并且由于Advice将您的建议 class 的代码内联(“复制粘贴”)到系统 class 中,因此如果您不能使用堆栈步行器,则可以不受限制地使用 JDK 内部 API(Java 8 和更早版本) 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"; } } }

Which approach should you choose?您应该选择哪种方法?

The first approach is probably more reliable but it is rather costly since you have to process all classes in a package or subpackages.第一种方法可能更可靠,但成本相当高,因为您必须处理 package 或子包中的所有类。 If there are many classes in this package you will pay quite a price for processing all these classes to check for relevant call sites and therefore delay application startup.如果这个 package 中有很多类,您将付出相当大的代价来处理所有这些类以检查相关呼叫站点,从而延迟应用程序启动。 Once all classes are loaded, you have however paid the price and everything is in place without having altered a system class.一旦加载了所有类,您就已经付出了代价,并且一切都准备就绪,而无需更改系统 class。 You do however need to take care of class loaders to make sure that your substitution method is visible to everybody.但是,您确实需要注意 class 加载程序,以确保每个人都可以看到您的替换方法。 In the simplest case, you can use the Instrumentation API to append a jar with this class to the boot loader what makes it globally visible.在最简单的情况下,您可以使用Instrumentation API 到 append 和 jar 与此 ZA2F2ED4F8EBC2CBB4C21A 全局可见。

With the second approach, you only need to (re-)transform a single method.使用第二种方法,您只需要(重新)转换一个方法。 This is very cheap to do but you will add a (minimal) overhead to every call to the method.这样做非常便宜,但是您将为每次调用该方法添加(最小)开销。 Therefore, if this method is invoked a lot on a critical execution path, you'd pay a price on every invocation if the JIT does not discover an optimization pattern to avoid it.因此,如果在关键执行路径上多次调用此方法,如果 JIT 没有发现避免它的优化模式,那么每次调用都会付出代价。 I'd prefer this approach for most cases, I think, a single transformation is often more reliable and performant.在大多数情况下,我更喜欢这种方法,我认为,单个转换通常更可靠且性能更高。

As a third option, you could also use MemberSubstitution and add your own byte code as a replacement (Byte Buddy exposes ASM in the replaceWith step where you can define custom byte code instead of delegating).作为第三种选择,您还可以使用MemberSubstitution并添加您自己的字节码作为替换(Byte Buddy 在replaceWith步骤中公开 ASM,您可以在其中定义自定义字节码而不是委托)。 This way, you could avoid the requirement of adding a replacement method and just add the substitution code in-place.这样,您可以避免添加替换方法的要求,只需在原地添加替换代码。 This does however bear the serious requirement that you:但是,这确实承担了您的严格要求:

  • do not add conditional statements不要添加条件语句
  • recompute the stack map frames of the class重新计算 class 的堆栈 map 帧

The latter is required if you add conditional statements and Byte Buddy (or anybody) cannot optimize it in-method.如果您添加条件语句并且 Byte Buddy(或任何人)无法在方法中优化它,则需要后者。 Stack map frame recomputation is very expensive, fails comparable often and can require class loading locks to dead lock.堆栈 map 帧重新计算非常昂贵,经常失败,并且可能需要 class 加载锁到死锁。 Byte Buddy optimizes ASM's default recomputation, trying to avoid dead locks by avoiding class loading but there is no guarantee either, so you should keep this in mind. Byte Buddy 优化了 ASM 的默认重新计算,试图通过避免 class 加载来避免死锁,但也不能保证,所以你应该记住这一点。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM