W3docs

Пользовательские аннотации в Java

Объявляйте собственные типы аннотаций в Java, настраивайте их хранение и цели, читайте значения во время выполнения через рефлексию.

Пользовательская аннотация — это тип аннотации, который вы объявляете самостоятельно, а не тот, что поставляется JDK (например, @Override) или фреймворком (например, @Test). Синтаксис напоминает интерфейс, но правила строже. После объявления ваша аннотация становится настоящим типом, который можно прикреплять к коду, находить через рефлексию и обрабатывать во время компиляции.

Эта глава — практическое руководство по написанию собственных аннотаций: ключевое слово @interface, допустимые типы элементов, различия между обязательными и необязательными элементами, а также то, как процессор считывает значения во время выполнения. Если вы ещё не знакомы с аннотациями, начните с введения в аннотации Java и встроенных аннотаций; чтобы управлять тем, где может применяться аннотация и сколько она «живёт», смотрите главу о мета-аннотациях.

Объявление через @interface

Тип аннотации объявляется с помощью ключевого слова @interface:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

Это объявляет новый тип аннотации Audited, элементы которого выглядят как методы интерфейса, но ведут себя как именованные значения в местах применения. Каждый «метод» — это элемент.

Используется так:

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

Сокращение value (@Audited("...") вместо @Audited(value = "...")) доступно только тогда, когда элемент буквально называется value — именно поэтому так много аннотаций используют именно это имя в качестве основного параметра.

Допустимые типы элементов

Тело @interface — это закрытый набор объявлений элементов. Возвращаемый тип каждого элемента должен быть одним из следующих:

  • Примитив (int, long, double, boolean, ...).
  • String.
  • Class или параметризованный Class<?>.
  • Тип перечисления.
  • Другой тип аннотации.
  • Массив любого из вышеперечисленного.

Значения по умолчанию задаются с помощью default. Значение по умолчанию должно быть константой времени компиляции нужного типа:

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

Что нельзя объявлять в аннотации:

  • Методы с параметрами (скобки () обязательны, но всегда пусты).
  • Обобщённые элементы (<T> T value(); недопустимо).
  • Предложения throws.
  • Наследование от другого интерфейса (аннотации неявно расширяют java.lang.annotation.Annotation).
  • Конструкторы.

Вы можете вкладывать типы внутрь объявления аннотации — перечисление Level выше живёт внутри @RetryPolicy. Это полезный паттерн: он ограничивает область видимости связанных параметров аннотацией, которая их использует.

Обязательные и необязательные элементы

Элемент без default является обязательным в местах применения. Компилятор выдаст ошибку, если вы его забудете:

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

Небольшой совет по стилю: если есть одно очевидное значение, назовите элемент value и сделайте его обязательным. Если параметров несколько, дайте им имена и задайте разумные значения по умолчанию, чтобы типичный вызов оставался коротким.

Маркерные аннотации

Аннотация без элементов называется маркером:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ThreadSafe { }

Маркерные аннотации не несут данных; сам факт их наличия или отсутствия является сигналом. Рефлексия проверяет «есть ли у этого класса @ThreadSafe?» с помощью getAnnotation(ThreadSafe.class) != null или isAnnotationPresent(ThreadSafe.class).

Чтение аннотаций во время выполнения

Для аннотации с политикой RUNTIME рефлексия предоставляет несколько методов на Class, Method, Field, Constructor и Parameter (полный обзор см. в главе чтение аннотаций через рефлексию):

  • isAnnotationPresent(Class) — быстрый ответ да/нет.
  • getAnnotation(Class) — возвращает экземпляр аннотации или null.
  • getAnnotations() — возвращает все аннотации элемента (объявленные + унаследованные через @Inherited).
  • getDeclaredAnnotations() — только те, что объявлены непосредственно на элементе, без учёта @Inherited.
  • getAnnotationsByType(Class) — корректно обрабатывает случай @Repeatable.

Чтение выглядит одинаково независимо от типа цели:

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

Возвращаемый объект Audited — это прокси, сгенерированный JVM; методы элементов (value(), level(), tags()) — это настоящие вызовы методов на нём.

Равенство, идентичность и toString аннотаций

