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。如何做到这一点?
注意:
someMethod
是静态的并且
- 调用(至少其中一些)在静态初始化块中
Byte Buddy 有两种方法:
您将所有 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);
}
}
这样做,您可以设置断点并实现任何复杂的逻辑,而无需在设置代理后考虑太多字节代码。您甚至可以按照建议独立于代理发送替换方法。
检测系统 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 加载来避免死锁,但也不能保证,所以你应该记住这一点。
我有一个无法修改的大型第 3 方代码库,但我需要在许多不同的地方进行微小但重要的更改。我希望使用基于 ByteBuddy 的代理,但我不知道如何使用。我需要替换的调用是以下形式:
SomeSystemClass.someMethod("foo")
我需要将其替换为
SomeSystemClass.someMethod("bar")
同时保持对同一方法的所有其他调用不变
SomeSystemClass.someMethod("ignore me")
因为 SomeSystemClass
是 JDK class,我不想建议它,但只建议包含对它的调用的 classes。如何做到这一点?
注意:
someMethod
是静态的并且- 调用(至少其中一些)在静态初始化块中
Byte Buddy 有两种方法:
您将所有 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); } }
这样做,您可以设置断点并实现任何复杂的逻辑,而无需在设置代理后考虑太多字节代码。您甚至可以按照建议独立于代理发送替换方法。
检测系统 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 加载来避免死锁,但也不能保证,所以你应该记住这一点。