FindBugs-детектор для атрибутов Builder NonNull Lombok

У меня есть много классов с полями @NonNull с использованием Lombok-сборщиков.

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

Однако это дает вызывающей стороне возможность создать объект без установки mandatoryField, который при использовании приведет к сбою во время выполнения.

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

Я ищу способы поймать эти ошибки во время сборки.

Существуют способы, отличные от Lombok, такие как StepBuilders или даже конструктор, чтобы гарантировать, что обязательные поля всегда заданы, но меня интересуют способы достижения этого с помощью Lombok builder.

Кроме того, я понимаю, что проектирование классов (например, построитель шагов или @AllArgsConstructor) для проверки времени компиляции создаст много неуклюжего кода - вот почему я мотивирован на создание посткомпилирующего этапа FindBugs который их обнаруживает.

Теперь FindBugs не работает, когда я явно устанавливаю поле @NonNull 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 SomeObject.set* эти сбои.

У меня есть следующие вопросы:

  • Есть ли способ использовать построители Lombok таким образом, который вызывает сбои времени сборки (при запуске FindBugs или иначе), если @NonNull атрибуты @NonNull?
  • Есть ли какой-либо пользовательский детектор FindBugs, который обнаруживает эти сбои?

Ответ 1

Lombok учитывает эти @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 теоретически может найти проблему, потому что нулевая проверка присутствует в конструкторе, который позже вызывается с null значением в вашем примере. Тем не менее, FindBugs, вероятно, недостаточно мощный, чтобы сделать это (пока?), И я не знаю, какой пользовательский детектор способен на это.

Остаются вопросы, почему lombok не добавляет эти проверки методам setbuilder (что упростит FindBugs для выявления проблемы). Это связано с тем, что вполне законно работать с экземпляром builder, который по-прежнему имеет поля @NonNull заданные как null. Рассмотрим следующий прецедент:

Вы можете, например, создать новый строитель из экземпляра с помощью toBuilder(), а затем удалить одно из своих обязательных полей, вызвав mandatoryField1(null) (возможно, потому, что вы хотите избежать утечки значения экземпляра). Затем вы можете передать его другому методу, чтобы он снова заполнил обязательное поле. Таким образом, lombok не должен и не должен добавлять эти нулевые проверки к различным методам настройки сгенерированного построителя. (Разумеется, ломбок может быть расширен таким образом, чтобы пользователи могли "отказаться" от создания более нулевых проверок, см. Обсуждение в GitHub. Однако это решение зависит от поддерживающих ломбок).

TL;DR: Проблема может быть найдена теоретически, но FindBugs недостаточно эффективен. С другой стороны, ломбок не должен добавлять дополнительные нулевые проверки, поскольку он нарушает законные варианты использования.

Ответ 2

это может показаться как нит-кит...

... но имейте в виду, что ни одно из них:
  • FindBugs
  • Проверка валика (JSR303)
  • Проверка бобов 2.0 (JSR380)

происходят во время компиляции, что очень важно в этой дискуссии.

Проверка валиков происходит во время выполнения и, как таковая, требует явного вызова в коде или управляемой среде, неявно делает это (например, Spring или JavaEE), создавая и вызывая валидаторы.

FindBugs - это статический анализатор байт-кода и, следовательно, происходит после компиляции. Он использует умную эвристику, но не выполняет код и поэтому не на 100% водонепроницаем. В вашем случае он следил за проверкой недействительности только в мелком корпусе и пропустил строителя.

Также обратите внимание на то, что вручную создав конструктор и добавив необходимые аннотации @NotNull, FindBugs не @NotNull бы, если бы вы не назначили какое-либо значение, поскольку противно присваивать 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) }

    }        
}