Довольно таки часто можно заметить @Annotation в проектах Java, но не все и не всегда задумываются, что это и как ними работать, в этом уроке я более подробно объясню, что такое аннотации и как с ними работать к тому же напишем свою аннотацию.
Шаг 1. Теория
Аннотация(@Annotation) — специальная форма метаданных, которая может быть добавлена в исходный код.
Аннотации используются для анализа кода, компиляции или выполнения. Аннотированы могут быть пакеты, классы, методы, переменные и параметры.
Например всем известная библиотека JUnit использует аннотации для проведения модульного тестирования:
@Test public void test1(){ // Тестирование } @Ignore @Test public void test2(){ // Тестирование }
где @Test и @Ignore – аннотации.
Аннотация выполняет следующие функции:
1) дает необходимую информацию для компилятора;
2) дает информацию различным инструментам для генерации другого кода, конфигураций и т. д.;
3) может использоваться во время работы кода;
Самая часто встречаемая аннотация, которую встречал любой программист, даже начинающий это @Override:
@Override public String toString(){ return "devcolibri.com"; }
Шаг 2. Как создать свою Аннотацию.
В интернете большое количество документации по поводу аннотаций, потому что аннотации очень сильно облегчают жизнь программисту.
Написать свою аннотацию не так сложно, как могло бы казаться.
Все что нам нужно для создание своей аннотации, это создать файл About.java назвать его можно как угодно, но от этого названия будет зависеть имя вашей аннотации, если мы назвали его About.java то аннотация будет выглядеть так @About.
Напишем в созданном файле About.java следующий код:
public @interface About{ String info() default ""; }
как вы видите на месте где обычно пишут class или interface у нас написано @interface.
По просту структура практически та же, что и у интерфейсов, только пишется @interface.
@interface
– указывает на то, что это аннотация
default
– говорит про то, что метод по умолчанию будет возвращать определённое значение.
Вот и все аннотация готова теперь ею можно пользоваться, но есть одно НО, аннотацию можно сконфигурировать.
Шаг 3. Конфигурации для аннотации.
Так как мы в Шаге 2 ничего не конфигурировали, то она может применяться к чему только угодно, к классам, методам, атрибутам и т. п.
Для того чтобы ограничить использование аннотации её нужно проаннотировать
Для этого существует аннотация @Target.
@Target(ElementType.PACKAGE)
– только для пакетов;
@Target(ElementType.TYPE)
– только для классов;
@Target(ElementType.CONSTRUCTOR)
– только для конструкторов;
@Target(ElementType.METHOD)
– только для методов;
@Target(ElementType.FIELD)
– только для атрибутов(переменных) класса;
@Target(ElementType.PARAMATER)
– только для параметров метода;
@Target(ElementType.LOCAL_VARIABLE)
– только для локальных переменных.
В случае если вы хотите, что бы ваша аннотация использовалась больше чем для одного типа параметров, то можно указать @Target следующим образом:
@Target({ ElementType.PARAMETER, ElementType.LOCAL_VARIABLE })
тут мы говорим, аннотацию можно использовать только для параметров метода и для локальных переменных.
Шаг 4. Реализация
Вы наверное уже заметили что написанная нами аннотация About – это практически интерфейс который ничего не реализовывает, по этому нам нужно указать что же должно происходить с аннотированным фрагментом кода.
Как вам уже известно Аннотация это всего лишь маркер который выделяет что-то, и по этому маркеру мы можем легко найти фрагмент кода и что-то сделать.
Давайте реализуем всем известный framework JUnit.
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test { Class expected(); }
как вы уже заметили у наc появилась новая конфигурация для нашей аннотации @Retention
@Retention
– эта аннотация позволит позволит сохранять нашу аннотацию JVM во время выполнения, что даст возможность использовать отображение(reflection).
Теперь мы должны для нашей аннотации написать класс анализатор. Этот класс будет анализировать наши аннотации и выполнять некоторые действия, связанные с аннотируемыми параметрами.
Имейте в виду, что если у вас есть более чем одна пользовательская аннотации, то целесообразно иметь отдельный анализатор для каждой аннотации что вы определите.
Что должен делать анализатор? Он использует Java отражения для доступа к аннотируемым данным.
Пример анализатора для @Test:
public class TestAnnotationAnalyzer { public void parse(Class<?> clazz) throws Exception { Method[] methods = clazz.getMethods(); int pass = 0; int fail = 0; for (Method method : methods) { if (method.isAnnotationPresent(Test.class)) { try { method.invoke(null); pass++; } catch (Exception e) { fail++; } } } } }
Обратите внимание что в строке 10 мы вызываем аннотируемый метод.
Это все. Анализатор готов к использованию, но постойте, мы не реализовали атрибуты аннотации. Эта часть немного сложнее. Потому что вы не можете напрямую обращаться к этим атрибутам.
public class TestAnnotationAnalyzer { public void analyz(Class<?> clazz) throws Exception { Method[] methods = clazz.getMethods(); int pass = 0; int fail = 0; for (Method method : methods) { if (method.isAnnotationPresent(Test.class)) { // Получаем доступ к атрибутам Test test = method.getAnnotation(Test.class); Class expected = test.expected(); try { method.invoke(null); pass++; } catch (Exception e) { if (Exception.class != expected) { fail++; } else { pass++; } } } } } }
В строке 10 мы получаем доступ к атрибуту аннотации и дальше в строке 11 получаем значение атрибута аннотации, в нашем случае это значение типа Class так как expected – это ожидаемая ошибка и мы будем получать exception.
Ну и в строке 16 мы проверяем ожидаемый Exception с полученным.
Шаг 5. Использование
Давайте теперь используем нашу аннотацию:
public class Demo { public static void main(String [] args) { TestAnnotationAnalyzer analyzer = new TestAnnotationAnalyzer(); analyzer.analyz(MyTest.class); } }
Внимание!
В уроке я описал теоретическое объяснение Аннотациям и кратко продемонстрировал реализацию, скачав исходник полной реализации вы можете выше нажав кнопку Скачать.
P.S. Спасибо за внимание, оставляйте комментарии, предлагайте варианты лучше или же поправьте меня если что-то не так.
- None Found
Аннотации – это своеобразные маркеры, с помощью которых программист указывает компилятору Java и средствам разработки, что делать с участками кода помимо исполнения. Аннотировать можно переменные, параметры, классы, пакеты. Можно писать свои аннотации или использовать стандартные – встроенные в Java.
Вы узнаете аннотацию по символу @
в начале имени. Самая часто встречаемая аннотация, которую встречал любой программист это @Override
. Эта аннотация сообщает компилятору, что мы переопределили метод. Поэтому, когда метод суперкласса будет удален или изменен, компилятор выдаст сообщение об ошибке. Рассмотрим небольшой пример:
class SomeClass {
void method() {
System.out.println("Работает метод родительского класса.");
}
}
class AnotherClass extends SomeClass { // наследуем методы SomeClass в новом классе
@Override
void method() { // переопределяем метод
System.out.println("Работает метод класса-потомка.");
}
}
Если в имени метода из класса AnotherClass
будет опечатка, компилятор учтет @Override
и выдаст ошибку. Без аннотации он не заметил бы подвоха и создал бы новый метод в дополнение к method
из SomeClass
.
Обратите внимание, сама аннотация никак не влияет на переопределение метода, но позволяет контролировать успешность переопределения при компиляции или сборке. Мы защитили участок кода от неприметной ошибки, на поиск которой в большой программе ушли бы часы. Это лишь одно из многих применений аннотаций.
Спонсор поста
Структура аннотации
Создание аннотаций очень похоже на создание интерфейса, только вот само ключевое слово interface
пишется со знаком @
.
public @interface MyAnnotation {
String name() default "";
int value();
}
Параметры задаются как методы у интерфейсов, только без аргументов. А ключевое слово default
— говорит про то, что метод по умолчанию будет возвращать определённое значение.
Так как мы не сконфигурировали аннотацию, то она может применяться к чему угодно: к классам, методам, атрибутам и т. п. Для того чтобы ограничить использование аннотации, её нужно разметить аннотациями 😄
@Target(ElementType.TYPE)
public @interface MyAnnotation {
...
}
Аннотация @Target
позволяет ограничить область применения:
@Target(ElementType.PACKAGE)
– только для пакетов;@Target(ElementType.TYPE)
– только для классов;@Target(ElementType.CONSTRUCTOR)
– только для конструкторов;@Target(ElementType.METHOD)
– только для методов;@Target(ElementType.FIELD)
– только для атрибутов(переменных) класса;@Target(ElementType.PARAMETER)
– только для параметров метода;@Target(ElementType.LOCAL_VARIABLE)
– только для локальных переменных;@Target(ElementType.ANNOTATION_TYPE)
— означает аннотацию конфигурацию. Таким образом, аннотация может использоваться только для аннотирования других аннотаций. Как@Target
и@Retention
.
Если нужно, что бы ваша аннотация использовалась больше чем для одного типа, укажите @Target
следующим образом:
@Target({ ElementType.PARAMETER, ElementType.LOCAL_VARIABLE })
Помимо @Target
есть еще несколько аннотаций, для настройки:
@Retention
определяет в каком жизненном цикле кода аннотация будет доступна.
SOURCE
— аннотация доступна только в исходном коде и стирается во время создания.class
файла;CLASS
— аннотация хранится и в.class
файле, но недоступна во время выполнения программы;RUNTIME
— аннотация хранится в.class
файле и доступна во время выполнения программы.
@Inherited
позволяет реализовать наследование аннотаций родительского класса классом-наследником
@Inherited
public @interface MyAnnotation { }
@MyAnnotation
public class MySuperClass { ... }
public class MySubClass extends MySuperClass { ... }
В этом примере класс MySubClass
наследует аннотацию @MyAnnotation
, потому что MySubClass
наследуется от MySuperClass
, а MySuperClass
имеет @MyAnnotation
.
@Documented
— аннотация будет помещена в сгенерированную документацию javadoc
Обработчик аннотации
Но магии в программировании нет, и аннотации сами по себе ничего не делают, нужно написать обработчик аннотации.
Самое большое ограничение аннотаций — это не возможность изменять существующие классы, можно только создавать новые. Исключением является проект lombok, который может изменять классы, например добавлять геттеры и сеттеры, конструкторы и так далее.
Давайте закрепим полученные знания на примере. Создадим аннотацию @FieldNames
, которая будет генерировать новый класс содержащий строки названия полей. Проще на примере, есть у нас класс:
public class Simple {
private String text;
private Integer number;
private Long numberTwo;
}
А наша аннотация должна сгенерировать нам класс в том же пакете с названием SimpleFields
:
public class SimpleFields {
public final static String TEXT = "text";
public final static String NUMBER = "number";
public final static String NUMBER_TWO = "numberTwo";
}
Для этого создаем аннотацию @FieldNames
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface FieldNames {
String postfix() default "Fields";
}
Параметр postfix
будет отвечать за окончание названия сгенерированного класса. По умолчанию будет к названию класса добавляется Fields
.
Самое простое позади, теперь создадим обработчик FieldNameProcessor
, который наследуется от AbstractProcessor
.
@SupportedAnnotationTypes("org.sadtech.example.annotation.FieldNames")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
@AutoService(Processor.class)
public class FieldNameProcessor extends AbstractProcessor {
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
return false;
}
}
Аннотация @SupportedAnnotationTypes
отвечает за указание аннотации для которой этот обработчик создается.
Аннотация @AutoService
упрощает создание манифеста. Но для нее нужно добавить новую зависимость
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
<scope>provided</scope>
</dependency>
Как вы можете видеть нам необходимо реализовать метод process
.
Нам необходимо получить все классы, которые помечены нашей аннотацией.
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (TypeElement annotation : set) {
Set<? extends Element> annotatedElements = roundEnvironment.getElementsAnnotatedWith(annotation);
for (Element annotatedElement : annotatedElements) {
// тут будет логика обработки
}
}
return true;
}
Осталось написать логику обработки нашей аннотации. Логика будет такая:
- Получить необходимую информацию аннотированного класса:
- Имя класса
- Имя пакета
- Массив полей
- Сложим это информацию в обычный POJO класс
- На основании этого класса создадим новый класс.
Создадим 2 новых класса. ClassDto
будет содержать информацию, необходимую для генерации нового класса. Класс FieldDto
будет отвечать за информацию необходимую для создания public static final String
полей. Лучше смотреть на примерах, так сложно объяснить.
public class ClassDto {
private String className;
private String classPackage;
private Set fields;
// getters and setters
}
public class FieldDto {
private final String fieldStringName;
private final String fieldName;
private FieldDto(String fieldStringName, String fieldName) {
this.fieldStringName = fieldStringName;
this.fieldName = fieldName;
}
public static FieldDto of(String fieldStringName, String fieldName) {
return new FieldDto(fieldStringName, fieldName);
}
// getters
}
Чтобы преобразовать имя переменной numberTwo
в имя статической переменной NUMBER_TWO
нам понадобиться еще одна зависисомость:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
Теперь нам надо заполнить класс ClassDto
информацией:
@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
for (TypeElement annotation : set) {
Set annotatedElements = roundEnvironment.getElementsAnnotatedWith(annotation);
for (Element annotatedElement : annotatedElements) {
final TypeMirror mirror = annotatedElement.asType();
final String annotatedElementName = annotatedElement.getSimpleName().toString();
final FieldNames settings = annotatedElement.getAnnotation(FieldNames.class);
final String newClassName = annotatedElementName + settings.postfix();
final Set fields = annotatedElement.getEnclosedElements().stream()
.filter(this::isField)
.map(
element -> {
final String fieldName = element.getSimpleName().toString();
final String fieldStringName = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, fieldName);
return FieldDto.of(fieldStringName, fieldName);
}
).collect(Collectors.toSet());
final ClassDto newClass = new ClassDto();
newClass.setClassName(newClassName);
newClass.setFields(fields);
newClass.setClassPackage(getPackage(mirror));
ClassCreator.record(newClass, processingEnv);
}
}
return true;
}
public boolean isField(Element element) {
return element != null && element.getKind().isField();
}
public static String getPackage(TypeMirror typeMirror) {
final String[] split = typeMirror.toString().split("\.");
return String.join(".", Arrays.copyOf(split, split.length - 1));
}
- Строка 6: Переменная
TypeMirror mirror
позволит нам в дальнейшем получить пакет аннотированного класса. - Строка 7:
annotatedElementName
это имя аннотированного класса. Потом в строке 9 мы к нему добавляем нашpostfix
. - 8: Мы получаем нашу аннотацию с параметрами настройки.
- 11-19: Проходим по всем элементам аннотированного класса, находим только поля и преобразуем их в
FieldDto
. - 21-24: Складываем полученную информацию в новый класс
ClassDto
- 25: Предаем созданный класс в метод, который сгенерирует нам новый класс
EntityNameFields
. Обратите внимание, что мы так же передаем переменнуюprocessingEnv
, но нигде ее не создаем. Эта переменная классаAbstractProcessor
, от которого мы наследовали наш класс обработчик. Эта переменная поможет нам создать новый класс.
Рассмотрим класс генератор:
public class ClassCreator {
private ClassCreator() {
}
public static void record(ClassDto classDto, ProcessingEnvironment environment) {
JavaFileObject builderFile = null;
try {
builderFile = environment.getFiler().createSourceFile(classDto.getClassName());
} catch (IOException e) {
e.printStackTrace();
}
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
out.println("package " + classDto.getClassPackage() + ";");
out.println();
out.print("public class " + classDto.getClassName() + " {");
out.println();
out.println();
generateNames(classDto.getFields(), out);
out.println();
out.println("}");
out.println();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void generateNames(Set fields, PrintWriter out) {
for (FieldDto field : fields) {
out.println(" public static final String " + field.getFieldStringName() + " = "" + field.getFieldName() + "";");
}
}
}
- 8-13: Создают непосредственно физический файл формата .java.
- 15-34: Идет заполнение созданного файла данными.
- 16: Записываем имя пакета
- 18,23: Создаем пустой класс
- 21,30-31: Заполняем класс статическими переменными с именами полей аннотированного класса.
Проверка работы
Создадим новый проект, чтобы проверить работу нашей аннотации. В pom.xml указываем зависимость на нашу библиотеку.
<dependency>
<groupId>dev.struchkov.example</groupId>
<artifactId>create-annotation</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>
Создадим там класс TestEntity
, которую пометим нашей аннотацией.
@FieldNames
public class TestEntity {
private Long id;
private String title;
private String phoneNumber;
}
Теперь необходимо зарегистрировать обработчик аннотации в плагине maven-compiler
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.9.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>dev.struchkov.example</groupId>
<artifactId>create-annotation</artifactId>
<version>0.0.2-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Теперь запускаем ребилд проекта: “Build” -> “Rebuild Project”. Или можете запустить команду мавена: mvn clean compile
.
Заходим в папку target/generated-sources/annotations
. И видим там наш сгенерированный класс:
public class TestEntityFields {
public static final String ID = "id";
public static final String TITLE = "title";
public static final String PHONE_NUMBER = "phoneNumber";
}
Заключение
Мы разобрались что такое аннотация и как она выглядит. Также мы научились создавать свои собственные аннотации и обработчики к ним.
- →
Аннотации появились в Java 5 в 2004 году.
Это специальная форма синтаксических метаданных, которая может быть добавлена в исходный код. Аннотации используются для анализа кода, при компиляции или во время выполнения программы. Их можно применять к пакетам, классам, методам, переменным и параметрам.
Самая простая аннотация, применимая к классу, выглядит так:
@MyAnnotation
public class Foo {}
С момента появления языка Java возникла необходимость помечать, для выполнения тех или иных действий, определенным образом класс или иерархию классов. До Java 5 это делалось через интерфейсы без методов.
Этот вид интерфейса не похож ни на один другой. Он не определяет никаких контрактов между собой и реализующими его классами, т. к. всегда пуст. Поэтому он называется маркерным интерфейсом. Такие интерфейсы нужны для маркировки чего-либо для JVM, компилятора или какой-либо библиотеки.
Serializable и Cloneable — два примера маркерных интерфейсов, которые достались нам в наследство. Например, Serializable позволяет пометить класс, сообщая о том, что его экземпляры можно сериализовать. При этом перед сериализацией делается проверка на наличие имплементации этого интерфейса.
Пример маркерных интерфейсов из JDK
С появлением аннотаций необходимость в использовании маркерных интерфейсов хоть и отпала, но до сих пор повсеместно используется.
Пример интерфейса и аналогичной ему аннотации:
public class Foo implements MarkerInterface {} (1)
@MyAnnotation
public class Foo {} (2)
(1) Маркерный интерфейс
(2) Аннотация — эквивалент маркерного интерфейса
1.1 Интерфейсы определяют тип
По факту, маркерный интерфейс отмечает объект, реализующий какой-либо тип, что исключает ошибки на этапе компиляции.
Например, создадим интерфейс без методов MyMark и ряд классов: MarkedClass (реализует MyMark); NonMarkedClass; Main, в котором разместим метод test, принимающий на вход объект типа MyMark.
public interface MyMark {}
class MarkedClass implements MyMark {}
class NonMarkedClass {}
class Main {
public static void main(String[] args) {
MarkedClass marked = new MarkedClass();
NonMarkedClass nonMarked = new NonMarkedClass();
test(marked);
//test(nonMarked);
}
static void test(MyMark markedObject) {
System.out.println("Метод 'Test' успешно завершен!");
}
}
Код test (marked) успешно выполнится, поскольку класс объекта marked реализует интерфейс MyMark, что требуется для работы метода test (MyMark markedObject).
Если раскомментировать строку test (nonMarked), мы получим ошибку компиляции:
Ошибка вызвана тем, что требуемым типом для метода test() является MyMark, а мы передаем тип NonMarkedClass.
1.2 Интерфейс определяет тип для наследников класса
Если класс реализует интерфейс, то и все его наследники будут реализовывать этот интерфейс. Нельзя «отвязать» интерфейс от наследников.
В этом месте аннотации имеют преимущество, поскольку позволяют реализовать такой механизм «отвязывания». Но в этом есть и минус — проверка наличия маркера (аннотации) теперь проводится во время исполнения, а не во время компиляции, что чревато ошибками.
Рассмотрим следующий код (тут следует пояснить, что детальное объяснение, используемых в коде аннотаций, дается в последующих главах):
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.TYPE)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}
public interface MyMark {}
@MyAnnotation
class Parent implements MyMark {}
class Child extends Parent {}
class Main {
public static void main(String[] args) {
Parent parent = new Parent();
Child child = new Child();
testInterface(parent);
testInterface(child);
testAnnotation(parent);
testAnnotation(child);
}
public static void testInterface(MyMark markedObject) {
System.out.println("Метод 'TestInterface' успешно завершен!");
}
public static void testAnnotation(Object object) {
if (!object.getClass().isAnnotationPresent(MyAnnotation.class)) {
throw new RuntimeException("Объект не аннотирован аннотацией 'MyAnnotation'");
}
System.out.println("Метод 'testAnnotation' успешно завершен!");
}
}
Результат выполнения кода:
Вызов метода testAnnotation (child) на этапе исполнения генерирует исключение, сообщая, что объект не аннотирован аннотацией MyAnnotation, которой был аннотирован его родительский класс Parent. Для успешной компиляции классу Child также необходимо использовать MyAnnotation.
- Если требуется знать, могут ли методы принимать объекты каких-то классов, то такие классы удобнее пометить (реализовать) интерфейсами, так как ошибка выявится на этапе компиляции
- Если необходимо провести анализ метаданных класса, то использование аннотаций даёт больше возможностей, в том числе принимая во внимание возможность аннотаций иметь параметры. Однако в этом случае анализ аннотаций происходит во время исполнения кода
В прошлой главе вы смогли познакомиться с механизмом аннотаций в действии, а также с созданием собственной аннотации. Мы сознательно не описывали назначение тех или иных аннотаций, поскольку пример имел иную цель. В этой и последующих частях давайте рассмотрим детальнее определение и состав аннотации.
Итак, реализация базового определения аннотации имеет следующий вид:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@Documented
public @interface MyAnnotation {
String name() default "";
int value();
}
Начальный символ @ сообщает о наличии аннотации.
Кратко расшифруем каждую строку с аннотациями и что они определяют:
- @Retention: в каком жизненном цикле кода аннотация (тут и до конца абзаца речь про @MyAnnotation) будет доступна (в исходнике, в class-файле или во время выполнения)
- @Target: для какого элемента ее можно использовать (поле, класс, пакет и тд)
- @Inherited: позволяет реализовать наследование аннотаций родительского класса классом-наследником
- @Documented: аннотация будет помещена в сгенерированную документацию javadoc
- @interface: сообщает о том, что это аннотация
- Как значения параметров аннотации, так и значения по умолчанию, являются опциональными (в данном примере присутствует два параметра: name типа String со значением по умолчанию и value типа int).
Возможно, сейчас вам ничего не понятно, это нормально. Более детально эти аннотации будут рассмотрены далее.
Стоит заметить, что в аннотации можно перечислить несколько значений, если значения по умолчанию отсутствуют. При этом переменная, именуемая value, относится к специальным переменным. Значение value может использоваться без имени переменной, если другие значения отсутствуют. Например:
// Оба значения приведены, их именование обязательно
@MyAnnotation(name = "какое-то имя", value = 42)
public class MyType { ... }
// Присутствует только "value()", в качестве "name()" будет его значение по умолчанию
@MyAnnotation(value = 42)
public class MyType2 { ... }
// Если требуется только "value()", мы можем опустить имя
@MyAnnotation(42)
public class MyType3 { ... }
Закрепим полученные знания на примере. Создадим аннотацию JavaFileInfo, которая будет аннотировать классы и методы информацией об их авторах и версиях класса/метода:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JavaFileInfo {
String name() default "Igor M.";
String value() default "0.0";
}
Добавим аннотируемый класс DemoClass:
@JavaFileInfo("2.0")
public class DemoClass {
@JavaFileInfo(name = "Chi Max", value = "1.0")
public void printString() {}
}
Создадим класс Main, который при помощи рефлексии извлечет параметры нашей аннотации из DemoClass:
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws NoSuchMethodException, SecurityException {
Class<DemoClass> demoClassObj = DemoClass.class;
readAnnotationOn(demoClassObj);
Method method = demoClassObj.getMethod("printString");
readAnnotationOn(method);
}
static void readAnnotationOn(AnnotatedElement element) {
try {
System.out.println("nПоиск аннотаций в " + element.getClass().getName());
Annotation[] annotations = element.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof JavaFileInfo fileInfo) {
System.out.println("Автор: " + fileInfo.name());
System.out.println("Версия: " + fileInfo.value());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Результат выполнения программы:
3. Классификация аннотаций
Аннотации можно классифицировать по следующим признакам:
- аннотации для аннотаций:
- @Target
- аннотации типов:
- @Retention
- @Documented
- @Inherited
- @Repeatable
- аннотации для кода:
- @Override
- @Deprecated
- @SuppressWarnings
- @SafeVarargs
- @FunctionalInterface
- нативные аннотации
- аннотации, написанные программистом
Рассмотрим перечисленные классы аннотаций более детально.
4. Аннотации для аннотаций
Аннотации для аннотаций еще называют мета-аннотациями.
- @Target: указывает контекст, для которого применима аннотация
- @Retention: указывает, до какого шага во время компиляции аннотация будет доступна
- @Documented: указывает, что аннотация должна быть задокументирована с помощью javadoc
- @Inherited: позволяет реализовать наследование аннотаций родительского класса классом-наследником
- @Repeatable: указывает, что аннотация может быть использована повторно в том же месте
Рассмотрим перечисленные мета-аннотации более детально.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target
Обратите внимание, для определения мета-аннотации @Target используется мета-аннотация @Target.
@Target определяет контекст, для которого она применима (актуально для Java 15):
- ElementType.ANNOTATION_TYPE: применяется для определения другой аннотации
- ElementType.CONSTRUCTOR: применяется для определения конструктора
- ElementType.FIELD: применяется для определения поля, включая константы Enum
- ElementType.LOCAL_VARIABLE: применяется для определения локальной переменной
- ElementType.METHOD: применяется для определения метода
- ElementType.MODULE: применяется для определения модуля (с Java 9)
- ElementType.PACKAGE: применяется для определения пакета
- ElementType.PARAMETER: применяется для определения параметра
- ElementType.TYPE: применяется для определения класса, интерфейса (включая аннотируемый тип), Enum или record.
- ElementType.TYPE_PARAMETER: применяется для определения типа параметра (с Java
- ElementType.TYPE_USE: применяется для определения используемого типа (с Java
- ElementType.RECORD_COMPONENT: ассоциируется с records как компонент записи (с Java 14)
ElementType представляет собой Enum, обеспечивая простую классификацию возможных мест для размещения аннотаций в коде. В свою очередь, они делятся на контексты объявления, где аннотации применяются к объявлениям, и на контексты типов, где аннотации применяются к типам, используемые в объявлениях и выражениях.
Константы ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE и TYPE_PARAMETER соответствуют контекстам объявления. TYPE_USE соответствует контекстам типа, а также двум контекстам объявления: объявлениям типов (включая объявления типов аннотаций) и объявлениям параметров типа.
Например, аннотация, тип которой аннотирован с помощью @Target (ElementType.FIELD), может быть записан только как модификатор для объявления поля. В тоже время аннотация, тип которой аннотирован с помощью @Target (ElementType.TYPE_USE), может быть записана в типе поля, а также может выступать в качестве модификатора, например, для объявления класса.
Рассмотрим несколько примеров.
Пример 1: В этом примере @Target информирует о том, что определяемый аннотацией MetaAnnotationType тип сам по себе является мета-аннотацией и может быть использован только для аннотаций:
@Target(ElementType.ANNOTATION_TYPE)
public @interface MetaAnnotationType {
...
}
Ярким примером такого использования аннотации является определение самой аннотации @Target, показанное ранее.
Пример 2: @Target информирует о том, что объявленный ею тип предназначен исключительно для использования в качестве типа элемента в объявлениях сложных типов аннотаций:
@Target({})
public @interface MemberType {
...
}
Пример 3: Когда константа ElementType появляется более одного раза в аннотации @Target, возникает ошибка времени компиляции (compile-time error):
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
public @interface Bogus {
...
}
В последующем изложении материала вы будете встречать эту аннотацию постоянно, поэтому постарайтесь понять ее смысл как можно раньше.
До Java 8 аннотации можно было размещать только перед объявлением метода, класса, конструктора и т. д. В Java 8 добавилось еще одно место для размещения аннотаций — рядом с типом. Такой вид аннотации часто называют аннотацией типа. Теперь мы можем аннотировать возвращаемый методом тип, дженерики. Аннотации типов важны, поскольку улучшают систему типов Java и позволяют программным инструментам (например, IDE) автоматически выполнять дополнительные проверки типов во время компиляции.
Аннотация типа должна включать ElementType.TYPE_USE или/и ElementType.TYPE_PARAM в качестве «target». Пример:
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface TypeAnnotation { ... }
ElementType.TYPE_PARAMETER указывает, что аннотация TypeAnnotation может быть записана в объявлении переменной типа.
В тоже время, ElementType.TYPE_USE указывает, что аннотация может быть использована для любого типа (например, типов, появляющихся в объявлениях, дженериках и при преобразованиях типов).
Аннотацию @TypeAnnotation необходимо разместить перед аннотируемым типом:
void method() throws @TypeAnnotation NullPointerException {...}
Другие возможные варианты применения аннотации типов:
@NotNull String str = getValue(args);
@Encrypted String str;
@Format(theFormatterConstant) String str;
@Localized String str;
List<@ReadOnly T> list;
Store<@NotNull Product> product;
Store<@Prod(Type.Grocery) Product> product;
void showResources(@Authenticated User user);
@SwingElementOrientation int orientation;
@Positive int i;
@CreditCard string cardNumber;
Date date = (@Readonly Date) object;
Date date = (@NotNull Date) object;
В языке Java отсутствуют встроенные аннотации типов, но мы можем создать их самостоятельно, а также написать свой обработчик аннотаций и подключить его к компилятору для проверки аннотированного кода. При этом генерируя на основе созданных нами правил ошибки или предупреждения, если код им не соответствует.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention
Аннотация @Retention (с англ. означает удержание, задержка) определяет, до какого шага во время компиляции аннотация будет доступна. Все шаги (они еще называются политиками) находятся в Enum:
- RetentionPolicy.SOURCE: аннотация сохраняется только в исходном файле и удаляется во время компиляции
- RetentionPolicy.CLASS: аннотация сохраняется в файле .class во время компиляции, но недоступна во время выполнения через JVM
- RetentionPolicy.RUNTIME: аннотация сохраняется в файле .class во время компиляции и доступна через JVM во время выполнения
В случае отсутствия аннотации @Retention по умолнчанию будет использована политика RetentionPolicy.CLASS.
Опишем аннотацию в RetentionAnnotation.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RetentionAnnotation {}
Создадим файл AnnotatedClass. java, аннотированный двумя аннотациями:
@RetentionAnnotation
@Deprecated
public class AnnotatedClass {}
Создадим и запустим файл Main.java:
import java.lang.annotation.Annotation;
public class Main {
public static void main(String[] args) {
AnnotatedClass anAnnotatedClass = new AnnotatedClass();
Annotation[] annotations = anAnnotatedClass.getClass().getAnnotations();
System.out.println("Общее кол-во аннотаций времени исполнения (RunTime): " + annotations.length);
System.out.println("1: " + annotations[0]);
System.out.println("2: " + annotations[1]);
}
}
Результат выполнения программы:
В этом примере мы создали свою собственную аннотацию RetentionAnnotation, а также использовали аннотацию @Deprecated, которая также имеет политику RetentionPolicy.RUNTIME.
Если мы исправим политику аннотации RetentionAnnotation с RetentionPolicy.RUNTIME на RetentionPolicy.SOURCE (и закомментируем строку в классе Main, выводящую второй элемент массива), то программа отобразит только одну аннотацию deprecated, поскольку аннотация с RetentionPolicy.SOURCE во время компиляции будет удалена.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented
По умолчанию аннотации не включаются в javadoc. Аннотация, помеченная @Documented информирует, что такая аннотация должна быть задокументирована с помощью инструмента javadoc.
Рассмотрим пример использования @Documented.
Создадим аннотацию @TestDocumented, используя @Documented:
import java.lang.annotation.Documented;
@Documented
public @interface TestDocumented {
String doTestDocument();
}
Создадим аннотацию @TestNotDocumented, и не пометим её какой-либо аннотацией:
public @interface TestNotDocumented {
String doTestNoDocument();
}
Теперь создадим класс Tester, пометив в нем два метода, созданными ранее аннотациями:
public class Tester {
@TestDocumented(doTestDocument = "Hello DOC with annotation")
public void doSomeTestDocumented() {
System.out.println("Test for annotation with 'Documented'");
}
@TestNotDocumented(doTestNoDocument = "Hello DOC without annotation")
public void doSomeTestNotDocumented() {
System.out.println("Test for annotation without 'Documented'");
}
}
Теперь, если вы запустите команду javadoc (или используете IntellijIdea: Tools -> Generate JavaDoc…) и просмотрите сгенерированный файл Tester.html, то увидите следующее (представлена часть видимого экрана):
Как видно на скриншоте, для метода doSomeTestNotDocumented() отсутствует информация о типе аннотации, но эта информация предоставляется для метода doSomeTestDocumented(). Причина этому @Documented, прикрепленная к нашей аннотации @TestDocumented. @TestNotDocumented не использует эту аннотацию.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited
В данном примере @Inherited может использоваться только для аннотирования класса.
По умолчанию аннотации, примененные к родительскому классу, не будут доступны в наследуемом классе. Если мы хотим, чтобы аннотации также наследовались, родительский класс необходимо пометить аннотацией @Inherited: в этом случае все аннотации родительского класса будут применимы к наследникам.
Рассмотрим пример использования @Inherited.
Создадим наследуемую аннотацию @InheritantAnnotation:
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InheritantAnnotation {}
Создадим не «наследуемую» аннотацию @NonInheritantAnnotation:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface NonInheritantAnnotation {}
Создадим родительский класс Parent, применив к нему две аннотации:
@InheritantAnnotation
@NonInheritantAnnotation
public class Parent {}
Создадим наследника Parent, класс Child:
public class Child extends Parent {}
Что мы получили сейчас: в классе Parent применены две аннотации (одна из них наследуемая), а в классе Child аннотации явно отсутствуют, но неявно присутствует унаследованная от родительского класса аннотация @InheritantAnnotation.
Используем перечисленные выше классы в Main и запустим его:
import java.lang.annotation.Annotation;
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
Child child = new Child();
if(parent.getClass().getAnnotations().length > 0) {
System.out.println("Для класса 'Parent' применены следующие аннотации: ");
for(Annotation annotationName: parent.getClass().getAnnotations()) {
System.out.println(annotationName);
}
} else {
System.out.println("К классу 'Parent' не применены какие-либо аннотации.");
}
if(child.getClass().getAnnotations().length > 0) {
System.out.println("nДля класса 'Child' применены следующие аннотации: ");
for(Annotation annotationName: child.getClass().getAnnotations()) {
System.out.println(annotationName);
}
} else {
System.out.println("nК классу 'Child' не применены какие-либо аннотации.");
}
}
}
Результат выполнения программы:
Как говорится, что и требовалось доказать.
В коде класса Main используется рефлексия (Reflection), что может усложнить его понимание. Поскольку целью этой статьи является ознакомление вас с аннотациями, то механизм рефлексии в ней не рассматривается.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable
Иногда возникают ситуации, когда необходимо повторно применить одну и ту же аннотацию к какому-то элементу (объявлению класса, интерфейсу, полю или к используемому типу).
До Java 8 применялось группирование аннотаций в единый контейнер аннотаций. Выглядело это следующим образом.
Определим повторяемую аннотацию Game:
@interface Game {
String name() default "Что-то под вопросом";
String day();
}
Определим контейнер Games:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@interface Games {
Game[] value();
}
@Games({
@Game(name = "Крикет", day = "воскресенье"),
@Game(day = "четверг"),
@Game(name = "Хоккей", day = "понедельник")
})
public class Main {
public static void main(String[] args) {
Games games = Main.class.getAnnotation(Games.class);
for (Game game : games.value()) {
System.out.println(game.name() + " в " + game.day());
}
}
}
Обратите внимание, повторяющиеся аннотации разделяются запятой.
Результат выполнения программы:
С появлением Java 8 и @Repeatable все стало немного проще.
В поле value такой аннотации необходимо указать контейнер для повторяющейся аннотации. Контейнер также представляет собой аннотацию, в которой поле value является массивом типа повторяющейся аннотации.
Таким образом, мы должны создать контейнерную аннотацию, а затем указать её тип в качестве аргумента.
В нашем случае, перед определением аннотации @Game необходимо добавить новую аннотацию @Repeatable:
import java.lang.annotation.Repeatable;
@Repeatable(Games.class)
@interface Game {
String name() default "Что-то под вопросом";
String day();
}
Теперь перед определением класса Main можно применить несколько раз аннотацию @Game:
@Game(name = "Крикет", day = "воскресенье")
@Game(day = "четверг")
@Game(name = "Хоккей", day = "понедельник")
public class Main {
public static void main(String[] args) {
Games games = Main.class.getAnnotation(Games.class);
for (Game game : games.value()) {
System.out.println(game.name() + " в " + game.day());
}
}
}
Результат выполнения программы тот же:
Мы также можем вместо getAnnotation (Games.class) использовать getAnnotationsByType (Game.class) или getDeclaredAnnotationsByТуре (Game.class):
@Game(name = "Крикет", day = "воскресенье")
@Game(day = "вторник")
@Game(name = "Хоккей", day = "пятница")
public class Main {
public static void main(String[] args) {
Game[] arrayGames = Main.class.getAnnotationsByType(Game.class);
for(Game game : arrayGames) {
System.out.println(game.name() + " в " + game.day());
}
}
}
- @Override: указывает, что метод переопределяет, объявленный в суперклассе или интерфейсе метод
- @Deprecated: помечает код, как устаревший
- @SuppressWarnings: отключает для аннотированного элемента предупреждения компилятора. Обратите внимание, что если необходимо отключить несколько категорий предупреждений, их следует добавить в фигурные скобки, например @SuppressWarnings ({«unchecked», «cast»}).
- @SafeVarargs: отключает предупреждения для всех методов или конструкторов, принимающих в качестве параметра varargs
- @FunctionalInterface: помечает интерфейсы, имеющие только один абстрактный метод (при этом они могут содержать любое количество методов по умолчанию или статических)
Рассмотрим перечисленные аннотации более детально.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override
Аннотация @Override относится к маркерным аннотациям и указывает, что метод переопределяет/реализует унаследованный метод. Эта информация не является строго необходимой, но помогает уменьшить количество ошибок, поскольку при такой аннотации компилятор должен генерировать сообщение об ошибке, если не выполняется одно из двух следующих условий:
— Метод переопределяет или реализует метод, объявленный в супертипе
— У метода есть сигнатура (название метода + список параметров), эквивалентная переопределяемой сигнатуре метода, объявленного в родительском классе/интерфейсе.
Продемонстрируем применение аннотации. Создадим класс Parent с методом display(), класс Child, который является его наследником, и класс Main, который создает экземпляр Child и запускает метод display():
public class Parent {
public void display() {
System.out.println("Выполнился метод из родительского класса");
}
}
public class Child extends Parent {
public void display() {
System.out.println("Выполнился метод из класса-наследника");
}
}
public class Main {
public static void main(String args[]) {
Child instance = new Child();
instance.display();
}
}
Результат выполнения программы:
Давайте умышленно добавим ошибку в названии метода в классе Child:
public class Child extends Parent {
public void dispay() {
System.out.println("Выполнился метод из класса-наследника");
}
}
Результат выполнения программы:
В итоге в классе Child мы имеем два метода: унаследованный метод суперкласса display() и новый метод dispay(). В классе Main у нас вызывается именно родительский метод, поскольку другого метода display() в классе Child нет.
Перед определением метода в класс Child добавим аннотацию @Override:
public class Child extends Parent {
@Override
public void dispay() {
System.out.println("Выполнился метод из класса-наследника");
}
}
В такой ситуации IDE подчеркнет красным аннотацию, информируя, что «Method does not override method from its superclass» (метод не переопределяет метод его суперкласса).
При запуске получим ошибку компиляции:
Теперь уже компилятор сообщает нам, что «метод не переопределяет или не реализует метод его суперкласса»
Исправим «опечатку» в названии метода в классе Child и запустим программу:
public class Child extends Parent {
@Override
public void display() {
System.out.println("Выполнился метод из класса-наследника");
}
}
Результат выполнения программы:
Таким образом, применяя аннотацию @Override, мы даем задание компилятору выполнять проверку соответствия сигнатуры метода класса наследника классу родителя, что устраняет ошибки «по невнимательности» в виде опечаток.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated
Определяющие аннотацию аннотации мы рассмотрели ранее, тут для вас не должно быть трудностей. Приведем примеры использования.
В код предыдущего примера добавим в класс Child аннотацию @Deprecated:
public class Child extends Parent {
@Override
@Deprecated(since = "1.2", forRemoval = true)
public void display() {
System.out.println("Выполнился метод из класса-наследника");
}
}
Результат выполнения программы:
Результат остался тем же, ошибок нет. Но, обратите внимание на класс Main, используемый метод display() в IntellijIdea перечеркнут (!). Подобные визуальные оповещения есть и в других IDE.
public class Main {
public static void main(String args[]) {
Child instance = new Child();
instance.display();
}
}
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings
Предупреждающие сообщения компилятора обычно полезны, но иногда, они могут «зашумлять» полезную информацию. Особенно, когда мы не можем или не хотим их устранять. В таких случаях можно воспользоваться аннотацией @SuppressWarnings, отключив такие предупреждения, чтобы они не отображались.
Рассматривая код для аннотации @Override, мы вызывали в классе Main метод display() из класса Child. В тоже время метод display() из класса Parent не использовался. Среда IDE предполагала, что здесь где-то может быть ошибка (создали лишний метод или ошибочно используем не тот метод и т. д.) и соответственно, подсвечивая, выделяла цветом название неиспользуемого метода display() (и при наведении курсора выдавала сообщение: «Method ‘Display()’ is never used»).
Чтобы этого небыло, такое предупреждение можно отключить аннотацией @SuppressWarnings («unused»), установив её перед методом display():
public class Parent {
@SuppressWarnings("unused")
public void display() {
System.out.println("Выполнился метод из родительского класса");
}
}
Еще одним предупреждением компилятора является предупреждение о применении устаревшего метода, помеченного в коде аннотацией @Deprecated. Чтобы его устранить, необходимо пометить вызов метода main() в классе Main аннотацией @SuppressWarnings («deprecation»):
public class Main {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Child instance = new Child();
instance.display();
}
}
Сам код теперь стал проще для чтения, а название метода display() не перечеркивается.
Чтобы отключить список из нескольких предупреждений, необходимо через запятую перечислить список предупреждений.
Например, в следующем виде:
@SuppressWarnings({"unused", "deprecation"})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs
Эта аннотация, представленная в java 7, гарантирует, что тело аннотированного метода или конструктора не выполняет потенциально небезопасные операции с параметром varargs. Аннотация @SafeVarargs похожа на @SupressWarnings тем, что позволяет нам объявить, что конкретное предупреждение компилятора является ложным срабатыванием. Добавлять эту аннотацию мы можем только убедившись в том, что наши действия безопасны.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface
Как и аннотация @Override, аннотация @FunctionalInterface защищает код от возможных ошибок программиста. Несмотря на то, что любой интерфейс может содержать бесконечное множество абстрактных методов, функциональный интерфейс может содержать исключительно один абстрактный метод, иначе он не сможет использоваться в лямбда-выражении.
В тоже время, абстрактные методы, переопределяющие один из публичных методов класса Object, не учитываются.
Рассмотрим простейший пример: напишем функциональный интерфейс, реализующий что-то абстрактное и важное.
@FunctionalInterface
public interface MyFunctionalInterface {
abstract public void abstractMethod();
//abstract public void anotherAbstractMethod();
}
public class Main implements MyFunctionalInterface {
public static void main(String[] args) {
Main main = new Main();
main.abstractMethod();
}
@Override
public void abstractMethod() {
System.out.println("Это сообщение из abstractMethod()");
}
}
Если мы добавим еще один абстрактный метод (anotherAbstractMethod(), в коде он закомментирован), компилятор сообщит про ошибку:
Ошибка сообщает, что наш интерфейс functionalInterface не является функциональным интерфейсом и включает в себя несколько не переопределенных абстрактных методов.
Создадим еще один функциональный интерфейс и расширим им интерфейс, созданный нами ранее:
package functional;
@FunctionalInterface
public interface AnotherFunctionalInterface extends MyFunctionalInterface {
abstract public void anotherAbstractMethod();
}
Вроде все хорошо, у нас один абстрактный метод, но IDE снова подсказывает о наличии той же самой ошибки:
Ошибка вызвана тем, что мы, расширяя интерфейс MyFunctionalInterface, наследуем абстрактный метод расширяемого интерфейса, и как результат, имеем два абстрактных метода, что не совместимо с аннотацией @FunctionalInterface.
Таким образом, мы не сможем использовать интерфейс, помеченный аннотацией @FunctionalInterface и включающей в себя явно или неявно два и более абстрактных метода.
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native
Начиная с Java 8, в пакете java.lang.annotation появилась новая аннотация под названием @Native, применимая только к полям. Она указывает, что аннотированное поле является константой, на которую можно ссылаться с нативного кода. Например, вот как она используется в классе Integer:
public final class Integer {
@Native public static final int MIN_VALUE = 0x80000000;
// последующий код опущен
}
Эта аннотация также может служить подсказкой для программных инструментов, генерирующих некоторые вспомогательные файлы.
В этой главе мы поговорим об обработке аннотаций. И рефлексия, и обработчики аннотаций, затрагиваемые здесь, заслуживают отдельной, полноценной статьи не меньшего размера, чем текущая. Частично применение рефлексии рассматривалось в примерах использования аннотаций по тексту выше, но это был исключительно необходимый минимум для демонстрации использования аннотаций.
7.1 Обработка аннотаций во время выполнения: рефлексия
Рефлексия, как способность получать информацию о коде во время его выполнения, появилась в Java с момента появления языка.
var session = request.getHttpSession();
var object = session.getAttribute("object"); (1)
var clazz = object.getClass(); (2)
var methods = clazz.getMethods(); (3)
for (var method : methods) {
if (method.getParameterCount() == 0) { (4)
method.invoke(foo); (5)
}
}
(1) Получаем объект, хранящийся в сеансе
(2) Получаем класс среды выполнения (runtime class) объекта
(3) Получаем все общедоступные методы, имеющиеся у объекта
(4) Если у метода нет параметра, то
(5) Вызываем этот метод
С появлением аннотаций рефлексия получила соответствующие улучшения:
Фреймворки начали использовать аннотации для различных сценариев использования. Среди них сценарий конфигурирования был одним из наиболее часто используемых: например, вместо (или, точнее, в дополнение к) XML, Spring добавил возможность конфигурирования на основе аннотаций.
7.2 Обработка аннотаций во время компиляции: обработчики аннотаций
Долгое время и получатели данных, и поставщики данных были довольны доступом через рефлексию к аннотациям во время выполнения. Поскольку основное внимание уделяется сценариям конфигурирования, рефлексия происходит во время запуска приложения. В ограниченном по производительности окружении это слишком большая нагрузка для приложений: наиболее известным примером такого окружения является платформа Android. Здесь хотелось бы иметь самое быстрое время запуска, но рефлексия замедляет его.
Альтернативным решением этой проблемы является обработка аннотаций во время их компиляции. Для этого компилятор должен быть настроен на использование специальных обработчиков аннотаций. У них могут быть разные выходные данные: простые файлы, сгенерированный код и т. д. Компромисс этого подхода заключается в том, что компиляция приложения каждый раз снижает производительность, но при этом не влияет на время запуска.
Одним из первых фреймворков, в которых использовался этот подход для генерации кода, был Dagger: это DI-фреймворк (Dependency Injection) для Android. Работа фреймворка базируется не на времени выполнения (runtime-based), а на времени компиляции (compile-time). Долгое время генерация кода во время компиляции использовалась только в экосистеме Android.
Однако, в последнее время такой подход приняли и такие серверные фреймворки, как Quarkus и Micronaut. Цель состоит в том, чтобы сократить время запуска приложения за счет генерации кода во время компиляции вместо самоанализа во время выполнения. Кроме того, предварительная компиляция полученного байт-кода в собственный код дополнительно сокращает время запуска, а также потребление памяти.
Мир обработчиков аннотаций огромен: этот раздел представляет собой очень небольшое введение, поэтому при желании можно продолжить их изучение.
Обработчик аннотаций — это просто определенный класс, который необходимо зарегистрировать во время компиляции. Зарегистрировать их можно несколькими способами. С Maven это просто вопрос настройки плагина компилятора:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessors>
<annotationProcessor>ch.frankel.blog.SampleProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
Сам обработчик должен реализовывать Processor, но абстрактный класс AbstractProcessor самостоятельно реализует большую часть своих методов, кроме метода process(): на практике достаточно наследоваться от AbstractProcessor. Очень упрощенная схема API выглядит так:
Давайте создадим очень простой обработчик. Он должен перечислять только классы, помеченные конкретными аннотациями. Настоящие процессоры аннотаций, скорее всего, сделают что-нибудь полезное, например сгенерируют код, но эта дополнительная логика выходит далеко за рамки этого поста.
@SupportedAnnotationTypes("ch.frankel.blog.*") (1)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SampleProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, (2)
RoundEnvironment env) {
annotations.forEach(annotation -> { (3)
Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation); (4)
elements.stream()
.filter(TypeElement.class::isInstance) (5)
.map(TypeElement.class::cast) (6)
.map(TypeElement::getQualifiedName) (7)
.map(name -> "Class " + name + " is annotated with " + annotation.getQualifiedName())
.forEach(System.out::println);
});
return true;
}
}
(1) Обработчик будет вызываться для каждой аннотации, принадлежащей пакету ch.frankel.blog
(2) process(): основной метод, подлежащий переопределению
(3) Цикл вызывается для каждой аннотации
(4) Аннотация не так интересна, как аннотированный ею элемент. Это способ получить аннотированный элемент
(5) В зависимости от того, какой элемент аннотирован, его необходимо привести к правильному дочернему интерфейсу Element. Здесь могут быть аннотированы только классы, поэтому, переменная должна быть протестирована, чтобы проверить, имеет ли назначаемый TypeElement доступ к своим дополнительным атрибутам далее по цепочке операций
(6) Нам нужно полное имя класса, для которого установлена аннотация, поэтому необходимо привести его к типу, который делает этот конкретный атрибут доступным
(7) получаем полное имя от TypeElement
Аннотации очень эффективны и не важно, используются ли они во время выполнения или во время компиляции. С другой стороны, самая большая проблема заключается в том, что они работают как будто по волшебству: нет простого способа узнать, какой класс, использующий рефлексию, или обработчик аннотаций их использует. Каждый в своем контексте должен решать, перевешивают ли плюсы аннотаций их минусы. Использование аннотаций без каких-либо предположений о будущем программы оказывает большую медвежью услугу такому коду… так же, как и отказ от них из-за неуместной идеологии.
Автор: Малянов Игорь
Технический редактор: Чимаев Максим
Оцените статью, если она вам понравилась!