简体   繁体   English

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

[英]FindBugs detecter for NonNull Lombok builder attributes

I have a lot of classes with @NonNull fields using Lombok builders. 我使用Lombok构建器的@NonNull字段有很多类。

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

However, this gives the caller the option to create the object without setting a mandatoryField , which when used, would lead to a runtime failure. 但是,这为调用者提供了创建对象的选项,而无需设置mandatoryField ,这在使用时会导致运行时失败。

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

I'm looking for ways to catch these errors at build-time. 我正在寻找在构建时捕获这些错误的方法。

There are non-Lombok ways like StepBuilders or even a constructor to ensure mandatory fields are always set, but I'm interested in ways to achieve this using the Lombok builder. 有一些非Lombok方式,如StepBuilders,甚至是构造函数,以确保始终设置必填字段,但我对使用Lombok构建器实现此目的的方式感兴趣。

Additionally, I understand that designing classes (like a step-builder, or an @AllArgsConstructor ) in order to have compile-time checks would create a lot of clumsy code - which is why I'm motivated to build a post-compile FindBugs step that detects these. 另外,我理解为了进行编译时检查而设计类(如step-builder或@AllArgsConstructor )会产生很多笨拙的代码 - 这就是为什么我有动力构建一个后编译的FindBugs步骤检测这些。

Now, FindBugs does fail when I explicitly set a @NonNull field to null : 现在,当我将@NonNull字段显式设置为null时,FindBugs确实失败了:

FindBugs detects this failure, FindBugs检测到此失败,

new SomeObject().setMandatoryField1(null);

but it doesn't detect this: 但它没有检测到这个:

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

Nor does it detect this: 它也没有发现这个:

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

This seems to be happening because the Delomboked builder looks something like, 这似乎正在发生,因为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);
    }
}

I observe that: 我观察到:

  • Lombok doesn't add any @NonNull to its internal fields, nor does it add any null-checks to the non-null fields. Lombok不会向其内部字段添加任何@NonNull ,也不会向非空字段添加任何空值检查。
  • It doesn't call any SomeObject.set* methods, for FindBugs to catch these failures. 它不会调用任何SomeObject.set*方法,因为FindBugs会捕获这些失败。

I have the following questions: 我有以下问题:

  • Is there any way to use Lombok builders in a way that causes build-time failures (while running FindBugs, or otherwise), if @NonNull attributes are set? 有没有办法以导致构建时失败的方式使用Lombok构建器(在运行FindBugs时,或者其他方式),如果设置了@NonNull属性?
  • Is there any custom FindBugs detector that detects these failures? 是否有任何自定义FindBugs检测器可以检测到这些故障?

Lombok takes these @NonNull annotations into account when generating @AllArgsConstructor . 生成@AllArgsConstructor时,Lombok @NonNull这些@NonNull注释考虑@AllArgsConstructor This also holds for the constructor that is generated by @Builder . 这也适用于@Builder生成的构造@Builder This is the delomboked code of the constructor in your example: 这是示例中构造函数的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;
}

Thus, FindBugs could in theory find the problem, because the null check is present in the constructor, which is called later with a null value in your example. 因此,FindBugs理论上可以找到问题,因为null检查存在于构造函数中,稍后在示例中使用null值调用。 However, FindBugs is probably not powerful enough to do so (yet?), and I don't know of any custom detector that is capable of that. 但是,FindBugs可能不够强大(但是?),而且我不知道任何能够做到这一点的自定义探测器。

The questions remains why lombok doesn't add those checks to the builder's setter methods (which would make it easier for FindBugs to spot the problem). 问题仍然是为什么lombok不会将这些检查添加到构建器的setter方法(这将使FindBugs更容易发现问题)。 This is because it is perfectly legit to work with a builder instance that still has @NonNull fields set to null . 这是因为使用仍然将@NonNull字段设置为null的构建器实例是完全合法的。 Consider the following use case: 考虑以下用例:

You may, eg, create a new builder from an instance using the toBuilder() method, and then remove one of its mandatory fields by calling mandatoryField1(null) (maybe because you want to avoid leaking an instance value). 例如,您可以使用toBuilder()方法从实例创建新构建器,然后通过调用mandatoryField1(null)删除其必需字段之一(可能是因为您希望避免泄漏实例值)。 Then you could pass it to some other method to let it re-fill the mandatory field. 然后你可以将它传递给其他方法让它重新填充必填字段。 Thus, lombok does not and should not add those null checks to the different setter methods of the generated builder. 因此,lombok 不会也不应该将这些空检查添加到生成的构建器的不同setter方法中。 (Of course, lombok could be extended such that users could "opt-in" to generating more null checks; see this discussion at GitHub . However, that decision is up to the lombok maintainers.) (当然,lombok可以扩展,以便用户可以“选择加入”生成更多的空检查;请参阅GitHub上的讨论 。但是,这个决定取决于lombok维护者。)

TLDR: The problem could be found theoretically, but FindBugs is not powerful enough. TLDR:问题可以从理论上找到,但FindBugs还不够强大。 On the other hand, lombok should not add further null checks, because it would break legitimate use cases. 另一方面,lombok不应该添加进一步的空检查,因为它会破坏合法的用例。

it might seem like a nit pick... 它看起来像是一个挑剔......

... but please keep in mind that none of these: ...但请记住,这些都不是:

  • Findbugs FindBugs的
  • Bean Validation ( JSR303 ) Bean验证JSR303
  • Bean Validation 2.0 ( JSR380 ) Bean验证2.0JSR380

happen at compile time, which very much matters in this discussion. 发生在编译时,这在本次讨论中非常重要。

Bean Validation happens at runtime and as such requires either explicit invocation in the code or the managed environment implicitly does it (like Spring or JavaEE ) by creating and invoking validators. Bean Validation在运行时发生,因此需要在代码中进行显式调用,或者托管环境通过创建和调用验证器来隐式执行它(如SpringJavaEE )。

FindBugs is a static bytecode analyser and therefore happens post-compilation. FindBugs是一个静态字节码分析器,因此在编译后发生。 It uses clever heuristics, but it does not execute the code, and therefore is not 100% watertight. 它使用巧妙的启发式方法,但它不执行代码,因此不是100%防水。 In your case it followed nullability check only in shallow case and missed the builder. 在你的情况下,它只是在浅的情况下遵循可空性检查,并错过了建设者。

Please also note that by manually creating the builder and adding necessary @NotNull annotations, FindBugs would not kick in, if you did not assign any value, as oppose to assigning null . 还请注意,通过手动创建构建器并添加必要的@NotNull注释,如果您没有分配任何值,则FindBugs不会启动,因为反对分配null Another gap is reflection and deserialisation. 另一个差距是反思和反序列化。

I understand that you would like the contract expressed in validation annotations (like @NotNull ) to be verified as soon as possible. 我知道您希望尽快验证验证注释中表达的合同(如@NotNull )。

There is a way to do it on SomeClassBuilder.build() (still runtime!), but it's a bit involved and requires creating custom builder: 有一种方法可以在SomeClassBuilder.build()运行它(仍然是运行时!),但它有点涉及并需要创建自定义构建器:

perhaps it could be made generic to accommodate many classes - somoeone please edit! 也许它可以通用,以适应许多类 - 索莫索请编辑!

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM