用于NonNull Lombok构建器属性的FindBugs检测器

Joh*_*pit 13 java findbugs builder lombok null-check

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

@Builder
class SomeObject {
    @NonNull String mandatoryField1;
    @NonNull String mandatoryField2;
    Integer optionalField;
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

SomeObject.builder()
          .mandatoryField1("...")
          // Not setting mandatoryField2
          .build();
Run Code Online (Sandbox Code Playgroud)

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

有一些非Lombok方式,如StepBuilders,甚至是构造函数,以确保始终设置必填字段,但我对使用Lombok构建器实现此目的的方式感兴趣.

另外,我理解@AllArgsConstructor为了进行编译时检查而设计类(比如一个步骤构建器或者一个)会产生很多笨拙的代码 - 这就是为什么我有动力构建一个后编译的FindBugs步骤来检测这些.

现在,当我明确地将@NonNull字段设置为时,FindBugs确实失败了null:

FindBugs检测到此失败,

new SomeObject().setMandatoryField1(null);
Run Code Online (Sandbox Code Playgroud)

但它没有检测到这个:

SomeObject.builder()
          .mandatoryField1(null)
          .build();
Run Code Online (Sandbox Code Playgroud)

它也没有发现这个:

SomeObject.builder()
          .mandatoryField1("...")
          //.mandatoryField2("...") Not setting it at all.
          .build();
Run Code Online (Sandbox Code Playgroud)

这似乎正在发生,因为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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我观察到:

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

我有以下问题:

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

Jan*_*eke 7

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


dig*_*ise 4

这可能看起来像是一个挑剔......

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

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

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

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

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

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

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

    }        
}
Run Code Online (Sandbox Code Playgroud)