Файл: Кей С. Хорстманн.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 12.01.2024

Просмотров: 112

Скачиваний: 5

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

443
11.3. Стандартные аннотации то компилятор известит об ошибке в связи с тем, что метод equals() не пе- реопределяет одноименный метод equals() из класса Object, поскольку па- раметр этого метода относится к типу Object, а не к типу Point.
Аннотация @SuppressWarnings дает компилятору команду подавить пред- упреждения конкретного типа, как показано в следующем примере кода:
@SuppressWarnings("unchecked") T[] result =
(T[]) Array.newInstance(cl, n);
Аннотация @SafeVarargs утверждает, что метод не нарушает свой пара- метр переменной длины (см. главу 6).
Аннотация @Generated предназначена для применения в инструменталь- ных средствах генерирования кода. Любой генерируемый исходный код мо- жет быть аннотирован, чтобы отличать его от кода, написанного вручную.
Например, в редакторе исходного текста можно скрыть генерируемый код, а в генераторе кода — удалить прежние версии генерируемого кода. Каждая аннотация должна содержать однозначный идентификатор генератора кода.
Дополнительную строку с датой (в формате по стандарту ISO 8601) и строку комментариев указывать можно, но не обязательно:
@Generated(value="com.horstmann.generator", date="2015-01-04T12:08:56.235-0700");
В главе 3 был приведен пример употребления аннотации @Functional
Interface. Она аннотирует преобразование адресатов лямбда-выражений, как показано ниже. Если в дальнейшем ввести в данный интерфейс еще один абстрактный метод, компилятор выдаст ошибку.
@FunctionalInterface public interface IntFunction {
R apply(int value);
}
Разумеется, такие аннотации должны быть введены в интерфейсы, описы- вающие отдельные функции. Имеются и другие интерфейсы с единственным абстрактным методом (например, интерфейс AutoCloseable), по существу, не являющиеся функциями.
11.3.2. Аннотации для управления ресурсами
Аннотации @PostConstruct и @PreDestroy применяются в средах, управ- ляющих сроком действия объектов, например, в веб-контейнерах и серверах приложений. Методы, помеченные этими аннотациями, должны вызываться сразу же после создания объекта или непосредственно перед его удалением.
Аннотация @Resource предназначена для внедрения ресурсов. В каче- стве примера рассмотрим веб-приложение, осуществляющее доступ к базе
Java_SE_9_for_the_Impatient_2nd_Edit.indb 443 16.04.2018 16:40:27

Глава 11
„
Аннотации
444
данных. Безусловно, доступ к информации в базе данных не должен быть жестко закодирован в данном веб-приложении. Вместо этого у веб-контей- нера имеется свой пользовательский интерфейс для установки параметров подключения к базе данных, а также имя источника данных, определяемое в прикладном программном интерфейсе JNDI. Обращаться к этому источнику данных из веб-приложения можно следующим образом:
@Resource(name="jdbc/employeedb") private DataSource source;
Когда создается объект, содержащий приведенную выше переменную эк- земпляра, веб-контейнер внедряет ссылку на источник данных. Это означает, что он устанавливает в переменной экземпляра объект типа DataSource, на- страиваемый на имя "jdbc/employeedb".
11.3.3. Мета-аннотации
Мета-аннотации @Target и @Retention упоминались в разделе 11.2. А ме- та-документация @Documented предоставляет указание для инструментальных средств документирования вроде утилиты javadoc. Документируемые анно- тации следует рассматривать как разновидность модификаторов (например, private или static), употребляемых для документирования. Остальные ан- нотации не следует включать в документацию.
Например, аннотация @SuppressWarnings не документируется. Если метод или поле содержит такую аннотацию, то особенности ее реализации мало- интересны читающему документацию на прикладной код. С другой стороны, аннотация @FunctionalInterface документируется, поскольку программи- сту важно знать, что аннотируемый ею интерфейс предназначен для описа- ния функции. Пример документируемой аннотации приведен на рис. 11.1.
Мета-аннотация @Inherited применяется только к аннотациям классов.
Если в классе имеется наследуемая аннотация, то все его подклассы автома- тически получают ту же самую аннотацию. Благодаря этому упрощается со- здание аннотаций, действующих аналогично маркерным интерфейсам (на- пример, интерфейсу Serializable).
Допустим, аннотация @Persistent определяется с целью определить, что объекты класса могут быть сохранены в базе данных. В таком случае подклас- сы постоянных классов автоматически аннотируются как постоянные.
@Inherited @interface Persistent { }
@Persistent class Employee { . . . } class Manager extends Employee { . . . }
// Этот класс также имеет аннотацию @Persistent
Java_SE_9_for_the_Impatient_2nd_Edit.indb 444 16.04.2018 16:40:27


