Search code examples
javabytecodebyte-buddy

ByteBuddy - Modify load class's default value


I'm trying to change the return value of a method of an already loaded class.

From ByteBuddy's documentation (http://bytebuddy.net/#/tutorial) this seems possible using the Java agent, as long as I don't add any field/method.

My code is the following:

ByteBuddyAgent.install();

new ByteBuddy()
        .redefine(StuffImpl.class)
        .method(returns(Result.class))
        .intercept(FixedValue.value(new Result("intercepted")))
        .make()
        .load(StuffImpl.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());

But I'm getting the following exception:

java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)

The thing is, I'm not adding any method. Where does Byte Buddy add a field or method in the above code?

EDIT:

public class StuffImpl {

    public Result validate() {
        return new Result("original");
    }
}

public class Result {

    private String result;

    public Result(String result) {
        this.result = result;
    }
}

Solution

  • You define a delegation to a fixed value new Result("intercepted") that Byte Buddy needs to store somewhere. The FixedValue implementation creates a static field for you such that the generated method can read your value from it. You can work arround this in different ways by avoiding FixedValue, for example:

    1. Delegate to another class that holds the value in a field (retains reference identity).

      MethodDelegation.to(Holder.class);
      class Holder {
        static Result result = new Result("intercepted");
        static Result intercept() { return result; }
      }
      

      This is the most universal approach, you can of course return new Result("intercepted") directly from the method.

    2. Create the instance dynamically:

      MethodCall.construct(Result.class.getDeclaredConstructor(String.class))
                .with("intercepted");
      

      In this case, the "intercepted" string does not need to be stored in a field because it can be referenced in the class's constant pool (the same goes for primitive values).


    Your StuffImpl probably defines a static initializer. This initializer is factored out by Byte Buddy into a private method such that it can add additional statements to it.

    You can disable this behaviour by setting:

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

    This should really be in the docs and I will add it for the next release.