Детектор FindBugs для атрибутов построителя 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.

Кроме того, я понимаю, что разработка классов (таких как построитель шагов или @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 к своим внутренним полям, а также не добавляет никаких проверок на null к ненулевым полям.
  • Он не вызывает никаких SomeObject.set* методов, чтобы FindBugs мог отловить эти сбои.

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

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

person John Bupit    schedule 13.07.2018    source источник
comment
Вам действительно нужны проверки времени компиляции? Этот код не охватывается модульными тестами?   -  person Leonard Brünings    schedule 13.07.2018
comment
Нет, не весь код. Простое обеспечение высокого охвата, необходимого для проверки этого потока, для меня не является реальной задачей. Плюс есть риск, что разработчики пропустят этот поток кода во время своих тестов. Смысл этой проверки FindBugs состоит в том, чтобы не выполнить компиляцию до этих тестов, возможно, когда кто-то находится в процессе написания самого кода и для всех объектов.   -  person John Bupit    schedule 13.07.2018
comment
Поможет ли вам это предложение? Я думаю, что наличие всего пары обязательных полей довольно распространено.   -  person maaartinus    schedule 18.07.2018
comment
@maaartinus Да, это именно то, что я ищу! Хотя мой вопрос касается детектора FindBugs, моей мотивацией для добавления этого детектора было отсутствие этой функции в Lombok. Я тоже просто хочу, чтобы кто-то явно назвал .mandatoryField1(null), а не просто забыл этот атрибут. Я понимаю проблемы, упомянутые здесь, и я хотел бы, чтобы вариант имел эту удобочитаемость, а не имел подсказки во время компиляции для обязательных поля.   -  person John Bupit    schedule 23.07.2018
comment
@JohnBupit, я думаю, они могли бы исправить это в 1.18.4 вопросы/1634   -  person Adwait Kumar    schedule 04.12.2018


Ответы (2)


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, которая в вашем примере вызывается позже со значением null. Однако FindBugs, вероятно, недостаточно мощен для этого (пока?), и я не знаю ни одного специализированного детектора, способного на это.

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

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

TLDR: Теоретически проблему можно найти, но FindBugs недостаточно мощен. С другой стороны, lombok не должен добавлять дополнительные проверки на null, потому что это нарушит законные варианты использования.

person Jan Rieke    schedule 17.07.2018
comment
Для Ломбока совершенно нормально оставаться как есть. Я просто ищу предупреждение во время компиляции (или после компиляции, если на то пошло), вместо того, чтобы сбой с NPE. Более того, в случае использования, который вы упомянули, mandatoryField1 действительно ли @NonNull тогда? - person John Bupit; 23.07.2018
comment
ИМХО, @NonNull в переменной класса не должно что-то говорить о построителе (по крайней мере, по умолчанию), потому что построитель просто представляет промежуточное, предварительное состояние будущего объекта, который будет построен, а не сам объект. Следовательно: Да, mandatoryField1 все еще @NonNull. Что касается расширения ломбока: я думаю (необязательно) создание дополнительных нулевых проверок в сочетании с методом builder(), который принимает параметры, было бы хорошим дополнением. - person Jan Rieke; 23.07.2018
comment
Я бы не согласился. ИМО, если поле @NonNull, то оно не должно быть нулевым - в промежуточном состоянии или иным образом. Я считаю, что такого промежуточного состояния быть не должно (... или, по крайней мере, это то, что я хотел бы обеспечить в своем коде). - person John Bupit; 23.07.2018
comment
Но никогда не будет экземпляра с этим полем == null, так что это @NonNull. Это может быть только null в билдере. Одна из основных идей шаблона построителя заключается в том, что он обеспечивает контроль этапов процесса построения. Таким образом, большинство пользователей этого шаблона хотят пошагового построения, поэтому они также ожидают временных нарушений ограничений в построителе. Таким образом, ломбок не должен добавлять дополнительных ограничений по умолчанию, ИМХО. Если вы хотите обеспечить ограничения даже в билдере, то предложенное расширение ломбока будет решением. - person Jan Rieke; 23.07.2018

может показаться, что это придирка...

... but please keep in mind that none of these:

  • Найти ошибки
  • Проверка компонента (JSR303)
  • Проверка компонентов 2.0 (JSR380)

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

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

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

    }        
}
person diginoise    schedule 13.07.2018
comment
Спасибо за отзыв. Однако мой код уже дает сбой с NPE (из-за @NonNull), когда выполняется .build(). Единственным преимуществом этого CustomBuilder, кажется, является добавление проверенного ValidationException, который, возможно, не сможет выполнить компиляцию, если не будет правильно обработан. - person John Bupit; 23.07.2018
comment
хороший совет о @NonNull! - person diginoise; 23.07.2018