«1. Введение

Эта статья представляет собой введение в обработку аннотаций на уровне исходного кода Java и предоставляет примеры использования этого метода для создания дополнительных исходных файлов во время компиляции.

2. Применение обработки аннотаций

Обработка аннотаций на уровне исходного кода впервые появилась в Java 5. Это удобный метод создания дополнительных исходных файлов на этапе компиляции.

Исходные файлы не обязательно должны быть файлами Java — вы можете создавать любые описания, метаданные, документацию, ресурсы или файлы любого другого типа на основе аннотаций в исходном коде.

Обработка аннотаций активно используется во многих вездесущих библиотеках Java, например, для генерации метаклассов в QueryDSL и JPA, для дополнения классов шаблонным кодом в библиотеке Lombok.

Важно отметить ограничение API обработки аннотаций — его можно использовать только для создания новых файлов, а не для изменения существующих.

Заметным исключением является библиотека Lombok, которая использует обработку аннотаций в качестве механизма начальной загрузки, чтобы включить себя в процесс компиляции и изменить AST через некоторые внутренние API-интерфейсы компилятора. Этот хакерский метод не имеет ничего общего с предполагаемой целью обработки аннотаций и поэтому не обсуждается в этой статье.

3. API обработки аннотаций

Обработка аннотаций выполняется в несколько этапов. Каждый раунд начинается с того, что компилятор ищет аннотации в исходных файлах и выбирает обработчики аннотаций, подходящие для этих аннотаций. Каждый процессор аннотаций, в свою очередь, обращается к соответствующим источникам.

Если во время этого процесса сгенерированы какие-либо файлы, запускается другой раунд с сгенерированными файлами в качестве входных данных. Этот процесс продолжается до тех пор, пока на этапе обработки не будут созданы новые файлы.

Каждый обработчик аннотаций, в свою очередь, обращается к соответствующим источникам. Если во время этого процесса генерируются какие-либо файлы, запускается другой раунд с генерируемыми файлами в качестве входных данных. Этот процесс продолжается до тех пор, пока на этапе обработки не будут созданы новые файлы.

API обработки аннотаций находится в пакете javax.annotation.processing. Основной интерфейс, который вам нужно реализовать, — это интерфейс Processor, который имеет частичную реализацию в виде класса AbstractProcessor. Этот класс мы собираемся расширить, чтобы создать собственный процессор аннотаций.

4. Настройка проекта

Чтобы продемонстрировать возможности обработки аннотаций, мы разработаем простой процессор для создания быстрых компоновщиков объектов для аннотированных классов.

Мы собираемся разделить наш проект на два модуля Maven. Один из них, модуль annotation-processor, будет содержать сам процессор вместе с аннотацией, а другой, модуль annotation-user, будет содержать аннотированный класс. Это типичный пример использования обработки аннотаций.

Настройки модуля обработки аннотаций следующие. Мы собираемся использовать библиотеку автоматического обслуживания Google для создания файла метаданных процессора, который будет обсуждаться позже, и плагин maven-compiler-plugin, настроенный для исходного кода Java 8. Версии этих зависимостей извлекаются в раздел свойств.

Последние версии библиотеки автосервиса и плагина maven-compiler-plugin можно найти в центральном репозитории Maven:

<properties>
    <auto-service.version>1.0-rc2</auto-service.version>
    <maven-compiler-plugin.version>
      3.5.1
    </maven-compiler-plugin.version>
</properties>

<dependencies>

    <dependency>
        <groupId>com.google.auto.service</groupId>
        <artifactId>auto-service</artifactId>
        <version>${auto-service.version}</version>
        <scope>provided</scope>
    </dependency>

</dependencies>

<build>
    <plugins>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${maven-compiler-plugin.version}</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>

    </plugins>
</build>

Модуль Maven annotation-user с аннотированными исходниками не нуждается в какой-либо специальной настройке, кроме добавления зависимость от модуля обработчика аннотаций в разделе зависимостей:

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>annotation-processing</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

5. Определение аннотации

Предположим, у нас есть простой класс POJO в нашем модуле аннотации-пользователя с несколькими полями:

public class Person {

    private int age;

    private String name;

    // getters and setters …

}

Мы хотим создать вспомогательный класс построителя для более плавного создания экземпляров класса Person:

