Как стать автором
Обновить
71.52

Null-safety в Spring приложении с JSpecify и NullAway

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров729
Автор оригинала: Sébastien Deleuze

Новый перевод от команды Spring АйО расскажет о проблемах, вызываемых неожиданным появлением NullPointerException в продакшен, о способах борьбы с этой проблемой, над которыми работает команда Java и о скором выходе новых решений, которые помогут разработчикам навсегда избавиться от этого кошмара.


Первоначальное добавление поддержки null-safety в Spring случилось еще в далеком 2017-м при выпуске Spring Framework 5.0. В 2025 у этой истории произошел новый виток эволюции, чтобы дать разработчикам на Spring больше ценных возможностей, как в Java, так и в Kotlin. Но прежде чем мы посмотрим глубже на те изменения, над которыми сейчас идет работа, позвольте объяснить вам, зачем это делается и каких преимуществ от этих изменений следует ожидать. 

Какую проблему мы пытаемся решить?

Давайте возьмем конкретный пример и предположим, что мы используем библиотеку, предоставляющую интерфейс TokenExtractor, определяемый следующим образом:

interface TokenExtractor {
    
    /**
     * Extract a token from a {@link String}.
     * @param input the input to process
     * @return the extracted token
    */
    String extractToken(String input);
}

Если по какой-то причине реализация возвращает null, доступ к null-ссылке в  token.length() (как в примере ниже) вызывает NullPointerException, который обычно случается в рантайме, вызывая HTTP ответ с кодом статуса 500 Internal Server Error.

package com.example;

String token = extractor.extractToken("...");
System.out.println("The token has a length of " + token.length());

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

Ошибки такого рода настолько часты, что сам изобретатель null-ссылок, Тони Хоар, извинился за то, что создал их и назвал их “моей ошибкой на миллиард долларов”. Но, как прекрасно демонстрирует Kotlin, корень проблемы лежит не в самих null-ссылках, а в том, что они не заданы в явном виде в системе типов.

В Java при использовании непримитивных типов nullness не определяется. Параметр может принимать или не принимать null-аргумент. Возвращаемое значение может быть nullable или non-null. Вы ничего об этом не знаете и вынуждены полагаться на чтение Javadoc или анализ реализации, чтобы это выяснить. Но даже если автор библиотеки напишет к ней документацию, обычно единообразие для всех API отсутствует, автоматизированных проверок нет, и вы не можете на самом деле знать, является ли параметр или возвращаемое значение на самом деле non-null или автор просто забыл обозначить его в документации как nullable. Такой подход по определению приводит к ошибкам, и у вас нет нормального способа исправить эту ситуацию.

Комментарий от редакции Spring АйО

Под термином nullness понимается возможность принимать значение null.

JSpecify и NullAway

Решение этой коварной проблемы состоит в том, чтобы сделать nullness используемых типов явной во всех API и создавать соответствующие автоматические проверки на единообразие в нашей IDE и в наших сборках. Поскольку Java пока не предоставляет null-restricted и nullable типы, нам нужен способ задавать nullness любой Spring API.

В 2017 мы решили ввести  Spring аннотации для nullability, которые были надстроены над семантикой и аннотациями JSR 305 (находящийся в спячке, но довольно распространенный JSR). Это решение было далеко от идеала в силу технических ограничений, неясного статуса, отсутствия нормальной спецификации, но с прагматической точки зрения это был самый лучший выбор, доступный нам на тот момент. В дальнейшем команда Spring присоединилась к рабочей группе, возглавляемой Google и включающей в себя несколько компаний, вовлеченных в экосистему JVM, таких как JetBrains, Oracle, Uber, VMware/Broadcom и другие, чтобы разработать и внести свой вклад в создание лучшего решения, не привязанного к определенному инструменты верификации. Так начинался JSpecify.

Недопонимание, относящееся к nullness, с которым я часто сталкиваюсь, состоит в том, что сначала люди думают, что это понятие касается в основном выбора одного варианта из нескольких вариантов @Nullable, но это лишь верхушка айсберга. Эти аннотации должны предоставляться вместе с правильными определениями, поддержкой инструментов, и т.д. Коллективно согласиться на общую спецификацию nullness — это и есть причина того, почему JSpecify потребовалось несколько лет, чтобы достичь 1.0.

JSpecify — это набор аннотаций, спецификаций и документации, разработанной для обеспечения null-безопасности Java приложений и библиотек в IDE или во время компиляции, благодаря таким инструментам, как NullAway.