Значения аннотаций реализуют equals, hashCode и toString согласно спецификации java.lang.annotation.Annotation:

  • Два экземпляра аннотации равны, если они одного типа и каждый элемент сравнивается как равный (с глубоким сравнением массивов).
  • hashCode вычисляется из значений элементов по определённым правилам.
  • toString формирует стабильное, похожее на исходный код представление — полезно для логирования.

Рефлексия иногда возвращает один и тот же прокси при повторных обращениях к одному элементу, а иногда — новый. Используйте equals, а не == при сравнении экземпляров аннотаций.

Пример: объявление, применение и рефлексия

Программа объявляет две аннотации (@Audited и @Retry), применяет их к классу и обходит методы через рефлексию — выполняя каждый метод либо внутри обёртки для аудита, либо в цикле повторных попыток. Аннотации являются чистыми метаданными; поведение реализовано в исполнителе.

java— editable, runs on the server

Что следует вынести из этого примера:

  • greet имела только @Audited, поэтому исполнитель вывел пару сообщений входа/выхода вокруг метода, но не выполнял повторных попыток. Тот же исполнитель обработал save, обнаружив @Retry вместе с @Audited: первый вызов выбросил исключение (saveCalls == 1), вспомогательный метод зафиксировал ошибку и перешёл к следующей итерации, а вторая попытка вернула saved: data. Сами аннотации ничего не делали — поведение реализовал метод invoke.
  • unannotated прошёл через тот же цикл, потому что исполнитель единообразен. isAnnotationPresent вернул false для обеих аннотаций, поэтому вспомогательный метод не вёл журнал и не делал повторных попыток; метод просто выполнился один раз. В этом суть паттерна для процессоров: проверять аннотации, использовать разумное поведение по умолчанию при их отсутствии и никогда не выделять «аннотированный путь» в особый случай.
  • Каждый метод-аксессор элемента (a.value(), r.attempts(), r.when()) вернул значение, записанное в исходном коде. Retry.when() вернул константу перечисления ALWAYS, поскольку в месте вызова использовалось значение по умолчанию. Значения по умолчанию встраиваются в прокси аннотации компилятором; вызывающий код не может определить, было ли значение задано явно или используется по умолчанию.
  • toString объекта Audited вывел форму, похожую на исходный код, например @...Audited(level="WARN", value="Service.save"). Это свойство любого прокси аннотации — полезно для логирования и assertEquals в тестах. (Порядок, в котором элементы появляются внутри скобок, не гарантирован и различается между версиями JDK, поэтому не делайте проверок на точное строковое значение.)
  • Две аннотации полностью независимы на уровне исходного кода: один метод имеет обе сразу, и рефлексия вернула обе без проблем. Между типами аннотаций нет иерархии наследования; объединение поведений достигается путём наложения аннотаций на один элемент, а не расширением одной аннотации другой.

Ограничения и распространённые ошибки

Несколько типичных неожиданностей:

  • Аннотации с хранением SOURCE не видны через рефлексию. Если вы забудете @Retention(RUNTIME), рефлексия молча вернёт null. Значение по умолчанию — CLASS, а не RUNTIME.
  • Цели должны совпадать. Если указано @Target(METHOD), а аннотацию поставить на класс, компилятор откажет.
  • Значения по умолчанию элементов должны быть константами времени компиляции. Нельзя использовать new ArrayList<>() по умолчанию; можно использовать {} для массива, константу перечисления, литерал Class или примитивный литерал.
  • Аннотации не могут ссылаться на себя циклически. Элемент типа MyAnn внутри @interface MyAnn будет отклонён.

Следующая глава, обработка аннотаций, показывает сторону времени компиляции — генерацию новых исходных файлов в ответ на пользовательские аннотации вместо (или в дополнение к) их чтению во время выполнения.

Практика

Практика
Вы объявляете `@Cached { int ttlSeconds(); }` и ставите её на метод. Во время выполнения `m.getAnnotation(Cached.class)` возвращает `null`, хотя в исходном коде явно указано `@Cached(ttlSeconds = 60)`. В чём наиболее вероятная причина?
Вы объявляете `@Cached { int ttlSeconds(); }` и ставите её на метод. Во время выполнения `m.getAnnotation(Cached.class)` возвращает `null`, хотя в исходном коде явно указано `@Cached(ttlSeconds = 60)`. В чём наиболее вероятная причина?
Was this page helpful?