Reputation: 197
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));
}
Upvotes: 2
Views: 663
Reputation: 197
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());
Upvotes: 2