Рубрики
Без рубрики

Java Аннотация Обработка и создание строителя

Быстрое и практическое руководство по обработке аннотации на Java, показывающее, как создать строитель из POJO.

Автор оригинала: baeldung.

1. Введение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Последние версии автоугонка библиотека и Maven-компилятор-плагин можно найти в Центральном репозитории Maven:


    1.0-rc2
    
      3.5.1
    




    
        com.google.auto.service
        auto-service
        ${auto-service.version}
        provided
    




    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            ${maven-compiler-plugin.version}
            
                1.8
                1.8
            
        

    

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


    com.baeldung
    annotation-processing
    1.0.0-SNAPSHOT

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

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

public class Person {

    private int age;

    private String name;

    // getters and setters …

}

Мы хотим создать класс помощников строителя, чтобы мгновенно Лицо класс более свободно:

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

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

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

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

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

ИСТОЧНИК Политика удержания означает, что эта аннотация доступна только во время обработки источника и недоступна во время выполнения.

Лицо класс с свойствами, аннотированными @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

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

Во-первых, мы должны указать аннотации, что этот процессор способен обрабатывать, а также поддерживаемую версию исходных кодов. Это может быть сделано либо путем внедрения методов getSupportedAnnotationTypes и getSupportedИсточникВерсия из Процессор интерфейс или путем аннотации класса с @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 annotations, 
      RoundEnvironment roundEnv) {
        return false;
    }
}

Можно указать не только названия конкретных аннотации, но и подстановочные знаки, такие как “com.baeldung.аннотация.” для обработки аннотаций внутри com.baeldung.аннотация пакет и все его подпакеты, или даже “*” для обработки всех аннотаций.

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

Аннотации передаются в качестве первой Set аннотации аргумент, и информация о текущем раунде обработки передается как РаундЭнвиромент раундЭнв аргумент.

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

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

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

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

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

@Override
public boolean process(Set annotations, 
  RoundEnvironment roundEnv) {

    for (TypeElement annotation : annotations) {
        Set annotatedElements 
          = roundEnv.getElementsAnnotatedWith(annotation);
        
        // …
    }

    return true;
}

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

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

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

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

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

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

Мы должны предупредить пользователя о неправильно аннотированных методах, поэтому давайте использовать Сообщение экземпляр доступен из 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 setterMap = setters.stream().collect(Collectors.toMap(
    setter -> setter.getSimpleName().toString(),
    setter -> ((ExecutableType) setter.asType())
      .getParameterTypes().get(0).toString()
));

6.3. Создание выходной файл

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

Для создания вывода файла мы будем использовать Филер экземпляр, снова предоставленный объектом в 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 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. Запуск примера

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

Сгенерированный Человекостроитель класс можно найти внутри аннотация-пользователь/цель/генерируемые источники/аннотации/com/baeldung/аннотация/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 он был deprecated в пользу других вариантов и удален полностью в Java 8. Это не будет обсуждаться в этой статье.

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

-процессор ключ компилятора является стандартным механизмом JDK для увеличения стадии обработки источника компилятора с вашим собственным процессором аннотации.

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

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

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

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

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

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

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

Maven-компилятор-плагин позволяет указывать аннотации процессоров как часть его конфигурации.

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


    

        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.5.1
            
                1.8
                1.8
                UTF-8
                ${project.build.directory}
                  /generated-sources/
                
                    
                        com.baeldung.annotation.processor.BuilderProcessor
                    
                
            
        

    

8.4. Добавление процессора Jar к Classpath

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

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

com.baeldung.annotation.processor.BuilderProcessor

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

package1.Processor1
package2.Processor2
package3.Processor3

Если вы используете Maven построить эту банку и попытаться положить этот файл непосредственно в 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

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

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

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

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

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

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

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

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

Исходный код статьи доступен для на GitHub .