16

我有很多@NonNull使用 Lombok 构建器的字段类。

@Builder
class SomeObject {
    @NonNull String mandatoryField1;
    @NonNull String mandatoryField2;
    Integer optionalField;
    ...
}

但是,这为调用者提供了在不设置 a 的情况下创建对象的选项mandatoryField,使用时会导致运行时失败。

SomeObject.builder()
          .mandatoryField1("...")
          // Not setting mandatoryField2
          .build();

我正在寻找在构建时捕获这些错误的方法。

有像 StepBuilders 甚至构造函数这样的非 Lombok 方法来确保始终设置必填字段,但我对使用 Lombok 构建器实现此目的的方法很感兴趣。

此外,我知道@AllArgsConstructor为了进行编译时检查而设计类(例如 step-builder 或这些。

@NonNull现在,当我将字段显式设置为时,FindBugs 确实会失败null

FindBugs 检测到此故障,

new SomeObject().setMandatoryField1(null);

但它没有检测到这一点:

SomeObject.builder()
          .mandatoryField1(null)
          .build();

它也没有检测到这一点:

SomeObject.builder()
          .mandatoryField1("...")
          //.mandatoryField2("...") Not setting it at all.
          .build();

这似乎正在发生,因为 Delomboked 构建器看起来像,

public static class SomeObjectBuilder {
    private String mandatoryField1;
    private String mandatoryField2;
    private Integer optionalField;

    SomeObjectBuilder() {}

    public SomeObjectBuilder mandatoryField1(final String mandatoryField1) {
        this.mandatoryField1 = mandatoryField1;
        return this;
    }

    // ... other chained setters.

    public SomeObject build() {
        return new SomeObject(mandatoryField1, mandatoryField2, optionalField);
    }
}

我观察到:

  • Lombok 不会@NonNull向其内部字段添加任何内容,也不会向非空字段添加任何空检查。
  • 它不调用任何SomeObject.set*方法,以便 FindBugs 捕获这些故障。

我有以下问题:

  • @NonNull如果设置了属性,有没有办法以导致构建时失败(在运行 FindBugs 或其他方式时)的方式使用 Lombok 构建器?
  • 是否有任何自定义 FindBugs 检测器可以检测到这些故障?
4

2 回答 2

8

Lombok@NonNull在生成@AllArgsConstructor. 这也适用于由 生成的构造函数@Builder。这是您的示例中构造函数的 delomboked 代码:

SomeObject(@NonNull final String mandatoryField1, @NonNull final String mandatoryField2, final Integer optionalField) {
    if (mandatoryField1 == null) {
        throw new java.lang.NullPointerException("mandatoryField1 is marked @NonNull but is null");
    }
    if (mandatoryField2 == null) {
        throw new java.lang.NullPointerException("mandatoryField2 is marked @NonNull but is null");
    }
    this.mandatoryField1 = mandatoryField1;
    this.mandatoryField2 = mandatoryField2;
    this.optionalField = optionalField;
}

因此,FindBugs 理论上可以找到问题,因为构造函数中存在空检查,稍后null在您的示例中使用值调用该构造函数。但是,FindBugs 可能还不够强大(还没有?),而且我不知道有任何自定义检测器能够做到这一点。

问题仍然是为什么 lombok 不将这些检查添加到构建器的 setter 方法中(这将使 FindBugs 更容易发现问题)。这是因为使用仍然将@NonNull字段设置为的构建器实例是完全合法的null。考虑以下用例:

例如,您可以使用该toBuilder()方法从一个实例创建一个新的构建器,然后通过调用删除它的一个必填字段mandatoryField1(null)(可能是因为您想避免泄漏实例值)。然后你可以将它传递给其他方法,让它重新填充必填字段。因此,lombok不会也不应该将这些 null 检查添加到生成的构建器的不同 setter 方法中。(当然,可以扩展 lombok 以便用户可以“选择加入”以生成更多空检查;请参阅GitHub 上的讨论。但是,该决定取决于 lombok 维护者。)

TLDR:理论上可以找到问题,但 FindBugs 还不够强大。另一方面,lombok 不应该添加更多的空检查,因为它会破坏合法的用例。

于 2018-07-17T15:38:27.780 回答
4

这似乎是一个挑剔的选择......

...但请记住,这些都不是:

  • 查找错误
  • Bean 验证( JSR303 )
  • Bean 验证 2.0 ( JSR380 )

发生在编译时,这在本次讨论中非常重要。

Bean Validation发生在运行时,因此需要在代码中显式调用或托管环境通过创建和调用验证器来隐式执行它(如SpringJavaEE )。

FindBugs是一个静态字节码分析器,因此发生在编译后。它使用巧妙的启发式方法,但它不执行代码,因此不是 100% 无懈可击的。在您的情况下,它仅在浅表情况下遵循可空性检查并错过了构建器。

另请注意,通过手动创建构建器并添加必要的@NotNull注释,如果您没有分配任何值,FindBugsnull将不会启动,这与分配相反。另一个差距是反射和反序列化。

我了解您希望@NotNull尽快验证以验证注释(如 )表示的合同。

有一种方法可以做到SomeClassBuilder.build()(仍然是运行时!),但它有点复杂,需要创建自定义构建器:

也许它可以被通用化以适应许多类 - 请编辑!

@Builder
class SomeObject {
  @NonNull String mandatoryField1;
  @NonNull String mandatoryField2;
  Integer optionalField;
  ...

  public static SomeObjectBuilder builder() { //class name convention by Lombok
    return new CustomBuilder();
  }

  public static class CustomBuilder extends SomeObjectBuilder {
    private static ValidationFactory vf = Validation.buildDefaultValidationFactory();
    private Validator validator = vf.getValidator();

    @Overrride
    public SomeObject build() {
      SomeObject result = super.build();
      validateObject(result);
      return result;
    }

    private void validateObject(Object object) {
      //if object is null throw new IllegalArgException or ValidationException
      Set<ConstraintVioletion<Object>> violations = validator.validate(object);

      if (violations.size() > 0) { 
        //iterate through violations and each one has getMessage(), getPropertyPath() 
        // - to build up detailed exception message listing all violations
        [...]
        throw new ValidationException(messageWithAllViolations) }

    }        
}
于 2018-07-13T16:32:54.697 回答