Пишем код без NPE. Настройка Intellij Idea

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Чтобы соответствовать принципу подстановки Барбары Лисков (SOLID) с точки зрения заменяемости класса-родителя классом-наследником, нужны следующие проверки аргументов метода и возвращаемых значений:

  1. Если возвращаемый тип метода предка является Nonnull, то переопределенный метод наследника тоже должен быть Nonnull. Остальное допустимо.

  2. Если аргумент метода предка является Nullable, то переопределенный метод наследника тоже должен иметь Nullable аннотацию. Остальное допустимо.

Но это не все проверки, которые выполнит за вас Idea после соответствующей настройки. Полный перечень проверок приведен в конце статьи. Из "коробки" Idea выполняет только две проверки (не те, что выше). Если вы пишете null free код, то статья для вас окажется все равно полезной по причине: 1) наличия стороннего кода; 2) легаси кода; и 3) по причине, что null может быть использован в критических участках кода.

Зависимости

Для обеспечения таких проверок каждый метод и аргумент метода должны быть обозначены аннотациями @Nullable и @Nonnull. Чтобы не утонуть в этих аннотациях можно прийти к соглашению, что аннотацию @Nonnull не нужно указывать, т.е. что она неявная.

Чтобы научить Idea определять отсутствие аннотации как @Nonnull, нужно выполнить некоторые манипуляции с кодом. Рассматривалось три подхода, которые умеет обрабатывать Idea. Вариант с аннотацией org.eclipse.jdt.annotation.NonNullByDefault не рассматриваю.

  1. Подход на основе JSR-305. Требуется создать мета-аннотацию, которая настраивается на классы одного пакета. Действие аннотации не распространяется на классы подпакетов. Не поддерживаемая технология.

  2. Подход на основе Checker Framework. Мета-аннотация не требуется. Действие применяется на пакеты класса и на все классы подпакетов.Поддерживается Lombok.

Подход с использованием JSR-305

Для реализации подхода, добавляется зависимость

<dependency>
    <groupId>com.google.code.findbugs</groupId>
    <artifactId>jsr305</artifactId>
    <version>3.0.2</version>
    <scope>provided</scope>
</dependency>

Scope зависимости можно указать "provided", возможно и вам в Runtime эти аннотации не нужны, JVM никак не импортит классы аннотаций при загрузке классов, если только аннотация не используется в Runtime через вызов Class.getAnnotations() и обработку класса аннотации. Подход с provided использует и Spring Framework, убедиться в этом можно, если открыть класс org.springframework.lang.NonNull (если не подключена транзитивная зависимость, то import javax.annotation.Nonnull будет подсвечен красным, но это не будет мешать работе приложения). Размер jar библиотеки ~20 кБ.

Далее создается класс аннотации @NonnullByDefault


import любимая.реализация.Nonnull

/**
 * This annotation can be applied to a package, class or method to indicate that the class fields,
 * method return types and parameters in that element are not null by default unless there is:
 * The method overrides a method in a superclass (in which
 * case the annotation of the corresponding parameter in the superclass applies) there is a
 * default parameter annotation applied to a more tightly nested element.
 *
 * @see <a href="https://youtrack.jetbrains.com/issue/IDEA-125281">Impl</a>
 */