Главное — понять, что по умолчанию nullness используемых типов в Java не задается, и что случаи использования non-null типов встречаются гораздо чаще, чем случаи использования nullable. Чтобы кодовая база оставалось читаемой, мы обычно задаем по умолчанию, что в определенной области видимости типы используются как non-null, если они не помечены как  nullable. Именно такова цель @NullMarked, что обычно устанавливается на уровне пакета через файл package-info.java, например:

@NullMarked
package org.example;

import org.jspecify.annotations.NullMarked;

Эта аннотация меняет nullness по умолчанию для используемых типов с "unspecified" (настройка по умолчанию в Java) на "non-null" (настройка по умолчанию для JSpecify @NullMarked). Теперь мы можем сделать наши API и документацию лучше.

package org.example;

interface TokenExtractor {
    
    /**
     * Extract a token from a {@link String}.
     * @param input the input to process
     * @return the extracted token or {@code null} if not found
    */
    @Nullable String extractToken(String input);
}

Теперь IDE должным образом предупреждает нас о потенциальном NullPointerException при вызове метода на возвращаемом значении, она также пожаловалась бы, если бы мы передавали аргумент null, поскольку для этого null-маркированного кода настройка по умолчанию установлена в non-null.

В то время как мы могли бы проигнорировать или пропустить эти предупреждения от IDE, единообразие этих nullness аннотаций по всей кодовой базе может быть проверена во время сборки, если сконфигурировать NullAway таким образом, чтобы он выбрасывал ошибки. Если выявлено несоответствие, сборка обрывается, естественным образом предотвращая создание небезопасных по null API (за исключением неаннотированных типов, привнесенных зависимостями от третьих сторон).

> Task :compileJava FAILED
/Users/sdeleuze/workspace/jspecify-nullway-demo/src/main/java/org/example/Main.java:7: error: [NullAway] dereferenced expression token is @Nullable
                System.out.println("The token has a length of " + token.length());
                                                                       ^
    (see http://t.uber.com/nullaway )
1 error

См. https://github.com/sdeleuze/jspecify-nullway-demo, если хотите попробовать ее самостоятельно или посмотреть на пример соответствующей Gradle сборки.

Эти ошибки от nullness заставляют разработчика разбираться с null-ссылками самостоятельно, если он использует эти API:

String token = extractor.extractToken("...");
if (token == null) {
    System.out.println("No token found");	
}
else {
    System.out.println("The token has a length of " + token.length());
}

Вы можете возразить, что Optional<T> от Java был создан для того, чтобы отображать присутствие или отсутствие значения. Но на практике Optional<T> не годится для большинства use case-ов, поскольку приводит к большому перерасходу ресурсов в рантайме (по крайней мере пока не появятся value-классы от проекта Valhalla), он увеличивает сложность кода и API, плохо подходит для параметров и ломает сигнатуры существующих API.

Новый уровень null-safety в грядущем мажорном релизе Spring

В Spring Framework 7 (сейчас находится на стадии milestone) вся кодовая база уже переведена на JSpecify. Вы можете найти соответствующую документацию здесь. Ключевое улучшение, по сравнению с предыдущей инкарнацией состоит в том, что nullness теперь задается также для массивов и vararg элементов, а также для дженерик типов. Это очень хорошо для разработчиков на Java, но также и для разработчиков на Kotlin, которые увидят идиоматические, null-безопасные API, как если бы Spring был написан на Kotlin.

Но самое большое улучшение состоит в том, что вся команда Spring работает над тем, чтобы в первом приближении предоставить null-безопасные API по всему портфолио Spring с соответствующими проверками во время сборки, чтобы гарантировать единообразие. Эта работа находится в процессе, и пока мы не можем дать никаких обещаний, что мы сможем закончить это к выходу Spring Boot 4.0 в ноябре, но мы попытаемся подобраться как можно ближе к полному покрытию. Project Reactor тоже будет охвачен. 

Когда Spring Boot 4 выйдет в свет и будет использоваться в ваших приложениях, особенно если вы включите эти проверки на nullness также и на уровне приложения, риск появления NullPointerException в продакшен будет очень серьезно снижен, если не устранен полностью, поскольку возможность его появления будет связана только с библиотеками от третьих сторон. Явно задавая, где могут появляться null-ссылки, правильно обрабатывая эти пути в коде и вводя автоматизированные проверки, мы превращаем “ошибку на миллиард долларов” в абстракцию, которая ничего не будет стоить, но позволить полностью выразить потенциальное отсутствие значения, значительно повышая безопасность Spring приложений.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Теги:
Хабы:
+5
Комментарии0

Публикации

Информация

Сайт
t.me
Дата регистрации
Численность
11–30 человек