W3docs

Введение в аннотации 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.

Кто читает аннотации

Данные аннотаций потребляются в трёх местах:

  1. Компилятор. Встроенные аннотации вроде @Override, @SafeVarargs и @FunctionalInterface проверяются самим javac.
  2. Процессоры аннотаций. Подключаемые инструменты времени компиляции, запускаемые во время javac. Они могут читать аннотации в компилируемых исходниках и генерировать новые исходные файлы в ответ. Lombok, Dagger, статическая метамодель Hibernate и фреймворк Micronaut работают именно так.
  3. Рефлексия во время выполнения. Method.getAnnotations(), Class.getAnnotation(...) и т.д. возвращают экземпляры аннотаций для любого элемента с хранением RUNTIME. Именно так Spring решает, что внедрять, как JUnit находит ваши тесты и как Jackson отображает JSON.

Первые два не требуют никакой поддержки виртуальной машины помимо того, что предоставляет javac. Третий требует, чтобы аннотация была записана в class-файл и загружена средой выполнения.

Рабочий пример: инспекция аннотаций собственного класса

Цель этого примера — показать, что @Override, @Deprecated и @SuppressWarnings выглядят одинаково в исходниках, но ведут себя по-разному после компиляции класса. Программа объявляет класс с несколькими аннотациями, а затем спрашивает у рефлексии, что та реально видит.

java— editable, runs on the server

Что следует вынести из запуска:

  • Цикл по аннотациям уровня класса увидел @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 обработки аннотаций времени компиляции, который фреймворки используют для генерации кода.

Дуга движется от «каких аннотаций даёт вам язык» до «какие аннотации вы можете построить поверх них».

Практика

Практика
Коллега пишет `@Cached` рядом с методом и ожидает, что второй вызов вернёт кэшированный результат. В чём его ошибка в понимании аннотаций?
Коллега пишет `@Cached` рядом с методом и ожидает, что второй вызов вернёт кэшированный результат. В чём его ошибка в понимании аннотаций?
Was this page helpful?