TLDR; Вместо annotation.getClass().getMethod("value")
вызывается annotation.annotationType().getMethod("value")
.
Все разработчики Java слышали об аннотациях. Аннотации с нами начиная с Java 1.5 (или только 1.6, если вы настаиваете). Основываясь на моем опыте интервьюирования кандидатов, я чувствую, что большинство разработчиков Java знают, как использовать аннотации. Я имею в виду, что большинство разработчиков знают, что это похоже на @Test
или @Override
и что они поставляются с Java или с какой-то библиотекой и должны быть написаны перед классом, методом или переменной.
Некоторые разработчики знают, что вы также можете определить аннотацию в своем коде, используя @interface
и что ваш код может выполнять метапрограммирование с помощью аннотации. Еще меньше людей знают, что аннотации могут обрабатываться процессорами аннотаций, а некоторые из них могут обрабатываться во время выполнения.
Я мог бы продолжить, но вкратце, аннотации являются загадкой для большинства разработчиков Java. Если вы считаете, что я ошибаюсь, заявляя о том, насколько неразумно связано с аннотациями большинство разработчиков Java, то подумайте, что число программистов (или вообще программистов) росло в геометрической прогрессии в течение последних 30 лет, и разработчики Java, особенно, делали это. так в течение последних 20 лет, и это все еще растет в геометрической прогрессии. Экспоненциальная функция имеет такую особенность: если число сучков растет экспоненциально, то большинство сучков молодые.
Именно поэтому большинство разработчиков Java не знакомы с аннотациями.
Если честно, обработка аннотаций не является чем-то простым. Это заслуживает отдельной статьи, особенно когда мы хотим обрабатывать аннотации при использовании модульных систем.
Во время последних изменений в версии 1.2.0 инфраструктуры генерации кода Java :: Geci я столкнулся с проблемой, вызванной неправильным использованием аннотаций и отражений. Затем я понял, что, вероятно, большинство разработчиков, которые обрабатывают аннотации с использованием отражения, делают то же самое неправильно. В сети почти не было подсказки, чтобы помочь мне понять проблему. Все, что я нашел, это билет GitHub, и, основываясь на информации, я должен был выяснить, что на самом деле происходит.
Итак, давайте немного освежим аннотации, а после этого давайте посмотрим, что мы можем делать неправильно, что было хорошо, но может вызвать проблемы, когда в дело вступит JPMS.
Что такое аннотация?
Аннотации — это интерфейсы, которые объявлены с использованием ключевого слова interface
которому предшествует символ @
. Это делает аннотацию пригодной для использования в коде, как мы привыкли. Использование имени интерфейса аннотации с @
перед ним (например: @Example). Наиболее часто используемая такая аннотация — @Override
которую компилятор Java использует во время компиляции.
Многие платформы используют аннотации во время выполнения, другие подключаются к фазе компиляции, реализуя процессор аннотаций. Я писал о процессорах аннотаций и о том, как их создать. На этот раз мы сосредоточимся на более простом способе: обработке аннотаций во время выполнения. Мы даже не реализуем интерфейс аннотации, который редко используется, но сложен и труден для выполнения, как описано в статье .
Чтобы использовать аннотацию во время выполнения, аннотация должна быть доступна во время выполнения. По умолчанию аннотации доступны только во время компиляции и не попадают в сгенерированный байт-код. Распространенная ошибка — забывать (я всегда так делаю) помещать аннотацию @Retention(RetentionPolicy.RUNTIME)
в интерфейс аннотации, а затем начинать отлаживать, почему я не вижу свою аннотацию при обращении к ней с помощью отражения.
Простая аннотация во время выполнения выглядит следующим образом:
1
2
3
4
5
|
@Retention (RetentionPolicy.RUNTIME) @Repeatable (Demos. class ) public @interface Demo { String value() default "" ; } |
Аннотации имеют параметры при использовании в классах, методах или других аннотированных элементах. Эти параметры являются методами в интерфейсе. В этом примере в интерфейсе объявлен только один метод. Это называется value()
. Это особенный. Это своего рода метод по умолчанию. Если нет никаких других параметров интерфейса аннотации, или даже если они есть, но мы не хотим использовать другие параметры, и все они имеют значения по умолчанию, тогда мы можем написать
1
|
@Demo ( "This is the value" ) |
вместо
1
|
@Demo (value= "This is the value" ) |
Если есть другие параметры, которые нам нужно использовать, у нас нет этого ярлыка.
Как вы можете видеть, аннотации были введены поверх существующей структуры. Интерфейсы и классы используются для представления аннотаций, и это не было чем-то совершенно новым, введенным в Java.
Начиная с Java 1.8, на аннотированном элементе может быть несколько аннотаций одного типа. Вы могли бы иметь эту функцию даже до Java 1.8. Вы можете определить другую аннотацию, например
1
2
3
4
|
@Retention (RetentionPolicy.RUNTIME) public @interface Demos { Demo[] value(); } |
а затем использовать эту аннотацию оболочки на аннотированный элемент, как
1
2
3
4
5
|
@Demos (value = { @Demo ( "This is a demo class" ), @Demo ( "This is the second annotation" )}) public class DemoClassNonAbbreviated { } |
Чтобы облегчить тендинит, вызванный чрезмерной типизацией, в Java 1.8 была введена аннотация Repeatable
(как вы можете видеть в интерфейсе аннотации Demo
), и таким образом вышеприведенный код можно написать просто как
1
2
3
4
|
@Demo ( "This is a demo class" ) @Demo ( "This is the second annotation" ) public class DemoClassAbbreviated { } |
Как читать аннотацию, используя отражение
Теперь, когда мы знаем, что аннотация — это просто интерфейс, следующий вопрос — как мы можем получить информацию о них. Методы, которые доставляют информацию об аннотациях, находятся в отражающей части JDK. Если у нас есть элемент, который может иметь аннотацию (например, объект Class
, Method
или Field
), тогда мы можем вызвать getDeclaredAnnotations()
для этого элемента, чтобы получить все аннотации, которые есть у элемента, или getDeclaredAnnotation()
в случае, если мы знаем, какую аннотацию мы нужно.
Возвращаемым значением является объект аннотации (или массив аннотаций в первом случае). Очевидно, что это объект, потому что в Java все является объектом (или примитивом, но аннотации совсем не примитивны). Этот объект является экземпляром класса, который реализует интерфейс аннотации. Если мы хотим знать, какую строку написал программист между скобками, мы должны написать что-то вроде
1
2
3
4
5
|
final var klass = DemoClass. class ; final var annotation = klass.getDeclaredAnnotation(Demo. class ); final var valueMethod = annotation.getClass().getMethod( "value" ); final var value = valueMethod.invoke(annotation); Assertions.assertEquals( "This is a demo class" , value); |
Поскольку значение — это метод в интерфейсе, безусловно реализуемый классом, к которому у нас есть доступ через один из его экземпляров, мы можем вызвать его рефлексивно и вернуть результат, который в данном случае является "This is a demo class"
.
В чем проблема с этим подходом
Вообще ничего, пока мы не в сфере JPMS. Мы получаем доступ к методу класса и вызываем его. Мы могли бы получить доступ к методу интерфейса и вызвать его на объекте, но на практике это то же самое. (Или не в случае JPMS.)
Я использовал этот подход в Java :: Geci. Фреймворк использует аннотацию @Geci
чтобы определить, к какому классу требуется сгенерированный код. У него довольно сложный алгоритм поиска аннотаций, потому что он принимает любую аннотацию с именем Geci
независимо от того, в каком пакете он находится, и также принимает любой @interface
, аннотированный с помощью аннотации Geci
(он называется Geci
или аннотация имеет аннотация, которая является Geci
рекурсивно).
Эта сложная обработка аннотаций имеет свою причину. Каркас является сложным, поэтому использование может быть простым. Вы можете также сказать:
1
|
@Geci ( "fluent definedBy='javax0.geci.buildfluent.TestBuildFluentForSourceBuilder::sourceBuilderGrammar'" ) |
или вы можете иметь свои собственные аннотации, а затем сказать
1
|
@Fluent (definedBy= "javax0.geci.buildfluent.TestBuildFluentForSourceBuilder::sourceBuilderGrammar" ) |
Код работал нормально до Java 11. Когда код был выполнен с использованием Java 11, я получил следующую ошибку от одного из тестов
1
2
3
4
|
java.lang.reflect.InaccessibleObjectException: Unable to make public final java.lang.String com.sun.proxy.jdk.proxy1.$Proxy12.value() accessible: module jdk.proxy1 does not "exports com.sun.proxy.jdk.proxy1" to module geci.tools |
(Для удобства чтения были вставлены некоторые разрывы строк.)
Включается защита JPMS, и она не позволяет нам получить доступ к чему-то в JDK, к которому мы не должны. Вопрос в том, что мы действительно делаем и почему мы это делаем?
При выполнении тестов в JPMS мы должны добавить много --add-opens
командной строки --add-opens
к тестам, потому что тестовая среда хочет получить доступ к той части кода, используя отражение, которое недоступно для пользователя библиотеки. Но этот код ошибки не о модуле, который определен в Java :: Geci.
JPMS защищает библиотеки от неправильного использования. Вы можете указать, какие пакеты содержат классы, которые можно использовать извне. Другие пакеты, даже если они содержат открытые интерфейсы и классы, доступны только внутри модуля. Это помогает разработке модуля. Пользователи не могут использовать внутренние классы, поэтому вы можете изменять их, пока остается API. Файл module-info.java
объявляет эти пакеты как
1
2
3
|
module javax0.jpms.annotation.demo.use { exports javax0.demo.jpms.annotation; } |
Когда пакет экспортируется, к классам и интерфейсам в пакете можно получить доступ напрямую или через отражение. Есть еще один способ предоставить доступ к классам и интерфейсам в пакете. Это открытие пакета. Ключевое слово для этого opens
. Если module-info.java
только opens
пакет, то это доступно только через отражение.
Вышеупомянутое сообщение об ошибке говорит, что модуль jdk.proxy1
не включает в свой module-info.java
строку, которая exports com.sun.proxy.jdk.proxy1
. Вы можете попробовать добавить add-exports jdk.proxy1/com.sun.proxy.jdk.proxy1=ALL_UNNAMED
но это не работает. Я не знаю, почему это не работает, но это не так. И на самом деле, это хорошо, что он не работает, потому что пакет com.sun.proxy.jdk.proxy1
является внутренней частью JDK, как unsafe
, которая в прошлом вызывала такую большую головную боль для Java.
Вместо того, чтобы пытаться незаконно открыть сундук с сокровищами, давайте сосредоточимся на том, почему мы хотели открыть его в первую очередь и нужен ли нам доступ к нему?
Мы хотим получить доступ к методу класса и вызвать его. Мы не можем этого сделать, потому что JPMS запрещает это. Почему? Потому что класс Annotation objects не является Demo.class
(что очевидно, поскольку это просто интерфейс). Вместо этого это прокси-класс, который реализует Demo
интерфейс. Этот прокси-класс является внутренним для JDK, и поэтому мы не можем вызывать annotation.getClass()
. Но зачем нам обращаться к классу прокси-объекта, когда мы хотим вызвать метод нашей аннотации?
Короче говоря (я имею в виду несколько часов отладки, исследования и понимания вместо бессмысленного копирования / вставки в стеке, который никто не делает): мы не должны касаться метода value()
класса, который реализует интерфейс аннотаций. Мы должны использовать следующий код:
1
2
3
4
5
|
final var klass = DemoClass. class ; final var annotation = klass.getDeclaredAnnotation(Demo. class ); final var valueMethod = annotation.annotationType().getMethod( "value" ); final var value = valueMethod.invoke(annotation); Assertions.assertEquals( "This is a demo class" , value); |
или в качестве альтернативы
1
2
3
4
5
|
final var klass = DemoClass. class ; final var annotation = klass.getDeclaredAnnotation(Demo. class ); final var valueMethod = Demo. class .getMethod( "value" ); final var value = valueMethod.invoke(annotation); Assertions.assertEquals( "This is a demo class" , value); |
(Это уже исправлено в Java :: Geci 1.2.0) У нас есть объект аннотации, но вместо того, чтобы запрашивать его класс, мы должны получить доступ к annotationType()
, который является самим интерфейсом, который мы кодировали. Это то, что модуль экспортирует, и поэтому мы можем вызвать его.
Мой сын Михал Верхас, который также является разработчиком Java в EPAM, обычно рецензирует мои статьи. В этом случае «обзор» был расширен, и он написал немаловажную часть статьи.
Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Обработка аннотаций и JPMS Мнения, высказанные участниками Java Code Geeks, являются их собственными. |