Чем плохи аннотации Java
Я впервые опубликовал эту заметку на Medium в декабре 2019 года. За прошедшие три года я много работал с Java, но мое отношение к аннотациям осталось прежним: считаю, что они часто усложняют код. Medium же мне больше не подходит, поэтому решил перенести статью сюда.
Аннотации появились в Java в 2004 году и применяются на всех этапах разработки:
- Документирование: Стандартные аннотации @Deprecated и @Override предоставляют лишь базовую информацию для разработчиков.
- Статический анализ: Инструменты, такие как IntelliJ IDEA, используют аннотации для анализа кода.
- Компиляция: Lombok, Dagger и MapStruct изменяют или генерируют код на этапе компиляции, сокращая объем шаблонного кода.
- Выполнение: Фреймворки Java EE, Spring и Micronaut используют аннотации для подключения контроллеров, внедрения зависимостей и других задач.
Сама по себе аннотация ничего не делает – это просто метка. Чтобы она заработала, нужен специальный обработчик. Из-за этого бывает сложно понять, как работают аннотации: нужно знать, какие библиотеки подключены к проекту и как настроены зависимости в Maven или Gradle для аннотационного процессора.
Усложняет ситуацию и обилие схожих аннотаций, которые обрабатываются по-разному. Классический пример – аннотация @NotNull:
- javax.validation.constraints.NotNull – обрабатывается только во время выполнения с использованием валидационных интерфейсов.
- org.jetbrains.annotations.NotNull – служит маркером для статического анализа в IDE.
- lombok.NonNull – позволяет Lombok вставлять код во время компиляции для проверки аннотированного параметра метода.
- И другие.
Этот хаос затрудняет работу не только начинающих, но и опытных разработчиков. Иногда приходится использовать несколько видов @NotNull в одном приложении. Отладка ситуаций, когда IDE импортирует не ту аннотацию, становится болезненной, долгой и дорогостоящей.
Особенно сложно отследить, правильно ли обрабатывается аннотация во время выполнения. Например, для @Bean из Spring требуются тесты, учитывающие контекст исполнения – такие, которые запускают Spring Context. В противном случае придется, по старинке, полностью запускать приложение и проверять работоспособность вручную.
Аннотации времени исполнения особенно болезненны для сторонников явной строгой типизации, коих большинство в мире Java. Компилятор не сможет выявить проблемы, поскольку они не связаны с системой типов. Аннотации – инструмент, пришедший из мира неявной слабой типизации или её полного отсутствия. Это противоречит основным принципам типизации Java.
Если взглянуть на ситуацию в более широком масштабе, становится очевидно, что механизм аннотаций в Java – это уникальный пример неудачного языкового дизайна, попытка совместить несовместимые вещи в одном инструменте.
Другие языки программирования четко разделяются на те, что имеют аналоги аннотаций для времени компиляции: C и Rust с их macro функциональностью, C++ с template и constexpr. Или аналоги аннотаций времени исполнения: декораторы Python, теги Go.
Совмещение всего в одном инструменте вынуждает разработчика изучать практически другой язык программирования в рамках Java и одного из многочисленных громоздких фреймворков. Если серьезно, аннотации вообще-то являются тьюринг-полными: достаточно взглянуть на repeating annotations и AliasFor!
К счастью, сегодня у нас есть альтернативы. Простой пример – лямбды. Сравните примеры минимального контроллера на Spring MVC:
package example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/")
public String index() {
return "Hello World";
}
}
И Javalin:
package example;
import io.javalin.Javalin;
public class HelloWorld {
public static void main(String[] args) {
Javalin app = Javalin.create().start();
app.get("/", ctx -> ctx.result("Hello World"));
}
}
В Spring мы видим, как контроллер и путь для GET-запроса описываются декларативно. Проблема в том, что мы не видим, как именно происходит связывание – это скрытая информация. Нам будет сложно изменить поведение этих аннотаций без глубокого изучения того, как они работают. Нужно погружение в документацию.
В Javalin, напротив, все прозрачно и предельно ясно, как создается привязка маршрута для GET-запроса. У нас есть полный контроль над тем, как эта привязка будет работать, и мы можем управлять ей с помощью своего кода. К тому же, можно просто перейти к описанию app.get в IDE и изучить код библиотеки Javalin.
Радует, что некоторые проекты в мире Kotlin также идут по альтернативному пути, используя DSL. Koin представляет собой альтернативу Dagger и Spring, обходясь без аннотаций. В результате, он также прост в понимании и работе, как и подход Javalin.
В заключение: если внимательно разобраться в каждом случае, становится ясно, что аннотации далеко не всегда являются единственным способом достижения цели. Существуют более перспективные альтернативы, которые упростят понимание приложения как для автора кода, так и для его читателя.
И, конечно, есть области, где аннотации вполне оправданы. Это схожий с подходом Go способ тегирования для (де)сериализации данных. Важно отметить, что это точечное применение, которое не создает серьезных препятствий в работе.
В конечном счете, выбор инструментов Java всегда остается за Вами. Но, честно говоря, Java напоминает мне шведский стол, где можно легко затеряться, даже если Вы опытный разработчик. Если у Вас будет возможность, изучите альтернативы аннотациям – результаты могут Вас удивить.
Журнал изменений
16 сентября 2022 г.: перевел и перенес заметку с Medium.
28 марта 2025 г.: стилистически обновил текст.