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

Posted

技术标签:

【中文标题】NonNull Lombok 构建器属性的 FindBugs 检测器【英文标题】:FindBugs detecter for NonNull Lombok builder attributes 【发布时间】:2018-12-21 19:45:10 【问题描述】:

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

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

但是,这使调用者可以选择在不设置 mandatoryField 的情况下创建对象,使用时会导致运行时失败。

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

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

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

此外,我知道为了进行编译时检查而设计类(如 step-builder 或 @AllArgsConstructor)会产生很多笨拙的代码 - 这就是为什么我有动力写一篇文章- 编译检测这些的 FindBugs 步骤。

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

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

我观察到:

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

我有以下问题:

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

【问题讨论】:

你真的需要编译时检查吗?单元测试未涵盖该代码吗? 不,不是所有的代码都是。仅仅确保检查此流程所需的高覆盖率对我来说不是一项现实的任务。此外,开发人员在测试期间还存在丢失此代码流的风险。 FindBugs 检查的重点是在那些测试之前 编译失败,也许当某人正在编写代码本身以及所有 对象时。跨度> this proposal 会帮助你吗?我想,只有几个必填字段是很常见的。 @maaartinus 是的,这正是我要找的!尽管我的问题要求使用 FindBugs 检测器,但我添加该检测器的动机是 Lombok 中缺少此功能。我也是,只是希望有人明确调用 .mandatoryField1(null) 而不是简单地忘记这个属性。我理解mentioned here 的担忧,我希望选择具有这种可读性而不是在必填字段上有编译时提示。 @JohnBupit,我认为他们可能已经在 1.18.4 github.com/rzwitserloot/lombok/issues/1634 中修复了这个问题 【参考方案1】:

这似乎是一个挑剔...

...但请记住,这些都不是: 查找错误 Bean 验证 (JSR303) Bean 验证 2.0 (JSR380)

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

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

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

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

我了解您希望尽快验证验证注释(如@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) 

            

【讨论】:

感谢您的意见。但是,当 .build() 执行时,我的代码已经因 NPE 失败(由于 @NonNull)。这个 CustomBuilder 的唯一好处似乎是添加了一个选中的 ValidationException,如果处理不当,它可能会导致编译失败。 关于@NonNull的好消息!【参考方案2】:

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

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 以便用户可以“选择加入”以生成更多空检查;请参阅 this discussion at GitHub。但是,该决定取决于 lombok 维护者。)

TLDR:理论上可以发现问题,但 FindBugs 功能不够强大。另一方面,lombok 不应添加进一步的 null 检查,因为它会破坏合法的用例。

【讨论】:

Lombok 保持原样完全没问题。我只是在寻找编译时(或后编译)警告,而不是因 NPE 而失败。此外,在您提到的用例中,mandatoryField1 真的是@NonNull 吗? 恕我直言,类变量上的@NonNull 不应该说明构建器(至少默认情况下不是),因为构建器只是表示未来对象的中间初步状态构造,而不是对象本身。因此:是的,mandatoryField1 仍然是 @NonNull。关于扩展 lombok:我认为(可选)生成进一步的空检查并结合使用参数的 builder() 方法将是一个很好的补充。 我不同意。 IMO,如果一个字段是@NonNull,那么它不能为空 - 处于中间状态或其他状态。我相信,不应该有任何这样的中间状态(......或者至少这是我想要在我的代码中确保的)。 但是永远不会有那个字段== null的实例,所以它是@NonNull。在构建器中只能是null。构建器模式的核心思想之一是它提供了对构建过程的步骤的控制。因此,这种模式的大多数用户都希望逐步构建,因此他们也希望在构建器中出现临时约束违规。因此,lombok 默认情况下不应添加进一步的限制,恕我直言。如果您想在构建器中确保约束,那么建议的 lombok 扩展将是一个解决方案。

以上是关于NonNull Lombok 构建器属性的 FindBugs 检测器的主要内容,如果未能解决你的问题,请参考以下文章

覆盖 lombok 构建器并更改值类型

STS中如何使用lombok

Lombok

lombok注解简介

lombok --- 常用注解解析

004Springboot整合lombok