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