Содержание
Backend Java Developer
Backend Java Developer

Аннотации – это своеобразные маркеры, с помощью которых программист указывает компилятору 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</version>
    <scope>provided</scope>
</dependency>

Как вы можете видеть нам необходимо реализовать метод process.

Дебажить обработчик обычным способом у вас не получится. Как дебажить обработчики я писал в отдельной статье: Debugging an annotation processor using IntelliJ IDEA

Нам необходимо получить все классы, которые помечены нашей аннотацией.


@Override
public boolean process(Set set, RoundEnvironment roundEnvironment) {
    for (TypeElement annotation : set) {
        Set 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>30.1.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: Заполняем класс статическими переменными с именами полей аннотированного класса.

Чтобы проверить работу нашей аннотации, создадим новый проект, в который добавим нашу библиотеку в виде зависимости.

Создадим там класс TestEntity, которую пометим нашей аннотацией.


@FieldNames
public class TestEntity {

    private Long id;
    private String title;
    private String phoneNumber;

}

Сгенерированный класс

Теперь запускаем ребилд проекта: “Build” -> “Rebuild Project”.

Заходим в папку 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 аннотаций.

Комментарии