Пользовательские аннотации в 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), применяет их к классу и обходит методы через рефлексию — выполняя каждый метод либо внутри обёртки для аудита, либо в цикле повторных попыток. Аннотации являются чистыми метаданными; поведение реализовано в исполнителе.
Что следует вынести из этого примера:
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будет отклонён.
Следующая глава, обработка аннотаций, показывает сторону времени компиляции — генерацию новых исходных файлов в ответ на пользовательские аннотации вместо (или в дополнение к) их чтению во время выполнения.