2

I'm trying to use Byte Buddy to compile a JSON schema to JavaBeans. I've got class, field, and getter/setter generation working. I want to generate toString/equals/hashCode as well, but it seems like doing so requires getting a FieldDescription for fields on the class I'm in the process of defining, and I don't see any way to do that. Is it possible? Or am I approaching this completely the wrong way?

Essential portions of my code:

public Class<?> createClass(final String className) {
    DynamicType.Builder<Object> builder = new ByteBuddy()
            .subclass(Object.class)
            .name(className);

    // create fields and accessor methods
    for(final Map.Entry<String, Type> field : this.fields.entrySet()) {
        final String fieldName = field.getKey();

        Type fieldValue = field.getValue();
        if (fieldValue instanceof ClassDescription) {
            // recursively generate classes as needed
            fieldValue = ((ClassDescription) fieldValue).createClass(fieldName);
        }

        builder = builder
            // field
            .defineField(fieldName, fieldValue, Visibility.PRIVATE);
            // getter
            .defineMethod(getterName(fieldName), fieldValue, Visibility.PUBLIC)
            .intercept(FieldAccessor.ofBeanProperty())
            // setter
            .defineMethod(setterName(fieldName), Void.TYPE, Visibility.PUBLIC)
            .withParameter(fieldValue)
            .intercept(FieldAccessor.ofBeanProperty());
    }

    // TODO: Create toString/hashCode/equals
    // builder = builder
    //        .defineMethod("toString", String.class, Visibility.PUBLIC)
    //        .intercept(new ToStringImplementation(fieldDescriptions));

    final Class<?> type = builder
            .make()
            .load(this.getClass().getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
            .getLoaded();

    return type;
}

public Implementation makeToString(final LinkedHashMap<String, FieldDescription> fields) {
    final ArrayList<StackManipulation> ops = new ArrayList<>();

    try {
        final TypeDescription stringBuilderDesc = new TypeDescription.ForLoadedType(StringBuilder.class);
        final MethodDescription sbAppend = new MethodDescription.ForLoadedMethod(
                StringBuilder.class.getDeclaredMethod("append", Object.class));
        final MethodDescription sbToString = new MethodDescription.ForLoadedMethod(
                StringBuilder.class.getDeclaredMethod("toString"));

        // create the StringBuilder
        ops.add(MethodInvocation.invoke(
                new MethodDescription.ForLoadedConstructor(StringBuilder.class.getConstructor()))
        );
        // StringBuilder::append returns the StringBuilder, so we don't need to 
        // save the reference returned from the 'new'

        for(final Map.Entry<String, FieldDescription> field : fields.entrySet()) {
            ops.add(FieldAccess.forField(field.getValue()).read());
            ops.add(MethodInvocation.invoke(sbAppend));
        }

        // call StringBuilder::toString
        ops.add(MethodInvocation.invoke(sbToString).virtual(stringBuilderDesc));

        // return the toString value
        ops.add(MethodReturn.of(TypeDescription.STRING));
    } catch (final NoSuchMethodException | SecurityException e) {
        throw new RuntimeException(e);
    }

    return new Implementation.Simple(ops.toArray(EMPTY_STACKMANIPULATION_ARRAY));
}
mmebane
  • 197
  • 1
  • 10

1 Answers1

2

I came up with a solution based on this answer. Here's a simplified version.

This example will take a class which contains a single String field called name, and generate a toString which will result in output similar to MyGeneratedClass[name=Tom].

public static class ToStringImplementation implements Implementation {
    public static final TypeDescription SB_TYPE;
    public static final MethodDescription SB_CONSTRUCTOR_DEFAULT;
    public static final MethodDescription SB_APPEND_STRING;
    public static final MethodDescription SB_TO_STRING;

    static {
        try {
            SB_TYPE = new TypeDescription.ForLoadedType(StringBuilder.class);
            SB_CONSTRUCTOR_DEFAULT = new MethodDescription.ForLoadedConstructor(StringBuilder.class.getConstructor());
            SB_APPEND_STRING = new MethodDescription.ForLoadedMethod(StringBuilder.class.getDeclaredMethod("append", String.class));
            SB_TO_STRING = new MethodDescription.ForLoadedMethod(StringBuilder.class.getDeclaredMethod("toString"));
        }
        catch (final NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public InstrumentedType prepare(final InstrumentedType instrumentedType) {
        return instrumentedType;
    }

    @Override
    public ByteCodeAppender appender(final Target implementationTarget) {
        final TypeDescription thisType = implementationTarget.getInstrumentedType();

        return new ByteCodeAppender.Simple(Arrays.asList(
            // allocate the StringBuilder
            TypeCreation.of(SB_TYPE),
            // constructor doesn't return a reference to the object, so need to save a copy
            Duplication.of(SB_TYPE),
            // invoke the constructor
            MethodInvocation.invoke(SB_CONSTRUCTOR_DEFAULT),

            // opening portion of toString output
            new TextConstant(thisType.getName() + "["),
            MethodInvocation.invoke(SB_APPEND_STRING),

            // field label
            new TextConstant("name="),
            MethodInvocation.invoke(SB_APPEND_STRING),

            // field value
            // virtual call first param is always "this" reference
            MethodVariableAccess.loadThis(),
            // first param to append is the field value
            FieldAccess.forField(thisType.getDeclaredFields()
                    .filter(ElementMatchers.named("name"))
                    .getOnly()
            ).read(),
            // invoke append(String), since name is a String-type field
            MethodInvocation.invoke(SB_APPEND_STRING),

            // closing portion of toString output
            new TextConstant("]"),
            MethodInvocation.invoke(SB_APPEND_STRING),

            // call toString and return the result
            MethodInvocation.invoke(SB_TO_STRING),
            MethodReturn.of(TypeDescription.STRING)
        ));
    }
}

Apply it like

builder
    .method(ElementMatchers.named("toString"))
    .intercept(new ToStringImplementation());
Community
  • 1
  • 1
mmebane
  • 197
  • 1
  • 10
  • 1
    This is how you should do it. I consider to some day add a default implementation to Byte Buddy. – Rafael Winterhalter Mar 01 '17 at 21:09
  • @RafaelWinterhalter: Thanks for the feedback. One more question, though: is there a way I can resolve the best overload of `StringBuilder::append` for the type of a field instead of manually specifying it? I've been looking at `MethodDelegation`, but I can't see a way to use it in this scenario. – mmebane Mar 02 '17 at 17:32
  • You should code out the byte code in this case, with regards to primitives, this is just much more efficient. – Rafael Winterhalter Mar 02 '17 at 19:46
  • Yes, I didn't really want a delegate, I was just trying to find some way of finding the most specific method (as per [JLS 15.12.2](https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2)), using `TypeDescription`s for the parameter types, so I could emit that for my invocation. – mmebane Mar 02 '17 at 20:26