Person person = new PersonBuilder()
  .setAge(25)
  .setName("John")
  .build();

Этот класс PersonBuilder является очевидным выбором для поколения, поскольку его структура полностью определяется методами установки Person.

«Давайте создадим аннотацию @BuilderProperty в модуле обработчика аннотаций для методов установки. Это позволит нам сгенерировать класс Builder для каждого класса, у которого есть аннотированные методы установки:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}

Аннотация @Target с параметром ElementType.METHOD гарантирует, что эта аннотация может быть помещена только в метод.

Политика хранения SOURCE означает, что эта аннотация доступна только во время обработки исходного кода и недоступна во время выполнения.

Класс Person со свойствами, аннотированными аннотацией @BuilderProperty, будет выглядеть следующим образом:

public class Person {

    private int age;

    private String name;

    @BuilderProperty
    public void setAge(int age) {
        this.age = age;
    }

    @BuilderProperty
    public void setName(String name) {
        this.name = name;
    }

    // getters …

}

6. Реализация процессора

6.1. Создание подкласса AbstractProcessor

Мы начнем с расширения класса AbstractProcessor внутри модуля Maven процессора аннотаций.

Во-первых, мы должны указать аннотации, которые этот процессор способен обрабатывать, а также поддерживаемую версию исходного кода. Это можно сделать либо путем реализации методов getSupportedAnnotationTypes и getSupportedSourceVersion интерфейса процессора, либо путем аннотирования класса аннотациями @SupportedAnnotationTypes и @SupportedSourceVersion.

Аннотация @AutoService является частью библиотеки автоматического обслуживания и позволяет генерировать метаданные процессора, которые будут объяснены в следующих разделах.

@SupportedAnnotationTypes(
  "com.baeldung.annotation.processor.BuilderProperty")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
      RoundEnvironment roundEnv) {
        return false;
    }
}

Вы можете указать не только конкретные имена классов аннотаций, но и подстановочные знаки, такие как «com.baeldung.annotation.*», для обработки аннотаций внутри пакета com.baeldung.annotation и всех его подпакетов, или даже «*» для обработки всех аннотаций.

Единственный метод, который нам нужно реализовать, это метод процесса, который выполняет саму обработку. Он вызывается компилятором для каждого исходного файла, содержащего соответствующие аннотации.

Аннотации передаются как первый Set\u003c? расширяет аргумент аннотации TypeElement\u003e, а информация о текущем раунде обработки передается в качестве аргумента RoundEnviroment roundEnv.

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

6.2. Сбор данных

Наш процессор еще не делает ничего полезного, так что давайте заполним его кодом.

Во-первых, нам нужно перебрать все типы аннотаций, которые находятся в классе — в нашем случае набор аннотаций будет иметь один элемент, соответствующий аннотации @BuilderProperty, даже если эта аннотация встречается несколько раз. раз в исходном файле.

Тем не менее, для полноты картины лучше реализовать метод process как итерационный цикл:

@Override
public boolean process(Set<? extends TypeElement> annotations, 
  RoundEnvironment roundEnv) {

    for (TypeElement annotation : annotations) {
        Set<? extends Element> annotatedElements 
          = roundEnv.getElementsAnnotatedWith(annotation);
        
        // …
    }

    return true;
}

В этом коде мы используем экземпляр RoundEnvironment для получения всех элементов, аннотированных аннотацией @BuilderProperty. В случае класса Person эти элементы соответствуют методам setName и setAge.

Пользователь аннотации @BuilderProperty может ошибочно аннотировать методы, которые на самом деле не являются сеттерами. Имя метода установки должно начинаться с set, и метод должен получать один аргумент. Итак, давайте отделим зёрна от плевел.

В следующем коде мы используем сборщик Collectors.partitioningBy() для разделения аннотированных методов на две коллекции: правильно аннотированные сеттеры и другие ошибочно аннотированные методы:

Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
  Collectors.partitioningBy(element ->
    ((ExecutableType) element.asType()).getParameterTypes().size() == 1
    && element.getSimpleName().toString().startsWith("set")));

List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);

Здесь мы используем метод Element.asType() чтобы получить экземпляр класса TypeMirror, который дает нам некоторую возможность интроспекции типов, даже если мы находимся только на этапе обработки исходного кода.

