简体   繁体   中英

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. 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:

  1. someMethod is static and
  2. the calls (at least some of them) are inside a static initializer block

There are two approaches to this with 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:

     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:

     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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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