Intercept method at runtime to return class instance

Viewed 504

I'm trying to change some method code so it returns a fixed value, instead of actually executing the original code. The idea is to create a loader that, using a json that specifies some class methods, specifies the result it should return (another json serializated instance). Something like that:

{
    "overrides": [
        {
            "className": "a.b.C",
            "method": "getUser",
            "returns": "{ \"name\": \"John\" }"
        }
    }
}

The result should be that, each time C.getUser() is called, the serializated instance shoud be returned instead of actually executing the method (all this without changing the source code).

I've tried something like this:

ByteBuddyAgent.install();
final ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
new ByteBuddy().redefine(className).method(ElementMatchers.named(methodName))
    .intercept(FixedValue.nullValue()).make()
    .load(clase.getClassLoader(), classReloadingStrategy);

And it returns null instead of executing the method body but, how can I return the deserializated result? When I try to une a FixedValue.value(deserializatedInstance) it throws the following exception:

> java.lang.RuntimeException: java.lang.IllegalStateException: Error invoking java.lang.instrument.Instrumentation#retransformClasses
    at es.abanca.heracles.ServiceLauncher.addTiming2(ServiceLauncher.java:129)
    at es.abanca.heracles.ServiceLauncher.establecerMocks(ServiceLauncher.java:65)
    at es.abanca.heracles.ServiceLauncher.run(ServiceLauncher.java:40)
    at es.abanca.heracles.MifidServiceLauncher.main(MifidServiceLauncher.java:6)
Caused by: java.lang.IllegalStateException: Error invoking java.lang.instrument.Instrumentation#retransformClasses
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Dispatcher$ForJava6CapableVm.retransformClasses(ClassReloadingStrategy.java:503)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$2.apply(ClassReloadingStrategy.java:568)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:225)
    at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:100)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:6156)
    at es.abanca.heracles.ServiceLauncher.addTiming2(ServiceLauncher.java:127)
    ... 3 more
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
    at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:144)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Dispatcher$ForJava6CapableVm.retransformClasses(ClassReloadingStrategy.java:495)
    ... 8 more

Thank you all in advance

2 Answers

Your approach only works if the target class was not loaded yet.

When trying to modify (i.e. retransform) a class which was already loaded, as opposed to a class which is just being loaded (i.e. redefine), you need to make sure you

  • use retransform instead of redefine and
  • avoid manipulating the target class structure via .disableClassFormatChanges().

I am not a ByteBuddy expert, but I know you can modify methods without changing the class structure via Advice API. I usually do it like this:

import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.ClassFileLocator;
import org.acme.Sub;

import java.lang.instrument.Instrumentation;

import static net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy.RETRANSFORMATION;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.named;

class Scratch {
  public static void main(String[] args) {
    System.out.println(new Sub("original name").getName());

    Instrumentation instrumentation = ByteBuddyAgent.install();
    new AgentBuilder.Default()
      .with(RETRANSFORMATION)
      .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
      .disableClassFormatChanges()
      .type(is(Sub.class))
      .transform((builder, typeDescription, classLoader, module) ->
        builder.visit(
          Advice
            .to(MyAspect.class, ClassFileLocator.ForClassLoader.ofSystemLoader())
            .on(named("getName"))
        )
      )
      .installOn(instrumentation);

    System.out.println(new Sub("original name").getName());
  }

  static class MyAspect {
    @Advice.OnMethodEnter(skipOn = Advice.OnDefaultValue.class)
    public static boolean before() {
      // Default value for boolean is false -> skip original method execution
      return false;
    }

    @Advice.OnMethodExit(onThrowable = Throwable.class, backupArguments = false)
    public static void after(
      @Advice.Return(readOnly = false, typing = DYNAMIC) Object returnValue
    )
    {
      System.out.println("MyAspect");
      // Here you can define your return value of choice, null or whatever else 
      returnValue = "dummy name";
    }
  }

}

The console log would be something like:

original name
[Byte Buddy] TRANSFORM org.acme.Sub [sun.misc.Launcher$AppClassLoader@18b4aac2, null, loaded=true]
MyAspect
dummy name

There might be a simpler way. If so, Rafael Winterhalter definitely knows better than I.

Your problem is that you are enhancing a class in way that makes it ineligible for retransformation since you are changing the class's signature. You can force Byte Buddy into attempting to preserve the original shape by adding:

new ByteBuddy().with(Implementation.Context.Disabled.Factory.INSTANCE)

Doing so, Byte Buddy disables a few features but for a simple instrumentation such as FixedValue this has no effect.

Related