Мы должны предупредить пользователя о неправильно аннотированных методах, поэтому воспользуемся экземпляром Messager, доступным из защищенного поля AbstractProcessor.processingEnv. Следующие строки будут выводить ошибку для каждого ошибочно аннотированного элемента на этапе обработки исходного кода:

otherMethods.forEach(element ->
  processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
    "@BuilderProperty must be applied to a setXxx method " 
      + "with a single argument", element));

Конечно, если правильная коллекция сеттеров пуста, нет смысла продолжать итерацию набора элементов текущего типа: ~~ ~

if (setters.isEmpty()) {
    continue;
}

«Если в коллекции сеттеров есть хотя бы один элемент, мы собираемся использовать его для получения полного имени класса из включающего элемента, который в случае метода сеттера оказывается самим исходным классом:

String className = ((TypeElement) setters.get(0)
  .getEnclosingElement()).getQualifiedName().toString();

~~ ~ Последняя часть информации, которая нам нужна для создания класса построителя, — это сопоставление между именами сеттеров и именами их типов аргументов:

Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(
    setter -> setter.getSimpleName().toString(),
    setter -> ((ExecutableType) setter.asType())
      .getParameterTypes().get(0).toString()
));

6.3. Генерация выходного файла

Теперь у нас есть вся информация, необходимая для создания класса построителя: имя исходного класса, все имена его сеттеров и их типы аргументов.

Для создания выходного файла мы будем использовать экземпляр Filer, снова предоставленный объектом в защищенном свойстве AbstractProcessor.processingEnv:

JavaFileObject builderFile = processingEnv.getFiler()
  .createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
    // writing generated file to out …
}

Полный код метода writeBuilderFile приведен ниже. Нам нужно только вычислить имя пакета, полное имя класса построителя и простые имена классов для исходного класса и класса построителя. Остальной код довольно прост.

private void writeBuilderFile(
  String className, Map<String, String> setterMap) 
  throws IOException {

    String packageName = null;
    int lastDot = className.lastIndexOf('.');
    if (lastDot > 0) {
        packageName = className.substring(0, lastDot);
    }

    String simpleClassName = className.substring(lastDot + 1);
    String builderClassName = className + "Builder";
    String builderSimpleClassName = builderClassName
      .substring(lastDot + 1);

    JavaFileObject builderFile = processingEnv.getFiler()
      .createSourceFile(builderClassName);
    
    try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {

        if (packageName != null) {
            out.print("package ");
            out.print(packageName);
            out.println(";");
            out.println();
        }

        out.print("public class ");
        out.print(builderSimpleClassName);
        out.println(" {");
        out.println();

        out.print("    private ");
        out.print(simpleClassName);
        out.print(" object = new ");
        out.print(simpleClassName);
        out.println("();");
        out.println();

        out.print("    public ");
        out.print(simpleClassName);
        out.println(" build() {");
        out.println("        return object;");
        out.println("    }");
        out.println();

        setterMap.entrySet().forEach(setter -> {
            String methodName = setter.getKey();
            String argumentType = setter.getValue();

            out.print("    public ");
            out.print(builderSimpleClassName);
            out.print(" ");
            out.print(methodName);

            out.print("(");

            out.print(argumentType);
            out.println(" value) {");
            out.print("        object.");
            out.print(methodName);
            out.println("(value);");
            out.println("        return this;");
            out.println("    }");
            out.println();
        });

        out.println("}");
    }
}

7. Запуск примера

Чтобы увидеть генерацию кода в действии, вы должны либо скомпилировать оба модуля из общего родительского корня, либо сначала скомпилировать модуль процессора аннотаций, а затем модуль пользователя аннотаций.

Сгенерированный класс PersonBuilder находится внутри файла annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java и должен выглядеть следующим образом:

package com.baeldung.annotation;

public class PersonBuilder {

    private Person object = new Person();

    public Person build() {
        return object;
    }

    public PersonBuilder setName(java.lang.String value) {
        object.setName(value);
        return this;
    }

    public PersonBuilder setAge(int value) {
        object.setAge(value);
        return this;
    }
}

8. Альтернативные способы регистрации процессора

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

