英文:
ByteBuddy agent to replace one method param with another
问题
我有一个无法修改的大型第三方代码库,但我需要在许多不同的地方进行一个小但重要的更改。我希望使用基于ByteBuddy的代理,但我不知道该如何操作。我需要替换的调用形式如下:
SomeSystemClass.someMethod("foo")
我需要将其替换为:
SomeSystemClass.someMethod("bar")
同时保留对同一方法的所有其他调用不变
SomeSystemClass.someMethod("请忽略我")
由于`SomeSystemClass`是JDK类,我不想对它进行增强,而只想增强包含对它调用的类。如何实现这个目标?
请注意:
1. `someMethod`是静态方法,
2. 这些调用(至少其中一些)位于静态初始化块内。
英文:
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. I was hoping to use a ByteBuddy based agent, but I can't figure out how. 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. How can this be done?
Note that:
someMethod
is static and- the calls (at least some of them) are inside a static initializer block
答案1
得分: 1
以下是翻译好的内容:
使用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(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(...);
在这种情况下,你需要反射调用者类,以确保只为你想要应用此更改的类更改行为。这在JDK中并不罕见,由于
Advice
将你的advice类的代码内联(“复制粘贴”)到系统类中,所以你可以无限制地使用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); // 或者使用堆栈行走器 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的默认重新计算方式,尝试避免通过避免类加载来避免死锁,但也没有保证,所以你应该记住这一点。
英文:
There are two approaches to this with Byte Buddy:
-
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: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.
-
Instrument the system class itself and make it caller sensitive:
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. 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):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. 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. Once all classes are loaded, you have however paid the price and everything is in place without having altered a system class. You do however need to take care of class loaders to make sure that your substitution method is visible to everybody. 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.
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. 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). 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
The latter is required if you add conditional statements and Byte Buddy (or anybody) cannot optimize it in-method. Stack map frame recomputation is very expensive, fails comparable often and can require class loading locks to dead lock. 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论