W3docs

Введение в аннотации Java

Что такое аннотации Java, как они присоединяют метаданные к коду и где они обрабатываются.

Введение в аннотации Java

Аннотация — это маркер, который вы прикрепляете к фрагменту исходного кода Java — классу, методу, полю, параметру, использованию типа — и который добавляет метаданные. Сама аннотация ничего не делает. Это метка, которую другой фрагмент кода (компилятор, фреймворк, IDE, сборочный инструмент) читает позже и реагирует на неё. @Override на методе говорит компилятору: «Я переопределяю метод суперкласса; пожалуйста, ругайся, если это не так». @Test на методе говорит JUnit: «Это тест». @Entity на классе говорит JPA: «Отобрази его на таблицу базы данных». В каждом случае аннотация лишь несёт информацию; поведение находится в обработчике, который её читает.

Аннотации уже встречались вам на протяжении этой книги — @Override на переопределённых методах, @FunctionalInterface на интерфейсах с единственным методом, @SuppressWarnings, чтобы заставить компилятор замолчать. Эта часть книги — о системе, лежащей под ними: что такое аннотация, когда она доступна (только в исходнике, в файле класса или во время выполнения), как написать собственную и как обработчики её потребляют.

Облик аннотации

Синтаксически аннотация — это символ @, за которым следует имя аннотации, применяемый непосредственно перед тем, что она аннотирует:

@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 и добавлять поведение кэширования. Аннотация — это флаг, а не функция.
  • Аннотации — не комментарии. Комментарии исчезают во время компиляции; аннотации — полноправные языковые конструкции. Они участвуют в системе типов, могут быть обязаны присутствовать в файле класса и могут читаться во время выполнения через рефлексию.
  • Аннотации не заменяют ясный код. Длинная стопка аннотаций над классом — это плотность информации, не всегда ясность. Фреймворки, которые сильно опираются на аннотации (Spring, JPA, JAX-RS), расплачиваются за удобство кривой обучения и затратами во время выполнения.

Когда живёт аннотация

У каждой аннотации есть политика хранения (retention policy), которая решает, как долго сохраняются метаданные:

  • SOURCE — компилятор читает её, а затем отбрасывает. @Override и @SuppressWarnings работают так; в байткоде нет никаких их следов.
  • CLASS — аннотация записывается в файл .class, но не загружается JVM во время выполнения. Это значение по умолчанию. Инструменты, исследующие байткод (статические анализаторы, постобработчики), могут её прочитать.
  • RUNTIME — аннотация сохраняется до конца; рефлексия может в любой момент спросить у любого класса, метода или поля их аннотации во время выполнения. На это опираются такие фреймворки, как Spring и Jackson.

С мета-аннотацией @Retention(...), задающей эту политику, вы познакомитесь в главе Мета-аннотации. Если коротко: выбирайте хранение в зависимости от того, кому нужно читать аннотацию — компилятору, инструменту времени сборки или коду времени выполнения.

Где может стоять аннотация

У каждой аннотации также есть цель (target) — набор элементов программы, которые она может законно аннотировать. Распространённые цели:

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

Разобранный пример: исследование аннотаций на собственном классе

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

java— editable, runs on the server

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

  • Цикл на уровне класса увидел @Deprecated на Greeter, но ничего больше — у @Deprecated хранение RUNTIME. @Override и @SuppressWarningsSOURCE, поэтому компилятор удалил их до записи файла класса, и рефлексия не может их восстановить.
  • Цикл по методам напечатал только @Deprecated на oldHello. Хотя toString был объявлен с @Override, а cast — с @SuppressWarnings("unchecked"), ни одна аннотация не попала в файл класса. Информация существовала в момент работы javac — именно так выполнялась проверка переопределения — и затем была отброшена.
  • Проверка хранения разъяснила это: @Override и @SuppressWarnings несут в своём собственном объявлении @Retention(SOURCE), тогда как @Deprecated несёт @Retention(RUNTIME). Хранение — это свойство типа аннотации, а не способа её использования.
  • Чтение @Deprecated на Greeter через cls.getAnnotation(Deprecated.class) вернуло прокси, методы-элементы которого (since(), forRemoval()) вернули значения, записанные в исходнике. Это и есть интерфейс времени выполнения к метаданным аннотаций: экземпляр, элементы которого — методы доступа.
  • Вывод для выбора хранения: если единственный потребитель — javac (проверки переопределения, подавление предупреждений), используйте SOURCE. Если фреймворку нужно читать аннотацию во время работы программы (DI, ORM, привязка JSON), используйте RUNTIME. Глава о мета-аннотациях рассказывает, как объявить это для собственных типов аннотаций.

Что будет в этой части

Остальные главы этой части проведут вас через:

  • Самые распространённые встроенные аннотации в java.lang@Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface.
  • Мета-аннотации, настраивающие ваши собственные — @Retention, @Target, @Documented, @Inherited, @Repeatable.
  • Написание собственных типов аннотаций и их чтение через рефлексию.
  • API обработки аннотаций времени компиляции, которое фреймворки используют для генерации кода.

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

Practice

Практика

A teammate writes `@Cached` next to a method and expects the second call to return a cached result. What did they get wrong about annotations?