445
11.3. Стандартные аннотации
Рис. 11.1. Документируемая аннотация
Мета-аннотация @Repeatable позволяет применить одну и ту же аннота- цию неоднократно. Допустим, что аннотация @TestCase повторяется. В та- ком случае ею можно воспользоваться следующим образом:
@TestCase(params="4", expected="24")
@TestCase(params="0", expected="1") public static long factorial(int n) { ... }
По ряду исторических причин разработчикам повторяющейся аннотации пришлось предоставить контейнерную аннотацию, содержащую повторяю- щиеся аннотации в массиве. Ниже показано, каким образом определяется аннотация @TestCase и ее контейнер.
@Repeatable(TestCases.class)
@interface TestCase {
String params();
String expected();
}
@interface TestCases {
TestCase[] value();
}
Java_SE_9_for_the_Impatient_2nd_Edit.indb 445 16.04.2018 16:40:27

Глава 11
„
Аннотации
446
Всякий раз, когда пользователь предоставляет две или больше аннота- ции @TestCase, они автоматически заключаются в оболочку аннотации
@TestCases. Это усложняет обработку аннотации, как будет показано в сле- дующем разделе.
11.4. Обработка аннотаций во время выполнения
В приведенных до сих пор примерах было показано, каким образом анно- тации вводятся в исходные файлы и как определяются типы аннотаций. А те- перь настало время выяснить, какую же пользу можно извлечь из аннотаций.
В этом разделе на простом примере поясняется обработка аннотаций во время выполнения с использованием прикладного программного интерфей- са API для рефлексии, рассматривавшегося в главе 4. Допустим, требуется сократить затраты труда на реализацию методов типа toString. Можно, конечно, написать обобщенный метод toString(), используя рефлексию, чтобы учесть имена и значения всех переменных экземпляра. Но допустим, что этот процесс требуется специально настроить, чтобы не включать в него все переменные экземпляра или пропустить имена классов и переменных.
Например, для класса Point более предпочтительной может оказаться обо- значение координат точки [5,10] вместо обозначения Point[x=5,y=10]. Раз- умеется, в данный процесс можно внести и другие усовершенствования, но мы не будем этого делать ради простоты примера. Самое главное — проде- монстрировать в нем возможности процессора аннотаций.
Все классы, в которых требуется извлечь выгоду из данного процесса, сле- дует снабдить аннотацией @ToString. Аннотировать следует и все перемен- ные экземпляра, которые должны быть включены в данный процесс. Аннота- ция @ToString определяется следующим образом:
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) public @interface ToString { boolean includeName() default true;
}
Ниже приведены аннотированные классы Point и Rectangle. При этом преследуется цель представить прямоугольник в виде символьной строки с параметрами Rectangle[[5, 10], width=20,height=30].
@ToString(includeName=false) public class Point {
@ToString(includeName=false) private int x;
@ToString(includeName=false) private int y;
}
Java_SE_9_for_the_Impatient_2nd_Edit.indb 446 16.04.2018 16:40:27


1   2   3   4

