Reputation: 1859
I'm trying to get ByteBuddy to implement a step builder, given an interface for that builder. I'm stuck on 2 places.
I started with:
.method(ElementMatchers.isSetter())
.intercept(FieldAccessor.ofBeanProperty());
only I'd like to return the current builder instance so we can chain calls like:
final Object obj = ...builder().id(100).name("test").build();
so instead I created an interceptor like this, which seems like a hack and I'd like to avoid reflection where possible:
@RuntimeType
public Object intercept(@RuntimeType Object arg, @This Object source, @Origin Method method)
{
try
{
// Field name is same as method name.
final Field field = source.getClass().getDeclaredField(method.getName());
field.setAccessible(true);
field.set(source, arg);
}
catch (Throwable ex)
{
throw new Error(ex);
}
// Return current builder instance.
return source;
}
Currently I add fields to the builder class in a loop and my build method on the builder is intercepted like this:
private static final class InterBuilder
{
private final Collection<String> fields;
private final Constructor<?> constructor;
InterBuilder(final Constructor<?> constructor, final Collection<String> fields)
{
this.constructor = constructor;
this.fields = fields;
}
@RuntimeType
public Object intercept(@This Object source, @Origin Method method)
{
try
{
final Object[] args = Arrays.stream(source.getClass().getDeclaredFields())
.filter(f -> this.fields.contains(f.getName()))
.map(f -> { try {
f.setAccessible(true);
return f.get(source); }
catch (Throwable ex) { throw new Error(ex); } })
.toArray();
// Invoke a constructor passing in the private field values from the builder...
return this.constructor.newInstance(args);
}
catch (Throwable ex)
{
throw new Error(ex);
}
}
}
I saw the @FieldValue
annoation. I don't suppose there is something that will give me all fields without knowing their names up front?
The code is a proof of concept at this point. Are there better ways to do what I'm doing here?
Thank you!
Upvotes: 1
Views: 523
Reputation: 1859
Rafael gave me the information I needed to come up with a solution, so the answer credit goes to him, but I wanted to include my solution for anyone else to discover in the future.
DynamicType.Builder<?> builder = new ByteBuddy()
.subclass(Object.class)
.implement(interfaces)
.name(builderClassName);
// Find all of the setters on the builder...
// Here I'm assuming all field names match setter names like:
// MyClass x = theBuilder.name("hi").id(1000).isValid(true).build();
final List<Method> setters = ...
for (final Method setter : setters)
{
// This will define a field and a setter that will set the value and return the current instance.
builder = builder
.defineField(setter.getName(), setter.getParameterTypes()[0], Visibility.PRIVATE)
.define(setter)
.intercept(FieldAccessor.ofField(setter.getName()).setsArgumentAt(0).andThen(FixedValue.self()));
}
// Find the "build" method on the builder.
final Method buildMethod = ...
// Get a constructor that you want the builder to call and return the new instance.
final Constructor<?> constructor = ...
// Get the field names from the setters.
final List<String> fieldNames = setters.stream()
.map(Method::getName)
.collect(Collectors.toList());
// This will define a "build" method that will invoke the constructor of some object and
// pass in the fields (in order) of the builder to that constructor.
builder = builder
.define(buildMethod)
.intercept(MethodCall.construct(constructor)
.withField(fieldNames.toArray(new String[fieldNames.size()])));
Upvotes: 1
Reputation: 44032
You can compose two implementations:
FieldAccessor.ofBeanProperty().setsArgumentAt(0).andThen(FixedValue.self());
This will set the setters first (index 0) argument and then return this
.
If you wanted to set a field from a MethodDelegation
without reflection, have a look at FieldProxy
.
Upvotes: 3