W3docs

Java Reflection: Чтение аннотаций

Чтение метаданных аннотаций во время выполнения в Java через reflection — getAnnotation, getAnnotations.

В предыдущих главах рассматривалось объявление аннотаций (см. Пользовательские аннотации и Мета-аннотации); эта глава посвящена чтению аннотаций во время выполнения с помощью reflection. Аннотация с @Retention(RUNTIME) становится доступной для запросов на объектах Class, Method, Field, Constructor и Parameter, к которым она применена. Именно через чтение аннотаций JUnit находит @Test, Spring находит @Autowired, а фреймворки валидации находят @NotNull. В этой главе собран полный API для чтения аннотаций, включая особенности @Inherited и @Repeatable.

На этой странице описаны четыре метода чтения AnnotatedElement, объясняется, почему удержание является обязательным условием, как @Inherited и @Repeatable влияют на возвращаемые данные, как читать аннотации параметров, а также приведён рабочий пример сканера валидации, который можно запустить.

Четыре метода чтения

Каждый аннотируемый элемент (Class, Method, Field, Constructor, Parameter) реализует AnnotatedElement, который определяет одни и те же четыре метода повсюду:

AnnotatedElement el = SomeClass.class;   // or a Method, Field, etc.

el.isAnnotationPresent(Audited.class);   // boolean — quick check
el.getAnnotation(Audited.class);         // the annotation instance, or null
el.getAnnotations();                     // ALL annotations (declared + inherited)
el.getDeclaredAnnotations();             // only those declared directly here

Поскольку API единообразен, код для чтения аннотации с метода идентичен коду для чтения с класса — вы просто работаете с другим AnnotatedElement. Полученные значения аннотаций являются JVM-прокси; вызов a.value() — это настоящий вызов метода, возвращающего значение элемента, зафиксированное во время компиляции.

Удержание — обязательное условие

Это стоит повторить, поскольку является ошибкой номер один: только аннотации с удержанием RUNTIME видны через reflection.

@Retention(RetentionPolicy.RUNTIME)   // <-- required for reflection
@interface Audited { String value(); }

Удержание по умолчанию — CLASS: аннотация сохраняется в файле .class, но отбрасывается до запуска программы. Удержание SOURCE удаляет её ещё раньше. Если getAnnotation возвращает null для аннотации, которую вы явно видите в исходном коде, почти всегда причиной является отсутствующий @Retention(RUNTIME).

getAnnotations и getDeclaredAnnotations и @Inherited

Разница между этими двумя методами связана с @Inherited. По умолчанию аннотации не наследуются подклассами. Но если тип аннотации сам мета-аннотирован @Inherited, то подкласс наследует аннотацию уровня класса от своего суперкласса:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Component { }

@Component class Base { }
class Derived extends Base { }     // Derived has no @Component in source

Derived.class.getAnnotation(Component.class)          // → present! (inherited)
Derived.class.getDeclaredAnnotation(Component.class)  // → null (not declared here)

Таким образом, getAnnotations() включает унаследованные аннотации, а getDeclaredAnnotations() сообщает только о том, что физически написано на данном элементе. Два важных ограничения: @Inherited работает только для аннотаций классов (не методов или полей) и только вдоль цепочки суперкласса (не интерфейсов).

Повторяемые аннотации

Начиная с Java 8, аннотация, помеченная @Repeatable, может встречаться несколько раз на одном элементе. Под капотом компилятор объединяет повторения в контейнерную аннотацию, поэтому обычный getAnnotation не увидит их — нужно использовать getAnnotationsByType, который прозрачно распаковывает контейнер:

@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Role { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@interface Roles { Role[] value(); }     // the container

@Role("admin") @Role("user") class Account { }

Account.class.getAnnotationsByType(Role.class);   // → [Role(admin), Role(user)]
Account.class.getAnnotation(Role.class);          // → null! (it's wrapped in Roles)

Используйте getAnnotationsByType(Role.class) для повторяемых аннотаций; метод возвращает массив и обрабатывает как единичный, так и повторный случаи.

Чтение аннотаций параметров и других целей

Параметры получают свои аннотации через двумерный метод Method.getParameterAnnotations() (массив на каждый параметр) или более удобный API Parameter:

for (Parameter p : method.getParameters()) {
  if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}

Те же методы AnnotatedElement работают с Field, Constructor, Package и даже с самими аннотациями (для чтения мета-аннотаций, таких как @Retention). О том, как получить дескрипторы Method, Field и Constructor, рассказано в главах Отражение методов, Полей и Конструкторов.

Примечание

getParameterAnnotations() возвращает массив [parameterIndex][annotation] — по одному внутреннему массиву на каждый параметр, даже для параметров без аннотаций (такие внутренние массивы просто пусты). Цикл на основе Parameter, показанный выше, обычно понятнее.

Рабочий пример: мини-сканер валидации

Программа объявляет аннотации времени выполнения, демонстрирует случаи @Inherited и @Repeatable, а затем универсальный сканер обходит аннотации класса, аннотации его методов и аннотации параметров методов — скелет фреймворка валидации или маршрутизации.

java— editable, runs on the server

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

  • getAnnotation(Service.class) вернул живой прокси, чей метод value() вернул "users" — значение, написанное в исходном коде. Чтение аннотации — это просто вызов её методов-элементов; фреймворк реагирует на эти значения (здесь, трактуя "users" как префикс маршрута). Аннотация несёт данные, сканер обеспечивает поведение.
  • AdminController сообщил, что @Service присутствует, но не объявлена: isAnnotationPresent вернул true (унаследовано от UserController), тогда как getDeclaredAnnotation вернул null. Эта разница полностью обусловлена мета-аннотацией @Inherited, и работает она только потому, что @Service нацелена на класс — методы и поля никогда не наследуют аннотации таким образом.
  • list.getAnnotation(Role.class) вернул null, хотя в исходном коде явно указаны две аннотации @Role. Повторяемые аннотации оборачиваются компилятором в контейнер Roles, поэтому геттер одиночного значения их не находит; getAnnotationsByType(Role.class) распаковал контейнер и вернул обе роли. Для повторяемых аннотаций всегда используйте getAnnotationsByType.
  • Аннотации параметров были доступны для каждого параметра отдельно: параметр tenant сообщил о наличии @NotNull, а page — нет. Эта гранулярность на уровне параметров используется фреймворками bean-validation и привязки запросов для валидации или инъекции отдельных аргументов.
  • getDeclaredAnnotations() на методе list насчитал две аннотации — @Endpoint и синтетический контейнер Roles — подтверждая, что два @Role свернулись в один контейнер на уровне файла класса. Любая аннотация без @Retention(RUNTIME) не появилась бы в этом счёте вообще.

Практика

Практика
Фреймворк помечает методы повторяемой аннотацией '@Role' (у метода может быть несколько ролей). На методе, аннотированном '@Role('admin') @Role('editor')', вызов 'method.getAnnotation(Role.class)' возвращает null. Почему и что следует вызвать вместо него?
Фреймворк помечает методы повторяемой аннотацией '@Role' (у метода может быть несколько ролей). На методе, аннотированном '@Role('admin') @Role('editor')', вызов 'method.getAnnotation(Role.class)' возвращает null. Почему и что следует вызвать вместо него?
Was this page helpful?