我拥有无法修改的庞大的第三方代码库,但是我需要在许多不同的地方进行小而重要的更改。我希望使用基于ByteBuddy的代理,但是我不知道如何做。我需要替换的呼叫的格式为:
SomeSystemClass.someMethod("foo")
并且我需要替换为
SomeSystemClass.someMethod("bar")
同时保持对同一方法的所有其他调用不变
SomeSystemClass.someMethod("ignore me")
由于SomeSystemClass
是JDK类,所以我不想建议它,而只建议包含对其调用的类。如何做到这一点?
注意:
someMethod
是静态的,Byte Buddy有两种解决方法:
您使用所涉及的呼叫站点转换了所有类:
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)
}
}
这样,您可以设置断点并实现任何复杂的逻辑,而无需在设置代理后考虑太多的字节码。根据建议,您甚至可以独立于代理发布替代方法。
指示系统类本身并使其对调用者敏感:
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的默认重新计算,通过避免类加载来尝试避免死锁,但也不保证,因此您应牢记这一点。