Введение в аннотации Java
Что такое аннотации Java, как они добавляют метаданные к коду и где они обрабатываются.
Аннотация — это маркер, который вы прикрепляете к элементу Java-исходника: классу, методу, полю, параметру, использованию типа — и который добавляет метаданные. Сама аннотация ничего не делает. Это метка, которую какой-то другой код (компилятор, фреймворк, IDE, инструмент сборки) читает позже и реагирует на неё. @Override на методе говорит компилятору: «Я переопределяю метод суперкласса; пожалуйста, сообщи, если это не так». @Test на методе говорит JUnit: «это тест». @Entity на классе говорит JPA: «отобрази это в таблицу базы данных». В каждом случае аннотация лишь несёт информацию; поведение живёт в процессоре, который её читает.
Аннотации вы уже встречали на протяжении всей этой книги — @Override на переопределённых методах, @FunctionalInterface на интерфейсах с одним методом, @SuppressWarnings для подавления предупреждений компилятора. Эта часть книги посвящена системе, которая лежит в их основе: что такое аннотация, когда она доступна (только в исходниках, в class-файле или во время выполнения), как написать свою собственную и как процессоры их используют.
Форма аннотации
Синтаксически аннотация — это символ @, за которым следует имя аннотации, применённой непосредственно перед аннотируемым элементом:
@Override
public String toString() { ... }
@Deprecated
public void oldApi() { ... }
@SuppressWarnings("unchecked")
List<String> list = (List<String>) raw;Некоторые аннотации принимают элементы (свой аналог полей). Значения элементов указываются в скобках:
@SuppressWarnings("unchecked") // one element, value "unchecked"
@SuppressWarnings({"unchecked", "rawtypes"}) // array of strings
@RequestMapping(path = "/users", method = GET) // two named elementsЕсли аннотация объявляет единственный элемент с именем value, имя можно опустить — @SuppressWarnings("unchecked") является сокращением для @SuppressWarnings(value = "unchecked").
Чем аннотации не являются
Три отрицания, которые предотвращают самые распространённые заблуждения:
- Аннотации не выполняют код. Написание
@Cachedрядом с методом ничего не кэширует. Что-то другое должно найти@Cachedи добавить поведение кэширования. Аннотация — это флаг, а не функция. - Аннотации — не комментарии. Комментарии исчезают во время компиляции; аннотации являются полноправными языковыми конструкциями. Они участвуют в системе типов, могут быть обязаны сохраняться в class-файле и могут быть прочитаны во время выполнения через рефлексию.
- Аннотации не заменяют понятный код. Длинный стек аннотаций над классом — это плотность информации, но не всегда ясность. Фреймворки, активно использующие аннотации (Spring, JPA, JAX-RS), платят за удобство крутой кривой обучения и накладными расходами во время выполнения.
Жизненный цикл аннотации
Каждая аннотация имеет политику хранения, которая определяет, как долго сохраняются метаданные:
SOURCE— компилятор читает её, а затем отбрасывает.@Overrideи@SuppressWarningsработают именно так; в байткоде нет никаких записей о них.CLASS— аннотация записывается в.class-файл, но не загружается JVM во время выполнения. Это значение по умолчанию. Инструменты, инспектирующие байткод (статические анализаторы, постпроцессоры), могут её читать.RUNTIME— аннотация сохраняется до конца; рефлексия может запросить у любого класса, метода или поля его аннотации во время выполнения. Фреймворки вроде Spring и Jackson полагаются на это.
Мета-аннотацию @Retention(...), которая устанавливает эту политику, вы увидите в главе Мета-аннотации. Коротко: выбирайте хранение исходя из того, кто должен читать аннотацию — компилятор, инструмент времени сборки или код во время выполнения.
Куда может применяться аннотация
Каждая аннотация также имеет цель — набор программных элементов, к которым она может законно применяться. Распространённые цели:
TYPE— классы, интерфейсы, перечисления.METHOD— методы.FIELD— поля.PARAMETER— параметры методов.CONSTRUCTOR— конструкторы.LOCAL_VARIABLE— объявления локальных переменных.ANNOTATION_TYPE— другие объявления аннотаций (мета-аннотации).PACKAGE— пакеты, черезpackage-info.java.TYPE_USE— любое использование типа, включая обобщённые параметры и приведения типов (Java 8+).
Если вы применяете аннотацию там, где её @Target не позволяет, компилятор откажет. Попытка применить @Override к объявлению класса является ошибкой компиляции, поскольку @Override нацелена на METHOD.
Кто читает аннотации
Данные аннотаций потребляются в трёх местах:
- Компилятор. Встроенные аннотации вроде
@Override,@SafeVarargsи@FunctionalInterfaceпроверяются самимjavac. - Процессоры аннотаций. Подключаемые инструменты времени компиляции, запускаемые во время
javac. Они могут читать аннотации в компилируемых исходниках и генерировать новые исходные файлы в ответ. Lombok, Dagger, статическая метамодель Hibernate и фреймворкMicronautработают именно так. - Рефлексия во время выполнения.
Method.getAnnotations(),Class.getAnnotation(...)и т.д. возвращают экземпляры аннотаций для любого элемента с хранениемRUNTIME. Именно так Spring решает, что внедрять, как JUnit находит ваши тесты и как Jackson отображает JSON.
Первые два не требуют никакой поддержки виртуальной машины помимо того, что предоставляет javac. Третий требует, чтобы аннотация была записана в class-файл и загружена средой выполнения.
Рабочий пример: инспекция аннотаций собственного класса
Цель этого примера — показать, что @Override, @Deprecated и @SuppressWarnings выглядят одинаково в исходниках, но ведут себя по-разному после компиляции класса. Программа объявляет класс с несколькими аннотациями, а затем спрашивает у рефлексии, что та реально видит.
Что следует вынести из запуска:
- Цикл по аннотациям уровня класса увидел
@DeprecatedнаGreeter, но ничего больше —@Deprecatedимеет хранениеRUNTIME.@Overrideи@SuppressWarningsимеют хранениеSOURCE, поэтому компилятор удалил их до записи class-файла, и рефлексия не может их восстановить. - Цикл по методам вывел
@Deprecatedтолько дляoldHello. Даже несмотря на то, чтоtoStringбыл объявлен с@Override, аcast— с@SuppressWarnings("unchecked"), ни одна из этих аннотаций не попала в class-файл. Информация существовала в момент запускаjavac— именно тогда произошла проверка переопределения — а затем была отброшена. - Проверка хранения прояснила это:
@Overrideи@SuppressWarningsнесут@Retention(SOURCE)в своих собственных объявлениях, тогда как@Deprecatedнесёт@Retention(RUNTIME). Хранение — это свойство типа аннотации, а не того, как она используется. - Чтение
@DeprecatedнаGreeterчерезcls.getAnnotation(Deprecated.class)вернуло прокси, чьи методы-элементы (since(),forRemoval()) вернули значения, записанные в исходниках. Это и есть интерфейс времени выполнения к метаданным аннотаций: экземпляр, чьи элементы являются методами-аксессорами. - Вывод для выбора хранения: если единственный потребитель — это
javac(проверки переопределения, подавление предупреждений), используйтеSOURCE. Если фреймворку нужно читать аннотацию во время работы программы (внедрение зависимостей, ORM, привязка JSON), используйтеRUNTIME. Глава про мета-аннотации рассматривает, как объявить это для ваших собственных типов аннотаций.
Что ждёт впереди в этой части
Оставшиеся главы этой части проведут вас через:
- Наиболее распространённые встроенные аннотации из
java.lang—@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface. - Мета-аннотации, которые настраивают ваши собственные —
@Retention,@Target,@Documented,@Inherited,@Repeatable. - Написание пользовательских типов аннотаций и их чтение через рефлексию.
- API обработки аннотаций времени компиляции, который фреймворки используют для генерации кода.
Дуга движется от «каких аннотаций даёт вам язык» до «какие аннотации вы можете построить поверх них».