8.1. Использование инструмента обработки аннотаций

Инструмент apt представлял собой специальную утилиту командной строки для обработки исходных файлов. Он был частью Java 5, но начиная с Java 7 он был объявлен устаревшим в пользу других опций и полностью удален в Java 8. В этой статье он обсуждаться не будет.

8.2. Использование ключа компилятора

Ключ компилятора -processor — это стандартное средство JDK, позволяющее дополнить стадию обработки исходного кода компилятором с помощью вашего собственного процессора аннотаций.

Обратите внимание, что сам процессор и аннотация должны быть уже скомпилированы как классы в отдельной компиляции и представлены в пути к классам, поэтому первое, что вы должны сделать, это:

javac com/baeldung/annotation/processor/BuilderProcessor
javac com/baeldung/annotation/processor/BuilderProperty

Затем вы выполняете фактическую компиляцию ваши исходники с ключом -processor, указывающим класс процессора аннотаций, который вы только что скомпилировали:

javac -processor com.baeldung.annotation.processor.MyProcessor Person.java

Чтобы указать несколько процессоров аннотаций за один раз, вы можете разделить их имена классов запятыми, например:

javac -processor package1.Processor1,package2.Processor2 SourceFile.java

~ ~~ 8.3. Использование Maven

Плагин maven-compiler-plugin позволяет указывать процессоры аннотаций как часть его конфигурации.

Вот пример добавления обработчика аннотаций для подключаемого модуля компилятора. Вы также можете указать каталог для размещения сгенерированных источников, используя параметр конфигурации generateSourcesDirectory.

Обратите внимание, что класс BuilderProcessor уже должен быть скомпилирован, например, импортирован из другого jar-файла в зависимостях сборки:

<build>
    <plugins>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
                <generatedSourcesDirectory>${project.build.directory}
                  /generated-sources/</generatedSourcesDirectory>
                <annotationProcessors>
                    <annotationProcessor>
                        com.baeldung.annotation.processor.BuilderProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

    </plugins>
</build>

8.4. Добавление JAR-файла процессора в путь к классам

Вместо указания процессора аннотаций в параметрах компилятора вы можете просто добавить специально структурированный jar-файл с классом процессора в путь к классам компилятора.

Чтобы подобрать его автоматически, компилятор должен знать имя класса процессора. Поэтому вы должны указать его в файле META-INF/services/javax.annotation.processing.Processor как полное имя класса процессора:

com.baeldung.annotation.processor.BuilderProcessor

Вы также можете указать несколько процессоров из этого jar-файла, чтобы подобрать автоматически, разделив их новой строкой:

package1.Processor1
package2.Processor2
package3.Processor3

Если вы используете Maven для создания этого jar-файла и попытаетесь поместить этот файл непосредственно в каталог src/main/resources/META-INF/services, вы столкнетесь с следующая ошибка:

[ERROR] Bad service configuration file, or exception thrown while 
constructing Processor object: javax.annotation.processing.Processor: 
Provider com.baeldung.annotation.processor.BuilderProcessor not found

«

«Это связано с тем, что компилятор пытается использовать этот файл на этапе обработки исходного кода самого модуля, когда файл BuilderProcessor еще не скомпилирован. Файл должен быть либо помещен в другой каталог ресурсов и скопирован в каталог META-INF/services на этапе копирования ресурсов сборки Maven, либо (что еще лучше) сгенерирован во время сборки.

Библиотека автосервиса Google, обсуждаемая в следующем разделе, позволяет создать этот файл с помощью простой аннотации.

8.5. Использование библиотеки автосервисов Google

@AutoService(Processor.class)
public BuilderProcessor extends AbstractProcessor {
    // …
}

Для автоматического создания регистрационного файла можно использовать аннотацию @AutoService из библиотеки автосервисов Google, например:

Эта аннотация сама обрабатывается процессором аннотаций из библиотеки автосервиса. Этот процессор создает файл META-INF/services/javax.annotation.processing.Processor, содержащий имя класса BuilderProcessor.

9. Заключение

В этой статье мы продемонстрировали обработку аннотаций на уровне исходного кода на примере создания класса Builder для POJO. Мы также предоставили несколько альтернативных способов регистрации обработчиков аннотаций в вашем проекте.