使用 ByteBuddy 创建步骤生成器

Creating Step Builder with ByteBuddy

我正在尝试让 ByteBuddy 实现一个步骤构建器,并为该构建器提供一个接口。我被困在 2 个地方。

  1. 如何创建 setter 作为方法链接的当前实例的 return?

我开始于:

.method(ElementMatchers.isSetter())
.intercept(FieldAccessor.ofBeanProperty());

只有我想 return 当前构建器实例,这样我们就可以像这样进行链式调用:

final Object obj = ...builder().id(100).name("test").build();

所以我创建了一个这样的拦截器,这看起来像是一个 hack,我想尽可能避免反射:

@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;
}
  1. 有没有一种简单的方法来访问 class 上定义的字段,我正在定义而不用反射?

目前我在循环中向构建器 class 添加字段,我在构建器上的构建方法被拦截如下:

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

我看到了@FieldValue注释。我不认为有什么东西可以在不知道他们名字的情况下给我所有的领域?

此时代码是概念证明。有没有更好的方法来完成我在这里所做的事情?
谢谢!

您可以编写两个实现:

FieldAccessor.ofBeanProperty().setsArgumentAt(0).andThen(FixedValue.self());

这将首先设置设置器(索引 0)参数,然后设置 return this

如果您想在 MethodDelegation 中设置字段而不进行反射,请查看 FieldProxy

Rafael 给了我想出解决方案所需的信息,所以答案归功于他,但我想包括我的解决方案供其他人在未来发现。

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()])));