Мета-аннотации Java
Аннотации для аннотаций в Java — @Retention, @Target, @Documented, @Inherited, @Repeatable.
Мета-аннотация — это аннотация, которую помещают в объявление другой аннотации. Она настраивает поведение этой аннотации: как долго она существует, где разрешено её применять, наследуют ли её подклассы, можно ли применять её более одного раза. В пакете java.lang.annotation есть пять таких аннотаций, и каждый тип аннотации, который вы когда-либо напишете, будет использовать как минимум первые две.
Пять мета-аннотаций:
@Retention— управляет тем, сохраняется ли аннотация в файлах.classи во время выполнения.@Target— ограничивает виды программных элементов, которые аннотация может украшать.@Documented— включает аннотацию в сгенерированный Javadoc.@Inherited— позволяет подклассам наследовать аннотации уровня класса от суперкласса.@Repeatable— позволяет применять одну и ту же аннотацию к одному элементу более одного раза.
@Retention
@Retention выбирает одно из трёх значений RetentionPolicy:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE) // stripped by javac; never in bytecode
@interface SuppressEvenMoreWarnings { }
@Retention(RetentionPolicy.CLASS) // in bytecode; not loaded by the VM (the default)
@interface BytecodeOnly { }
@Retention(RetentionPolicy.RUNTIME) // in bytecode and accessible via reflection
@interface MyRuntimeMarker { }Правильный выбор зависит от потребителя:
- Компилятор является потребителем (проверка переопределения, подавление предупреждений, lint) →
SOURCE. - Инструмент обработки байткода, JIT-оптимизатор или анализатор после компиляции является потребителем →
CLASS. - Фреймворк читает аннотацию во время выполнения через рефлексию (DI, ORM, JSON-привязка) →
RUNTIME.
RUNTIME — наиболее разрешающий вариант, но и наиболее затратный: каждая выжившая аннотация добавляет байты в файл класса и небольшие накладные расходы на рефлексию при поиске.
@Target
@Target ограничивает места, где можно размещать аннотацию. Его значение — массив констант ElementType:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@interface Audited { } // only on methods or constructors
@Target(ElementType.TYPE)
@interface Entity { } // only on classes/interfaces/enums
@Target({})
@interface CannotBeApplied { } // exists only as a type — can't be used to annotate anythingЗначения ElementType, с которыми вы столкнётесь:
TYPE— класс, интерфейс, перечисление, аннотация.METHOD,CONSTRUCTOR,FIELD,PARAMETER,LOCAL_VARIABLE.ANNOTATION_TYPE— для мета-аннотаций, подобных тем, что описаны в этой главе.PACKAGE— вpackage-info.java.TYPE_USE(Java 8+) — любое использование типа, включая приведение типов ((@NonNull String) o), предложенияextends, аргументы обобщённых типов. Используется проверщиками обнуляемости, такими как Checker Framework.TYPE_PARAMETER(Java 8+) — только в объявлениях<T extends ...>.MODULE(Java 9+) — вmodule-info.java.RECORD_COMPONENT(Java 16+) — для параметров заголовка записи.
Если опустить @Target, аннотацию можно разместить почти везде — удобно для очень общих маркеров, но ограничивающе во всех остальных случаях. Почти всегда указывайте явный @Target.
@Documented
По умолчанию аннотации не отображаются в Javadoc элементов, которые они декорируют. @Documented позволяет включить аннотацию в документацию:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ApiNote {
String value();
}Если метод содержит @ApiNote("rate-limited to 5 rps"), Javadoc покажет эту аннотацию в сгенерированной документации. Без @Documented аннотация существует во время выполнения, но невидима в сгенерированной документации. Добавляйте @Documented ко всему, что пользователи вашей библиотеки должны видеть.
@Inherited
@Inherited применяется только к аннотациям, нацеленным на TYPE (классы). Она означает: если класс аннотирован, его подклассы считаются аннотированными тоже. Метод getAnnotation(...) рефлексии будет подниматься по цепочке суперклассов и найдёт её.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Auditable { }
@Auditable
class Account { }
class SavingsAccount extends Account { } // also "auditable" via inheritanceОговорки:
- Обход выполняется только по цепочке суперклассов — интерфейсы не распространяют аннотации, даже при наличии
@Inherited.class Foo implements Auditable { }не наделяетFooаннотацией@Auditableиз интерфейса. - Это влияет только на то, как рефлексия сообщает об аннотациях на классах. Методы, поля и параметры никогда не наследуют аннотации от переопределённых членов.
Большинство фреймворков теперь предпочитают явные повторяющиеся аннотации наследованию, поскольку правила проще. Используйте @Inherited только тогда, когда «всё, что наследует маркерный класс, тоже помечено» — это действительно то, что вам нужно.
@Repeatable
До Java 8 нельзя было применить одну и ту же аннотацию дважды к одному элементу. @Repeatable снимает это ограничение, но механика требует осторожности. Вы объявляете контейнерную аннотацию, которая содержит массив повторяемых значений, и указываете @Repeatable на контейнер:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules { Schedule[] value(); } // the container
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule { String cron(); } // the repeated annotation
class Reports {
@Schedule(cron = "0 9 * * MON")
@Schedule(cron = "0 9 * * FRI")
public void weekly() { /* ... */ }
}Правила:
- Элемент
valueконтейнера должен быть массивом повторяемого типа аннотации. - Контейнер и повторяемая аннотация должны иметь одинаковое сохранение и не меньшее количество целей.
- Если повторяемая аннотация помечена
@Documented, контейнер тоже должен быть помечен@Documented— аналогично для@Inherited. Компилятор отклоняет несоответствие с сообщениемcontaining annotation interface ... is not @Documented. Синхронизируйте их мета-аннотации. - Во время выполнения компилятор объединяет повторяющиеся использования в одну контейнерную аннотацию. У рефлексии есть оба метода:
getAnnotation(Schedule.class)(возвращает один элемент контейнера при наличии двух) иgetAnnotationsByType(Schedule.class)(возвращает массив напрямую). Используйте второй для аннотаций с@Repeatable.
Практический пример: все пять мета-аннотаций на реальной аннотации
В примере строится небольшая система аннотаций с нуля: @Schedule, которая имеет RUNTIME, только для методов, документирована и повторяема; маркер @Module, наследуемый подклассами. Метод main затем читает их через рефлексию.
Что можно извлечь из запуска:
@Moduleбыла объявлена на суперклассеReportingBase, но рефлексия нашла её наWeeklyReports, потому что аннотация содержит@Inherited. Поиск проходит по иерархии классов, пока не найдёт аннотацию или не исчерпает все суперклассы.- Случай с интерфейсом показал ограничение наследования:
WithInterfaceреализуетAnnotatedInterface, которая имеет@Module, ноgetAnnotation(Module.class)вернулnull.@Inheritedпроходит только поextends, но не поimplements. Это сбивает с толку многих новичков; если вам нужна видимость аннотаций через интерфейсы, придётся самостоятельно обходить дерево типов. - На
runWeeklyбыло две аннотации@Schedule.getAnnotationsByType(Schedule.class)вернул массив длиной 2 — правильный способ читать повторяющиеся аннотации. Контейнер@Schedulesневидим для пользовательского кода, если придерживатьсяgetAnnotationsByType. - Случай с одной
@ScheduleнаrunDailyбыл симметричным:getAnnotation(Schedule.class)сработал, поскольку контейнера не было, аgetAnnotationsByTypeвернул массив длиной 1. Любая форма подходит, когда вы знаете кратность. - Строки «repeated case via
getAnnotation» показали ловушку. НаrunWeeklygetAnnotation(Schedule.class)вернулnull— реальная аннотация в файле класса является синтезированным контейнером@Schedules, а неSchedule. Получение контейнера черезgetAnnotation(Schedules.class)работает. Правило: для любой аннотации с@Repeatableвсегда используйтеgetAnnotationsByType, чтобы оба случая (одно вхождение или несколько) выглядели одинаково.
Выбор набора мета-аннотаций
Когда вы пишете новую аннотацию, определитесь с всеми пятью сразу:
- Кто её будет читать? →
@Retention. - Где она может появляться? →
@Target. - Должны ли пользователи видеть её в Javadoc? →
@Documentedили нет. - Должны ли подклассы её наследовать? →
@Inheritedдля маркеров уровня класса вроде@Auditable. Пропустить для аннотаций уровня метода. - Можно ли применять её более одного раза? →
@Repeatableтолько если вам действительно нужна кратность.
Стандартный шаблон для аннотации метода, видимой во время выполнения:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@interface MyAnnotation { /* elements */ }Следующая глава — Пользовательские аннотации — использует именно этот шаблон для создания аннотации с нуля и её обработки через рефлексию. С аннотациями, поставляемыми в JDK, можно ознакомиться в разделе Встроенные аннотации; с API рефлексии, использованным выше, — в разделе Чтение аннотаций через рефлексию.