Введение в аннотации 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.
Кто читает аннотации
Данные аннотаций потребляют три места:
- Компилятор. Встроенные аннотации вроде
@Override,@SafeVarargsи@FunctionalInterfaceпроверяются самимjavac. - Обработчики аннотаций. Подключаемые инструменты времени компиляции, выполняющиеся во время
javac. Они могут читать аннотации на компилируемых исходниках и в ответ генерировать новые файлы исходного кода. Lombok, Dagger, статическая метамодель Hibernate и фреймворкMicronautработают именно так. - Рефлексия во время выполнения.
Method.getAnnotations(),Class.getAnnotation(...)и т. д. возвращают экземпляры аннотаций для любого элемента с хранениемRUNTIME. Так Spring решает, что внедрять, так JUnit находит ваши тесты, и так Jackson отображает JSON.
Первым двум не нужна поддержка виртуальной машины сверх того, что даёт javac. Третьему нужно, чтобы аннотация была записана в файл класса и загружена средой выполнения.
Разобранный пример: исследование аннотаций на собственном классе
Цель этого примера — показать, что @Override, @Deprecated и @SuppressWarnings выглядят в исходнике одинаково, но ведут себя по-разному после компиляции класса. Программа объявляет класс с несколькими аннотациями, а затем спрашивает у рефлексии, что она действительно может увидеть.
Что вынести из запуска:
- Цикл на уровне класса увидел
@DeprecatedнаGreeter, но ничего больше — у@DeprecatedхранениеRUNTIME.@Overrideи@SuppressWarnings—SOURCE, поэтому компилятор удалил их до записи файла класса, и рефлексия не может их восстановить. - Цикл по методам напечатал только
@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?