447
11.4. Обработка аннотаций во время выполнения
@ToString public class Rectangle {
@ToString(includeName=false) private Point topLeft;
@ToString private int width;
@ToString private int height;
}
Во время выполнения нельзя изменить реализацию метода toString() в отдельном классе. Вместо этого можно предоставить метод, способный от- форматировать любой объект, обнаруживая и применяя аннотации @To
String, если они имеются.
Для обработки аннотаций служат следующие методы из интерфейса
AnnotatedElement, который реализуется в классах рефлексии Class, Field,
Parameter, Method, Constructor и Package:
T getAnnotation(Class)
T getDeclaredAnnotation(Class)
T[] getAnnotationsByType(Class)
T[] getDeclaredAnnotationsByType(Class)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()
Как и остальные методы рефлексии, методы со словом Declared в их име- ни получают аннотации в самом классе, тогда как остальные методы включа- ют наследуемые аннотации. В контексте аннотаций это означает аннотацию
@Inherited, применяемую в суперклассе.
Если аннотация не является повторяющейся, для ее обнаружения следует вызвать метод getAnnotation(), как показано ниже.
Class cl = obj.getClass();
ToString ts = cl.getAnnotation(ToString.class); if (ts != null && ts.includeName()) ...
Обратите внимание на то, что методу getAnnotation() передается объ- ект класса для аннотации (в данном случае — объект ToString.class), а возвращается объект некоторого класса-заместителя, реализующего интер- фейс ToString. Для получения значений элементов аннотации можно вы- звать методы из этого интерфейса. Если же аннотация отсутствует, то метод getAnnotation() возвращает пустое значение null.
Дело несколько усложняется, если аннотация оказывается повторяющей- ся. Если вызвать метод getAnnotation() для поиска повторяющейся аннота- ции, которая на самом деле не повторялась, то и в этом случае может быть получено пустое значение null. Объясняется это тем, что повторяющиеся ан- нотации были заключены в оболочку контейнерной аннотации.
Java_SE_9_for_the_Impatient_2nd_Edit.indb 447 16.04.2018 16:40:27

Глава 11
„
Аннотации
448
В данном случае следует вызвать метод getAnnotationsByType(), где про- сматривается контейнер и предоставляется массив повторяющихся аннота- ций. Если бы имелась только одна аннотация, то она была бы получена в массиве единичной длины. Имея в своем распоряжении данный метод, мож- но вообще не беспокоиться о контейнерной аннотации.
Метод getAnnotations() получает все аннотации (любого типа), которы- ми аннотируется элемент кода, причем повторяющиеся аннотации заклю- чаются в оболочку контейнеров. Ниже приведен пример реализации метода toString() с учетом аннотаций.
public class ToStrings { public static String toString(Object obj) { if (obj == null) return "null";
Class> cl = obj.getClass();
ToString ts = cl.getAnnotation(ToString.class); if (ts == null) return obj.toString();
StringBuilder result = new StringBuilder();
if (ts.includeName()) result.append(cl.getName()); result.append("["); boolean first = true; for (Field f : cl.getDeclaredFields()) {
ts = f.getAnnotation(ToString.class); if (ts != null) { if (first) first = false; else result.append(","); f.setAccessible(true);
if (ts.includeName()) { result.append(f.getName()); result.append("=");
} try { result.append(ToStrings.toString(f.get(obj)));
} catch (ReflectiveOperationException ex) { ex.printStackTrace();
}
}
} result.append("]"); return result.toString();
}
}
Если класс аннотируется средствами интерфейса ToString, то в методе toString() перебираются поля этого класса и выводятся те из них, которые также аннотированы. Если же элемент includeName имеет логическое значе- ние true, то имя класса или поля включается в результирующую символь- ную строку.
Java_SE_9_for_the_Impatient_2nd_Edit.indb 448 16.04.2018 16:40:27


