Search code examples
byte-buddy

Unable to add mutator for an existing field of a class


I'm trying to add a mutator for an existing private final field. I can transform the field modifiers to remove the final specification and add an accessor method:

// accessor interface
public interface UniqueIdAccessor {
    Serializable getUniqueId();
}

// mutator interface
public interface UniqueIdMutator {
    void setUniqueId(Serializable uniqueId);
}


...

// fragment of Java agent implementation
return new AgentBuilder.Default()
        .type(hasSuperType(named("org.junit.runner.Description")))
        .transform(new Transformer() {
            @Override
            public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription,
                    ClassLoader classLoader, JavaModule module) {
                return builder.field(named("fUniqueId")).transform(ForField.withModifiers(FieldManifestation.PLAIN))
                              .implement(UniqueIdAccessor.class).intercept(FieldAccessor.ofField("fUniqueId"))
                //            .implement(UniqueIdMutator.class).intercept(FieldAccessor.ofField("fUniqueId"))
                              .implement(Hooked.class);
            }
        })
        .installOn(instrumentation);

...

Here's a method that uses reflection to check the modifiers of the target field and calls the accessor to get the value of the field.

private static void injectProxy(Description description) {
    try {
        Field bar = Description.class.getDeclaredField("fUniqueId");
        System.out.println("isFinal: " + ((bar.getModifiers() & Modifier.FINAL) != 0));
    } catch (NoSuchFieldException | SecurityException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    Serializable uniqueId = ((UniqueIdAccessor) description).getUniqueId();
    System.out.println("uniqueId: " + uniqueId);
}

// isFinal: false
// uniqueId: <description-unique-id>

... but if I uncomment the second "implement" expression to add the mutator, the transform blows up:

// isFinal: true
// java.lang.ClassCastException: 
//     class org.junit.runner.Description cannot be cast to class com.nordstrom.automation.junit.UniqueIdAccessor
//     (org.junit.runner.Description and com.nordstrom.automation.junit.UniqueIdAccessor
//     are in unnamed module of loader 'app')

I could set the field value with reflection, but that defeats the purpose of using Byte Buddy in the first place!


Solution

  • The problem with this approach is that the field accessor considers the input type prior to the modification. Byte Buddy prohibits this as it does not consider the mutation to be legal, not knowing about the removed modifier. As a result, the transformation fails in its entirety and you get the error you are seeing. (Register a listener to see this error.)

    To avoid this, you can implement a custom Implementation using FieldAccess (without or). You can have a look at the more convenient FieldAccessor to see how this is implemented, only that you need to drop the validity checks.