@Documented
@Nonnull
@TypeQualifierDefault({
        ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR,
        ElementType.FIELD,
        ElementType.LOCAL_VARIABLE,
        ElementType.METHOD,
        ElementType.PACKAGE,
        ElementType.PARAMETER,
        ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NonnullByDefault {
}

Реализацию аннотации Nonnull можно использовать любую. Рекомендуется либо org.springframework.lang.NonNull(если проект на Spring), либо javax.annotation.Nonnull, чтобы не повышать зацепление кода.

Далее в каждом пакете, в классах которого требуется анализ NPE (скорее всего это все пакеты проекта), создается файл package-info.java со следующим содержанием

@NonnullByDefault
package ru.my.package;

Idea будет отображать сообщения вида: "Method annotated with @Nullable must not override @NonnullByDefault method"

Подход с использованием Checker Framework

Добавляется зависимость (размер jar библиотеки ~200 кБ)

<dependency>
    <groupId>org.checkerframework</groupId>
    <artifactId>checker-qual</artifactId>
    <version>3.25.0</version>
    <scope>provided</scope>
</dependency>

Далее в корневом пакете проекта создается файл package-info.java со следующим содержанием

@DefaultQualifier(Nonnull.class)
package ru.my.package;
import любимая.реализация.Nonnull

Из минусов библиотеки - это, что в сообщение об ошибке идет ссылка не на NonNull, а DefaultQualifier: "Method annotated with @Nullable must not override @DefaultQualifier method"

Реализацию Nonnull можно использовать любую. Также рекомендуется либо org.springframework.lang.NonNull, либо org.checkerframework.checker.nullness.qual.NonNull.

Настройка Idea

Считаем, что одним из двух предыдущих способов настроена неявная аннотация @Nonnull. Можно быть спокойным насчет размера class-файлов, размер не увеличивается, аннотаций @Nonnull в class-файле не будет).

Проверки в Idea настраиваются в меню Editor → Inspections → Java → Probable bugs , в группах параметров @NotNull/@Nullable problemsReturn of 'null' и Constant conditions & exceptions. Сheckbox-ы расписывать не буду, удобнее настройки вычитывать из файла, а файл сохранить в CVS для всех участников команды. Для этого в директории .idea/inspectionProfiles нужно создать два файла:

  • Inspections.xml

<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Inspections" />
    <inspection_tool class="ConstantConditions" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="SUGGEST_NULLABLE_ANNOTATIONS" value="true" />
      <option name="DONT_REPORT_TRUE_ASSERT_STATEMENTS" value="false" />
    </inspection_tool>
    <inspection_tool class="NullableProblems" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
      <option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="false" />
      <option name="REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE" value="true" />
      <option name="REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL" value="true" />
      <option name="REPORT_NOT_ANNOTATED_GETTER" value="true" />
      <option name="REPORT_NOT_ANNOTATED_SETTER_PARAMETER" value="true" />
      <option name="REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS" value="false" />
      <option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
    </inspection_tool>
    <inspection_tool class="ReturnNull" enabled="true" level="WARNING" enabled_by_default="true">
      <option name="m_reportObjectMethods" value="true" />
      <option name="m_reportArrayMethods" value="true" />
      <option name="m_reportCollectionMethods" value="true" />
      <option name="m_ignorePrivateMethods" value="false" />
    </inspection_tool>
    <inspection_tool class="VariableTypeCanBeExplicit" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="WARNING_ATTRIBUTES" />
  </profile>
</component>
  • profiles_settings.xml

<component name="InspectionProjectProfileManager">
  <settings>
    <option name="PROJECT_PROFILE" value="Inspections" />
    <version value="1.0" />
  </settings>
</component>

Если работаете с GIT, то не забудьте добавить файлы в .gitignore

.idea
!.idea/inspectionProfiles

Далее нужно в разделе Constant conditions & exceptions настроить аннотации для автодополнений, кликнув по кнопке Configure Annotations... Настройка сохраняется в .idea/misc.xml. Если файл не добавить в CVS, каждый в группе должен ее настроить так, как настроили остальные участники, чтобы аннотации проставлялись одинаково всеми.

Инспекции в Idea
Инспекции в Idea
Настройка аннотаций для автодополнения
Настройка аннотаций для автодополнения

Результат

В таблице указаны проверки (там, где далее упоминается Nonnull, по соглашению считать, что аннотация на элементе должна отсутствовать; теоретически Nonnull можно и указывать, на проверки это не повлияет).

Проверка

По умолчанию

После конфигурации

1

Если возвращаемый тип метода предка является Nonnull, то переопределенный метод наследника тоже должен иметьNonnull аннотацию. Остальное допустимо

2

Если аргумент метода предка является Nullable, то переопределенный метод наследника тоже должен быть Nullable. Остальное допустимо

3

Проверяется, что аннотации на setter и getter методах соответствуют аннотациям полей класса

4

Проверяется передача null в аргумент метода, который объявлен Nonnull

5

Проверяется наличие аннотации Nullable на аргументе метода, который принимает null откуда-либо (можно исправлять либо этот warn, либо предыдущий)

6

Проверяется наличие аннотации Nullable, если полю присвоено null

7

Проверяется, что Nonnull полю не присваивается null (можно править этот или предыдущий warn)

8

Проверяется наличие аннотации Nullable, если метод может вернуть null

9

Проверяется отсутствие аннотации Nullable, если метод всегда возвращает не null

10

Проверяется возможность получения NPE при работе с объектом, например при вызове метода на объекте, который может принимать значение null

Так выглядит инспекция в Idea без настройки.

Инспекции Idea по умолчанию
Инспекции Idea по умолчанию

Так будет выглядеть инспекция после настройки.

Инспекции Idea после конфигурации
Инспекции Idea после конфигурации
Код для воспроизведения
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.framework.qual.DefaultQualifier;

@DefaultQualifier(NonNull.class)
@SuppressWarnings({"unused", "FieldMayBeFinal", "ResultOfMethodCallIgnored", "FieldCanBeLocal"})
class Sub extends Base {
    private Integer nonNull = 1;
    @Nullable private Integer nullable;

    @Nullable
    @Override
    Integer nonNull(Integer i) { return null; } // 1, 2

    Integer getNullable() { return nullable; } // 3, 8

    void setNullable(Integer nullable) { this.nullable = nullable; }

    @Nullable
    Integer getNonNull() { return nonNull; }

    void setNonNull(@Nullable Integer nonNull) { this.nonNull = nonNull; } // 7

    void test() { nonNullArg(1); nonNullArg(null); } // 4

    void nonNullArg(Integer i) {} // 5 (works for Checker Framework only; JSR 305 requires @NonNull on arg explicitly)

    void test2() { Integer i = null; } // 6

    // (configured by "Constraint conditions & exceptions" -> "Report nullable method always return non-null value")
    @Nullable // 9? (no warn, idea bug)
    Object nonNullResult(Integer i) { return new Object(); }

    void testNpe(@Nullable Integer i) { i.longValue(); } // 10

    void noTestNpe(Integer i) { i.longValue(); } // this is nonNull by default
}

@DefaultQualifier(NonNull.class)
class Base {
    Integer nonNull(@Nullable Integer i) { return 1; }
}

Запуск инспекций из maven

SpotBugs (JSR-305) plugin

Позволяет обнаружить кейсы 4, 7, 10 в схеме с аннотациями JSR-305, в схеме с аннотациями Checker Framework обнаруживает только 10 вариант NPE (есть ряд открытых запросов на SpotBugs). Настраивается maven следующим образом

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.7.2.1</version>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
            <phase>compile</phase>
        </execution>
    </executions>
</plugin>

Checker Framework plugin

Позволяет обнаружить для все проверки, кроме 5-ой, которая покрывается 4-ой проверкой. Для Java 11+ настраивается следующим образом

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.10.1</version>
    <configuration>
        <fork>true</fork> <!-- Must fork or else JVM arguments are ignored. -->
        <showDeprecation>true</showDeprecation>
        <showWarnings>true</showWarnings>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </path>
            <path>
                <groupId>org.checkerframework</groupId>
                <artifactId>checker</artifactId>
                <version>${checkerframework.version}</version>
            </path>
        </annotationProcessorPaths>
        <annotationProcessors>
            <annotationProcessor>
                lombok.launch.AnnotationProcessorHider$AnnotationProcessor
            </annotationProcessor>
            <annotationProcessor>
                org.checkerframework.checker.nullness.NullnessChecker
            </annotationProcessor>
        </annotationProcessors>
        <compilerArgs combine.children="append">
            <!--arg>-Awarns</arg--> <!-- CI: падать при наличии проблем -->
            <arg>-Astubs=jdk.astub</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
            <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
        </compilerArgs>
    </configuration>
</plugin>

Анализатор корректно работает с неаннотированным API JDK, но есть особенность относительно работы Objects.requireNonNull(@NonNull arg), аргумент считается @NonNull. Для обхода можно использовать механизм astub. В корне CVS репозитария нужно создать файл jdk.astub

import org.checkerframework.checker.nullness.qual.Nullable;
 
package java.util;
public class Objects {
    public static <T> T requireNonNull(@Nullable T obj);
    public static <T> T requireNonNull(@Nullable T obj, String message);
}

В файл можно вносить сигнатуры методов из других пакетов, начало классов пакета определяется ключевым словом package.

Второй вариант обхода более правильный - @SuppressWarnings({"nullness", "ConstantConditions"}). Во-первых, обязывает программиста реагировать на возможный NPE, во-вторых позволяет убрать warning не только для компилятора ("nullness"), но и для инспекций Idea ("ConstantConditions").

Источник: https://habr.com/ru/post/695004/


Интересные статьи

Интересные статьи

АннотацияВ статье рассмотрен процесс создания внешней компоненты для 1С в среде Qt Creator для операционной системы Linux (ubuntu, debian, mint и им подобных). На примере компоненты для сбор...
Приветствую читателей! В рамках текущей серии статей я рассказываю о том, как настроить сервер для простых проектов. Имеется ввиду сервер для работы нескольих сайтов, с небольшой нагрузкой под наибол...
Привет, Хабр. Меня зовут Дмитрий Гусаков. Я тимлид команды QA в компании Arenadata. Наша команда занимается тестированием компонентов Arenadata Enterprise Data Platform, в том числе тестированием орке...
Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Прочитав монументальную серию статей о подключении LCD экрана к роутеру мне захотелось сделать то же самое. Однако многообразие используемого стека (openwrt, stm32, usb) в сочетании с отс...