非空洞龙目岛建造者属性的 FindBugs 检测器

2022-09-03 07:28:50

我有很多使用龙目岛建造者的字段的类。@NonNull

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

但是,这为调用方提供了在不设置 的情况下创建对象的选项,当使用时,这将导致运行时故障。mandatoryField

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

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

有一些非龙目岛的方式,如StepBuilders甚至构造函数来确保始终设置必填字段,但我对使用龙目岛构建器实现这一目标的方法感兴趣。

此外,我明白设计类(如步骤生成器或)以便进行编译时检查会产生很多笨拙的代码 - 这就是为什么我有动力构建一个后编译的FindBugs步骤来检测这些。@AllArgsConstructor

现在,当我显式将字段设置为:@NonNullnull

FindBugs 检测到此故障,

new SomeObject().setMandatoryField1(null);

但它没有检测到这个:

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

它也没有检测到这一点:

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

这似乎正在发生,因为被破坏的建造者看起来像这样,

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

我有以下问题:

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

答案 1

龙目岛在生成 时会考虑这些注释。这也适用于 生成的构造函数。这是示例中构造函数的 delomboked 代码:@NonNull@AllArgsConstructor@Builder

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可以找到问题,因为构造函数中存在空检查,稍后在示例中使用值调用该构造函数。但是,FindBugs可能还不够强大(还没有?),我不知道有任何自定义检测器能够做到这一点。null

问题仍然是为什么龙目岛不将这些检查添加到构建器的设置器方法中(这将使FindBugs更容易发现问题)。这是因为使用仍然具有设置为 的字段设置为 的生成器实例是完全合法的。请考虑以下用例:@NonNullnull

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

TLDR:这个问题可以在理论上找到,但FindBugs不够强大。另一方面,龙目岛不应该添加进一步的空检查,因为它会破坏合法的用例。


答案 2

它可能看起来像一个尼特选择...

...但请记住,这些都不是:
  • 查找虫
  • 豆类验证JSR303)
  • Bean Validation 2.0JSR380)

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

Bean 验证在运行时进行,因此需要在代码中显式调用,或者托管环境通过创建和调用验证器隐式地执行此操作(如 SpringJavaEE)。

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

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

我理解您希望尽快验证在验证注释(如 )中表达的合同。@NotNull

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

也许它可以成为通用的,以适应许多类 - somoeone请编辑!

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

    }        
}