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));
}