W3docs

Мета-аннотации 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 затем читает их через рефлексию.

java— editable, runs on the server

Что можно извлечь из запуска:

  • @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» показали ловушку. На runWeekly getAnnotation(Schedule.class) вернул null — реальная аннотация в файле класса является синтезированным контейнером @Schedules, а не Schedule. Получение контейнера через getAnnotation(Schedules.class) работает. Правило: для любой аннотации с @Repeatable всегда используйте getAnnotationsByType, чтобы оба случая (одно вхождение или несколько) выглядели одинаково.

Выбор набора мета-аннотаций

Когда вы пишете новую аннотацию, определитесь с всеми пятью сразу:

  1. Кто её будет читать? → @Retention.
  2. Где она может появляться? → @Target.
  3. Должны ли пользователи видеть её в Javadoc? → @Documented или нет.
  4. Должны ли подклассы её наследовать? → @Inherited для маркеров уровня класса вроде @Auditable. Пропустить для аннотаций уровня метода.
  5. Можно ли применять её более одного раза? → @Repeatable только если вам действительно нужна кратность.

Стандартный шаблон для аннотации метода, видимой во время выполнения:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@interface MyAnnotation { /* elements */ }

Следующая глава — Пользовательские аннотации — использует именно этот шаблон для создания аннотации с нуля и её обработки через рефлексию. С аннотациями, поставляемыми в JDK, можно ознакомиться в разделе Встроенные аннотации; с API рефлексии, использованным выше, — в разделе Чтение аннотаций через рефлексию.

Практика

Практика
Вы объявляете `@Tagged` с `@Inherited` и `@Target(ElementType.TYPE)`. Интерфейс `interface Marker {}` аннотирован `@Tagged`, а класс `class Concrete implements Marker {}`. Что вернёт `Concrete.class.getAnnotation(Tagged.class)`?
Вы объявляете `@Tagged` с `@Inherited` и `@Target(ElementType.TYPE)`. Интерфейс `interface Marker {}` аннотирован `@Tagged`, а класс `class Concrete implements Marker {}`. Что вернёт `Concrete.class.getAnnotation(Tagged.class)`?
Was this page helpful?