449
11.5. Обработка аннотаций на уровне исходного кода
Следует иметь в виду, что данный метод вызывается рекурсивно. Всякий раз, когда объект принадлежит классу, который не аннотирован, вызывается его обычный метод toString() и рекурсия останавливается.
Это простой, но типичный пример применения прикладного программ- ного интерфейса API для обработки аннотаций во время выполнения. Клас- сы, поля и прочие элементы кода обнаруживаются с помощью рефлек- сии, а с целью извлечь аннотации вызывается метод getAnnotation() или getAnnotationsByType() для потенциально аннотированных элементов.
И далее для получения значений отдельных элементов аннотации вызывают- ся методы из соответствующего интерфейса аннотаций.
11.5. Обработка аннотаций на уровне исходного кода
В предыдущем разделе было показано, каким образом аннотации анали- зируются в выполняющейся программе. Еще одним примером применения аннотаций служит автоматическая обработка исходных файлов для получе- ния дополнительного исходного кода, файлов конфигурации, сценариев и вообще всего, что можно сгенерировать.
Чтобы продемонстрировать внутренний механизм обработки аннотаций на уровне исходного кода, вернемся к примеру формирования методов типа toString. Но на этот раз они будут сформированы в исходном файле Java.
Затем эти методы будут скомпилированы вместе с остальной частью про- граммы и выполнены с максимальным быстродействием вместо применения рефлексии.
11.5.1. Процессоры аннотаций
Обработка аннотаций встроена в компилятор Java. Во время компиляции
процессоры аннотаций можно вызывать по следующей команде:
javac -processor ИмяКлассаПроцессора
1
,
ИмяКлассаПроцессора
2
,...
Исходные_файлы
Компилятор обнаруживает аннотации в исходных файлах. Каждый про- цессор аннотаций выполняется по очереди с учетом тех аннотаций, к кото- рым он проявил интерес. Если процессор аннотаций создает новый исход- ный файл, то данный процесс повторяется. Как только все исходные файлы будут обработаны, они компилируются.
НА ЗАМЕТКУ. Процессор аннотаций может только формировать новые исходные файлы, но не может изменять уже имеющиеся исходные файлы.
Java_SE_9_for_the_Impatient_2nd_Edit.indb 449 16.04.2018 16:40:27

Глава 11
„
Аннотации
450
Процессор аннотаций реализует интерфейс Processor, как правило, рас- ширяя класс AbstractProcessor. При этом нужно указать, какие именно аннотации поддерживаются процессором. В данном случае это следующие аннотации:
@SupportedAnnotationTypes(
"com.horstmann.annotations.ToString")
@SupportedSourceVersion(SourceVersion.RELEASE_8) public class ToStringAnnotationProcessor extends AbstractProcessor {
@Override public boolean process(
Set extends TypeElement> annotations,
RoundEnvironment currentRound) {
}
}
Процессору могут потребоваться конкретные типы аннотаций, метасим- волы подстановки вроде "com.horstmann.*" (т.е. все аннотации из пакета com.horstmann и любых его подпакетов) или даже "*" (т.е. все аннотации во- обще). Метод process() вызывается один раз на каждом цикле обработки со всеми аннотациями, обнаруженными в любых файлах в данном цикле, а также со ссылкой на интерфейс RoundEnvironment, содержащей сведения о текущем цикле обработки.
11.5.2. Прикладной программный интерфейс API модели языка
Для анализа аннотаций на уровне исходного кода служит прикладной программный интерфейс API модели языка. В отличие от прикладного про- граммного интерфейса API для рефлексии, представляющего классы и ме- тоды на уровне виртуальной машины, прикладной программный интерфейс
API модели языка позволяет анализировать программу на Java по правилам языка Java.
Компилятор получает дерево, узлами которого являются экземпляры клас- сов, реализующих интерфейс javax.lang.model.element.Element и произво- дные от него интерфейсы TypeElement, VariableElement, ExecutableElement и т.д. Они служат статическими аналогами классов рефлексии Class, Field/
Parameter, Method/Constructor.
Не вдаваясь в подробности прикладного программного интерфейса API модели языка, ниже перечислим главные его особенности, о которых нужно знать для обработки аннотаций.

Интерфейс RoundEnvironment предоставляет все элементы кода, поме- ченные конкретной аннотацией. Для этой цели вызывается следующий метод:
Java_SE_9_for_the_Impatient_2nd_Edit.indb 450 16.04.2018 16:40:27