Search code examples
javabyte-buddy

Intercept method at runtime to return class instance


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


Solution

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