Аннотации — это специальные метки, которые помогают программистам взаимодействовать с компилятором и средствами разработки, указывая, как обращаться с определёнными участками кода, помимо его непосредственного исполнения.
В языке Java аннотации могут применяться к переменным, параметрам, классам и пакетам. Разработчики могут как создавать собственные аннотации, так и использовать уже существующие, встроенные в Java.
Все аннотации начинаются с символа @
. Одной из самых известных аннотаций является @Override
, с которой сталкивался, вероятно, каждый Java-разработчик. Эта аннотация сообщает компилятору о том, что метод был переопределён из родительского класса. Если метод родительского класса будет изменён или удалён, компилятор оповестит о наличии ошибки. Рассмотрим пример:
class SomeClass {
void method() {
System.out.println("Работает метод родительского класса.");
}
}
class AnotherClass extends SomeClass { // наследуем методы SomeClass в новом классе
@Override
void method() { // переопределяем метод
System.out.println("Работает метод класса-потомка.");
}
}
В случае, если в имени метода из класса AnotherClass
будет допущена ошибка (например, “methood
”), аннотация @Override
позволит компилятору выявить проблему и сообщить о несоответствии. Без этой аннотации компилятор не заметил бы ошибку и просто создал бы новый метод, не связанный с родительским method
.
Важно отметить, что сама аннотация @Override
не влияет на процесс переопределения метода. Её роль заключается в контроле корректности переопределения на этапе компиляции или сборки. Таким образом, аннотация защищает код от трудноуловимых ошибок, на поиск которых в большом проекте могло бы уйти много времени. Это лишь один пример полезного применения аннотаций в Java.
Структура аннотации
Создание аннотаций в Java похоже на создание интерфейсов, с той разницей, что вместо ключевого слова interface
используется @interface
. Например:
public @interface MyAnnotation {
String name() default "";
int value();
}
Параметры аннотации задаются так же, как методы интерфейсов, но без аргументов. Ключевое слово default
указывает на то, что метод будет возвращать значение по умолчанию, если оно не задано явно при использовании аннотации.
Ограничение области применения
Если аннотация не сконфигурирована, она может применяться ко всему: к классам, методам, атрибутам и т.д. Чтобы ограничить область применения аннотации, используется аннотация @Target
, которая определяет, где её можно применять:
@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({ ElementType.PARAMETER, ElementType.LOCAL_VARIABLE })
Другие полезные аннотации
Помимо @Target
, существуют и другие аннотации для настройки поведения аннотаций:
@Retention
определяет, на каком этапе жизненного цикла программы аннотация будет доступна:
SOURCE
- аннотация доступна только в исходном коде и удаляется при компиляции;CLASS
- аннотация сохраняется в.class
файле, но недоступна во время выполнения программы;RUNTIME
- аннотация сохраняется в.class
файле и доступна во время выполнения программы.
@Inherited
позволяет наследовать аннотации от родительского класса. Например:
@Inherited
public @interface MyAnnotation { }
@MyAnnotation
public class MySuperClass { ... }
public class MySubClass extends MySuperClass { ... }
В этом примере класс MySubClass
наследует аннотацию @MyAnnotation
от класса MySuperClass
.
@Documented
указывает, что аннотация должна быть включена в сгенерированную документацию JavaDoc.
Обработчик аннотации
Аннотации сами по себе не обладают функциональностью — их нужно дополнить обработчиком, который будет обрабатывать их на этапе компиляции.
Одним из основных ограничений аннотаций является то, что они не могут изменять существующие классы — можно только создавать новые.
Исключением является проект Lombok, аннотации которого изменяют существующие классы, например, добавляют геттеры, сеттеры и конструкторы.
Рассмотрим пример на практике. Создадим аннотацию @FieldNames
, которая будет генерировать новый класс с именами полей существующего класса. Пример:
public class Simple {
private String text;
private Integer number;
private Long numberTwo;
}
Аннотация @FieldNames
должна сгенерировать следующий класс:
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
упрощает создание манифеста для обработчика. Чтобы использовать её, необходимо добавить зависимость:
Метод process
должен реализовать логику обработки аннотации. Добавим код для получения классов, помеченных аннотацией @FieldNames
:
@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-класс.
- На основе полученных данных сгенерировать новый класс.
Для этого создадим два вспомогательных класса. ClassDto
будет содержать информацию о классе, а FieldDto
— данные о полях:
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
, нам понадобится ещё одна зависимость:
Ключевая часть работы обработчика аннотаций — это метод process
. Этот метод отвечает за сбор информации о классе, который помечен аннотацией, и за создание нового класса. Рассмотрим по шагам:
@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: Переменная mirror содержит информацию о типе класса, что поможет нам извлечь информацию о пакете.
- Строка 7:
annotatedElementName
это имя аннотированного класса. Потом в строке 9 мы к нему добавляем нашpostfix
. - Строка 8: Получаем саму аннотацию FieldNames и её параметры.
- Строки 11-19: Проходим по всем элементам класса, фильтруем только поля и преобразуем их в объекты
FieldDto
. При этом имена полей преобразуются из форматаcamelCase
вUPPER_UNDERSCORE
. - 21-24: Создаём объект
ClassDto
, который содержит всю необходимую информацию для генерации нового класса. Затем вызываем методClassCreator.record
, который создаст новый класс. - 25: Предаем созданный класс в метод, который сгенерирует нам новый класс
EntityNameFields
. Обратите внимание, что мы так же передаем переменнуюprocessingEnv
, но нигде ее не создаем. Эта переменная классаAbstractProcessor
, от которого мы наследовали наш класс обработчик. Эта переменная поможет нам создать новый класс.
Теперь рассмотрим класс ClassCreator
, который отвечает за создание и запись нового .java файла:
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 с помощью метода createSourceFile класса Filer, который предоставляется средой обработки аннотаций (processingEnv).
- 15-34: Идет заполнение созданного файла данными.
- 16: Записываем имя пакета в файл.
- 18,23: Создаём новый класс, добавляем в него статические поля.
- 21,30-31: Метод generateNames заполняет класс статическими переменными на основе данных из
FieldDto
. Для каждого поля генерируем строку видаpublic static final String FIELD_NAME = "fieldName";
.
Проверка работы
Для проверки работы нашей аннотации, создадим новый проект и настроим его для использования созданной библиотеки и обработчика аннотаций.
В файл pom.xml
нового проекта необходимо добавить зависимость на нашу библиотеку, которая содержит аннотацию и обработчик:
<dependency>
<groupId>dev.struchkov.example</groupId>
<artifactId>create-annotation</artifactId>
<version>0.0.2-SNAPSHOT</version>
</dependency>
mvn clean install
для проекта, содержащего аннотацию и обработчик.Создадим класс TestEntity
, который будет помечен аннотацией @FieldNames
:
@FieldNames
public class TestEntity {
private Long id;
private String title;
private String phoneNumber;
}
Чтобы Maven мог использовать наш обработчик аннотаций, нужно настроить плагин maven-compiler-plugin
. Для этого добавляем следующую конфигурацию в pom.xml
:
<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>
Теперь можно запустить ребилд проекта:
- В IDE: выберите опцию “Build” -> “Rebuild Project”.
- Через Maven: выполните команду
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";
}
Этот класс содержит статические константы, которые соответствуют именам полей класса TestEntity
, преобразованные в формат UPPER_UNDERSCORE
.
Заключение
Мы разобрались, что такое аннотации и как они работают в Java. Мы научились создавать собственные аннотации и обработчики, а также проверили их работу на практике. Использование аннотаций и обработчиков позволяет автоматизировать задачи генерации кода, делая разработку более удобной и эффективной.