Пользовательский процессор аннотаций, использующий сгенерированные аннотации

Вопрос

Как правильно создать обработчик аннотаций Java, который использует аннотации, которые он сам генерирует?

Контекст

Я рассматриваю обработку аннотаций как средство генерации повторяющегося/шаблонного кода, и в настоящее время в моем прицеле находятся аннотации, использующие перечисление. Насколько я понимаю, можно использовать только перечисления, на которые явно ссылаются, однако я хотел бы иметь возможность использовать любой клиентский Enum (таким образом, это не то, что известно обработчику аннотаций во время его компиляции).

public @interface GenericEnumAnnotation() {
    Enum<?> value();
}

не работает, скорее это нужно сделать как

public @interface MyEnumAnnotation() {
    MyEnum value();
}

Так что генерация кода вам на помощь! Вместо того, чтобы заставлять клиента создавать собственную аннотацию для каждого Enum, я настроил его на создание этой аннотации на основе аннотации @GenerateAnnotation. Таким образом

@GenerateAnnotation
public enum MyEnum {...}

сгенерирует действительный MyEnumAnnotation

@EnumAnnotation
public @interface MyEnumAnnotation() {
    MyEnum value();
}

Клиентский код затем может использовать сгенерированный @MyEnumAnnotation. Теперь, когда перечисление создано, я хочу использовать это @MyEnumAnnotation для создания дополнительного кода для клиентского кода, который с ним аннотирован. Вновь сгенерированная аннотация становится доступной во втором проходе обработчика аннотаций, и благодаря @EnumAnnotation я могу сказать, что это именно та аннотация, которую я хочу использовать для генерации кода, однако когда я делаю попытку, никаких использований не обнаружено.

@SupportedAnnotationTypes("com.company.generator.EnumAnnotation")
@AutoService(Processor.class)
public class EnumAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        annotations.forEach(enumAnnotation -> { //@EnumAnnotation
            env.getElementsAnnotatedWith(enumAnnotation).forEach(customAnnontation -> { //@MyEnumAnnotation
                env.getElementsAnnotatedWith(customAnnotation -> { // Elements using the @MyEnumAnnotation
                    // Never entered - nothing annotated is found
                });
            });
        });
    }
}

В результате экспериментов я определил, что это связано с тем, что второй проход рассматривает только «новые файлы», а не полный объем/масштаб классов. Клиентский код (который использует аннотацию) обрабатывается только во время первого прохода и поэтому больше не доступен для поиска/доступа во втором проходе, когда процессор аннотаций действительно знает об этой сгенерированной аннотации.

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

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
@AutoService(Processor.class)
public class FirstPassCollector extends AbstractProcessor {
    
    public static RoundEnvironment firstPassEnvironment = null;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (firstPassEnvironment == null)
            FirstPassCollector.firstPassEnvironment = roundEnv;
        return false;
    }

}


@SupportedAnnotationTypes("com.company.generator.EnumAnnotation")
@AutoService(Processor.class)
public class EnumAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        annotations.forEach(enumAnnotation -> {
            env.getElementsAnnotatedWith(enumAnnotation).forEach(customAnnontation -> {
                FirstPassCollector.firstPassEnvironment.getElementsAnnotatedWith(customAnnotation -> {
                    // Now searching the files from the first pass, and annotated classes are now found!
                });
            });
        });
    }
}

Я знаю, что в написанном коде есть недостатки (т. е. нет проверки на нулевое значение в firstPassEnvironment при его использовании), однако в принципе это то, что работает, но кажется довольно хрупким/взломанным решением. Есть ли лучший способ достижения этой конечной цели?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
0
112
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Проблема:

Буду рад ответить на вопрос:

Как правильно создать обработчик аннотаций Java, который использует аннотации, которые он сам генерирует?

Однако этот конкретный ответ касается следующего (слегка измененного) вопроса:

Как создать обработчик аннотаций Java, который использует аннотации, которые сами генерируют?

Разница в том, что я не уверен на 100% (или не могу гарантировать) «правильную» деталь.

Однако обратите внимание, что исходная проблема, которую вы представляете, похоже, другая:

Я хотел бы иметь возможность использовать любой клиент Enum

... на который я попробую дать другой ответ позже.

Концепция

По самой постановке задачи можно сформулировать следующую последовательность шагов, которые необходимо выполнить (по порядку):

  • Создавайте аннотации для конкретных enum.
  • Сгенерируйте код для элементов, аннотированных с помощью аннотаций, созданных на предыдущем шаге.

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

Короче говоря, еще одна подробная последовательность шагов для решения проблемы такова:

  1. Найдите аннотированные enum, для которых мы хотим создать аннотации.
  2. Создайте аннотации enum, найденных на шаге 1.
  3. Найдите элементы, помеченные любой аннотацией, созданной на шаге 2.
  4. Сгенерируйте любой код, необходимый для элементов, найденных на шаге 3.

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

Материализация решения:

На шаге 1 мы можем просто создать аннотацию, которая будет аннотировать каждый enum, который пользователь хочет поместить в категорию. Затем мы найдем эти enum с помощью getElementsAnnotatedWith метода RoundEnvironment (в каждом раунде). Эта идея взята из вашего GenerateAnnotation (в этом ответе она называется GenerateEnumAnnotation).

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

Тогда согласно документации Процессора :

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

а также согласно документации Filer:

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

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

Шаг 4 теперь готов к генерации требуемого кода, поскольку теперь в нашем распоряжении есть каждая аннотация перечисления, начиная с 3-го шага.

Выполнение:

Аннотация для создания аннотаций enum:

package annotations.soq78648395;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenerateEnumAnnotation {
}

Процессор:

package annotations.soq78648395;

import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("annotations.soq78648395.GenerateEnumAnnotation")
public class TwoStepEnumAnnotationProcessor extends AbstractProcessor {
    
    /**
     * We need an {@code Element} representation which can be independent of the round. That's
     * because {@code Element} information is populated as new rounds are coming (for example for
     * the generated annotations), so we need to reload {@code Element}s on every round.
     */
    private static final class InterRoundElement {
        
        //All public final to avoid getters and setters in order to save space for the answer post itself.
        public final String simpleName, packageName, qualifiedName;
        
        public InterRoundElement(final Elements elementUtils,
                                 final Element element) {
            this(element.getSimpleName().toString(), elementUtils.getPackageOf(element).getQualifiedName().toString());
        }
        
        public InterRoundElement(final String simpleName,
                                 final String packageName) {
            this.simpleName = simpleName;
            this.packageName = packageName;
            qualifiedName = packageName.isEmpty()? simpleName: (packageName + '.' + simpleName);
        }

        @Override
        public String toString() {
            return qualifiedName;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(qualifiedName);
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj)
                return true;
            if (obj == null || getClass() != obj.getClass())
                return false;
            return Objects.equals(qualifiedName, ((InterRoundElement) obj).qualifiedName);
        }
    }
    
    private boolean isAnnotatedWith(final AnnotationMirror annotationMirror,
                                    final TypeElement annotation) {
        final TypeElement other = (TypeElement) annotationMirror.getAnnotationType().asElement();
        //Note here: 'other.getKind()' may actually be 'CLASS' rather than 'ANNOTATION_TYPE' (it happens for annotations generated by annotation processing).
        return Objects.equals(annotation.getQualifiedName().toString(), other.getQualifiedName().toString());
    }
    
    /**
     * As <i>early elements</i> are named the {@code Element}s which are potentially annotated with
     * an enum annotation which is going to be generated. For example root elements of the first
     * round will not appear again in the following rounds, but they may be already annotated with
     * an enum annotation which is not yet generated, so we need to maintain them until we find out
     * what happens.
     */
    private final Set<InterRoundElement> earlyElements = new HashSet<>();
    
    /**
     * A {@code Map} from generated enum annotations to the {@code Element}s being annotated with
     * them. If an enum annotation is registered as a key of this map, then its code is already
     * generated even if no {@code Elements} are found to be annotated with it (ie for empty map
     * value).
     */
    private final Map<InterRoundElement, Set<InterRoundElement>> processedElements = new HashMap<>();
    
    /**
     * Just a zero based index of the processing round.
     */
    private int roundSerial = -2;
    
    /**
     * For debugging messages.
     * @param tokens
     */
    private void debug(final Object... tokens) {
        System.out.print(String.format(">>>> [Round %2d]", roundSerial));
        for (final Object token: tokens) {
            System.out.print(' ');
            System.out.print(token);
        }
        System.out.println();
    }
    
    /**
     * Opens a {@code PrintStream} for writing/generating code.
     * @param interRoundElement
     * @param originatingElements
     * @return
     * @throws IOException
     */
    private PrintStream create(final InterRoundElement interRoundElement,
                               final Element... originatingElements) throws IOException {
        debug("Will generate output for", interRoundElement);
        final JavaFileObject outputFileObject = processingEnv.getFiler().createSourceFile(interRoundElement.qualifiedName, originatingElements);
        return new PrintStream(outputFileObject.openOutputStream());
    }
    
    /**
     * Generates an enum annotation.
     * @param origin
     * @param output
     * @param originatingElements
     * @return {@code true} for success, otherwise {@code false}.
     */
    private boolean generateEnumAnnotation(final InterRoundElement origin,
                                           final InterRoundElement output,
                                           final Element... originatingElements) {
        try (final PrintStream outputFileOutput = create(output, originatingElements)) {
            if (!output.packageName.isEmpty()) { //The default package is represented as an empty 'packageName'.
                outputFileOutput.println("package " + output.packageName + ";");
                outputFileOutput.println();
            }
            for (final Object line: new Object[]{ //We obviously here need to utilize text blocks of the newer Java versions...
                "@java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE)",
                "@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE)",
                "public @interface " + output.simpleName + " {",
                "    " + origin.qualifiedName + " value();",
                "}"
            })
                outputFileOutput.println(line);
            return true;
        }
        catch (final IOException ioe) {
            ioe.printStackTrace(System.out);
            return false;
        }
    }
    
    private void reset() {
        processedElements.clear();
        earlyElements.clear();
        roundSerial = -1;
    }
    
    @Override
    public synchronized void init(final ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        if (super.isInitialized())
            reset(); //Initialize, prepare for new processes.
    }
    
    /**
     * This method handles elements annotated with {@code GenerateEnumAnnotation}.
     * @param annotatedElement
     */
    private void handleGenerationAnnotation(final Element annotatedElement) {
        final Messager messager = processingEnv.getMessager();
        if (annotatedElement.getKind() != ElementKind.ENUM)
            messager.printMessage(Diagnostic.Kind.ERROR, "Only enums are supported.", annotatedElement);
        else {
            final InterRoundElement origin = new InterRoundElement(processingEnv.getElementUtils(), (TypeElement) annotatedElement);
            final String pack = (origin.packageName.isEmpty()? "": (origin.packageName + '.')) + "enum_annotations";
            final InterRoundElement output = new InterRoundElement(origin.simpleName + "Annotation", pack); //The enum annotation element to generate...
            if (generateEnumAnnotation(origin, output, annotatedElement))
                processedElements.computeIfAbsent(output, dejaVu -> new HashSet<>()); //Store the generated enum annotation information.
            else
                messager.printMessage(Diagnostic.Kind.ERROR, "Failed to create annotation " + output + " (for " + origin + ").", annotatedElement);
        }
    }
    
    /**
     * Handles {@code Element}s annotated with a generated enum annotation. Modify this according to
     * your requirements. It assumes that generated enum annotations are not repeatable (otherwise
     * it should take a {@code Collection} of {@code AnnotationMirror}s instead of a single one).
     * @param enumAnnotation The generated enum annotation.
     * @param annotatedElement The {@code Element} annotated with {@code enumAnnotation}.
     * @param annotationMirror The mirror of annotating {@code annotatedElement} with {@code enumAnnotation}.
     */
    private void handleEnumAnnotation(final TypeElement enumAnnotation,
                                      final Element annotatedElement,
                                      final AnnotationMirror annotationMirror) {
        //The current implementation just prints some messages of the annotation we've found...
        debug("Processing", annotatedElement.getKind(), "element", annotatedElement);
        debug("    Annotated by enum annotation", enumAnnotation);
        debug("    With the following applicable mirror:", annotationMirror);
    }
    
    /**
     * Find out if any "early elements" are annotated with a generated enum annotation, and handle them.
     */
    private void processEarlyElements() {
        final Elements elementUtils = processingEnv.getElementUtils();
        //We need a defensive shallow copy of 'earlyElements' because it is going to be modified inside the loop:
        final Set<InterRoundElement> defensiveCopiedEarlyElements = new HashSet<>(earlyElements);
        processedElements.forEach((annotationInterRoundElement, annotatedElements) -> {
            final TypeElement generatedEnumAnnotation = elementUtils.getTypeElement(annotationInterRoundElement.qualifiedName);
            //'roundEnv.getElementsAnnotatedWith(generatedEnumAnnotation)' will actually return an empty List here, so we have to find elements annotated with 'generatedEnumAnnotation' via its annotation mirrors...
            defensiveCopiedEarlyElements.forEach(interRoundElement -> {
                final TypeElement annotatedElement = elementUtils.getTypeElement(interRoundElement.qualifiedName); //Reload annotated element for the current round (ie don't rely on its previous Element occurences), because its annotations may not yet be ready.
                //The following code assumes generated enum annotations are not repeatable...
                annotatedElement.getAnnotationMirrors().stream()
                        .filter(annotationMirror -> isAnnotatedWith(annotationMirror, generatedEnumAnnotation)) //Continue only for mirrors of type generatedEnumAnnotation.
                        .filter(annotationMirror -> annotatedElements.add(interRoundElement)) //If we've seen the early element before then skip it, otherwise add it to annotatedElements and process it...
                        .findAny()
                        .ifPresent(annotationMirror -> {
                            earlyElements.remove(interRoundElement); //No need to store it any more.
                            handleEnumAnnotation(generatedEnumAnnotation, annotatedElement, annotationMirror);
                        });
            });
        });
    }
    
    @Override
    public boolean process(final Set<? extends TypeElement> annotations,
                           final RoundEnvironment roundEnv) {
        ++roundSerial;
        final Elements elementUtils = processingEnv.getElementUtils();
        final Set<? extends Element> rootElements = roundEnv.getRootElements();
        
        //Store only root elements of type class in 'earlyElements':
        rootElements.stream()
                .filter(rootElement -> rootElement.getKind() == ElementKind.CLASS)
                .map(rootElement -> new InterRoundElement(elementUtils, rootElement))
                .forEachOrdered(earlyElements::add);

        debug("Annotations:", annotations);
        debug("Root elements:", rootElements);
        
        /*First process early elements and then generate enum annotations. The sequence of these two
        calls is ought to how their methods' body is implemented, and we know we won't loose any
        enum annotations because any generated enum annotations in the current round will be
        supplied as root elements in the next round and we already add "early elements" in the
        beginning of the processor's 'process' method.*/
        processEarlyElements();
        roundEnv.getElementsAnnotatedWith(GenerateEnumAnnotation.class).forEach(this::handleGenerationAnnotation);

        debug("Current early elements:", earlyElements);
        processedElements.forEach((annotation, elements) -> debug("Created annotation", annotation, "with the following elements processed for it so far:", elements));

        //Cleanup, prepare for later processes (if reused):
        if (roundEnv.processingOver())
            reset();
        
        /*GenerateEnumAnnotations are always consumed. Even if there is a failure in generating the
        resulting code from a GenerateEnumAnnotation annotated element, this doesn't mean that
        repeating the handleGenerationAnnotation has the potential in succeeding in later rounds,
        so we are not repeating the attempt (ie handling is finished, errors are handled, we move on).*/
        return true;
    }
}

Как это использовать:

В коде пользователя добавьте нужные вам enum с помощью GenerateEnumAnnotation:

package soq78648395;

import annotations.soq78648395.GenerateEnumAnnotation;

@GenerateEnumAnnotation
public enum MyEnum {
    MY_A, MY_B;
}

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

package soq78648395;

import soq78648395.enum_annotations.MyEnumAnnotation;

@MyEnumAnnotation(MyEnum.MY_A)
public class MyClass {
}

Предположения и отсутствие поддержки функций:

  1. Никакие другие процессоры не будут мешать аннотациям, генерируемым вашим процессором. Я не рассматривал такие сценарии в коде этого ответа.
  2. Поддерживаются только enum для аннотации GenerateEnumAnnotation.
  3. В настоящее время поддерживается только классы, которые можно аннотировать с помощью сгенерированных аннотаций перечисления. Это можно легко расширить для поддержки всех видов TypeElement посредством простой проверки условий, но для всех остальных видов Element потребуются дополнительные модификации (зависит от того, какие виды вы хотите).
  4. Сгенерированные аннотации перечислений не повторяются.
  5. Элементы, помеченные более чем одной из сгенерированных аннотаций перечисления, будут обработаны только один раз. Хорошей новостью является то, что вы можете получить зеркала аннотаций для этих элементов и проверить их (в методе handleEnumAnnotation), чтобы выяснить, аннотированы ли они более чем одной сгенерированной аннотацией перечисления, но только если вы сгенерируете все аннотации перечисления в одном и том же раунде. . Для большего количества раундов вам потребуется расширить реализацию. Я думаю, что это потенциально также должно решить проблему повторяемости сгенерированных аннотаций перечисления.

Примечания:

  1. Поскольку в коде этого ответа я использую только Java SE версии 8, для работы в более новых версиях может потребоваться адаптация. Этот ответ был для меня долгим путешествием (по сравнению с другими моими ответами на этом сайте и соответствующими исследованиями/тестированиями), поэтому я выбрал версию, которая мне наиболее удобна, и знаю, чего ожидать. Но я понимаю, что вместо этого вам нужна версия Java 21 (судя по вашему коду), поэтому прошу прощения за это. Я надеюсь, что алгоритмическое решение концепции останется в основном прежним и что этот код положит начало вашим усилиям по поддержке вашей новой версии Java.
  2. Поскольку мы неявно обрабатываем сгенерированные аннотации (то есть мы никоим образом не заявляем о поддержке сгенерированных аннотаций), то, вероятно, более уместно объявить поддержку любой аннотации ("*"), а затем заявлять только о тех, которые мы генерируем (вместе с с GenerateEnumAnnotation конечно). Единственное, что, я думаю, позволяет нам проверить правильность реализации этого ответа (в том, что касается неявного утверждения сгенерированных аннотаций), — это то, верно ли мое первое предположение. Если вы пойдете по пути поддержки любых аннотаций, постарайтесь быть осторожными и не требовать аннотаций, отличных от тех, которые вы действительно генерируете и знаете (например, путем поддержания структуры данных между раундами с этими аннотациями).

Обновление для обработки комментариев:

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

  1. В первом раунде все, что известно компилятору на данный момент, представлено через RoundEnvironment и не более того. Это включает в себя тот факт, что MyClass помечен MyEnumAnnotation, но соответствующий AnnotationMirror в настоящее время указывает, что тип аннотации имеет вид CLASS, а не ANNOTATION_TYPE, и кажется, что отсутствует дополнительная информация (например, вы не можете получить ее параметры), что должно быть нормальным, судя по тому, что код для MyEnumAnnotation еще не сгенерирован и не обработан. В этом же раунде мы полностью генерируем аннотации перечислений (т. е. MyEnumAnnotation) (то есть мы создаем исходный файл и закрываем его OutputStream, тем самым делая код доступным для инструмента компилятора).
  2. Наш процессор вызывается для второго раунда, где мы теперь можем найти сгенерированные аннотации перечисления в корне Elements (предполагается, что компилятор теперь обработал представление сгенерированных аннотаций из предыдущего раунда, так что теперь они доступны как Element с). Мы не получаем MyClass в корневых элементах во втором и последующих раундах, но они доступны через RoundEnvironment (нам нужно найти их, например, с помощью elementUtils.getTypeElement(...) метода). На данный момент мы знаем аннотации перечисления Element, поэтому можно сказать, что нам следует просто использовать roundEnv.getElementsAnnotatedWith(myEnumAnnotationElement);, но, согласно моему тестированию, здесь просто возвращается пустое Set (возможно, использование @SupportedAnnotationTypes("*") могло бы решить проблему, но я это не проверял). Другой вариант поиска AnnotationMirror — пойти наоборот, то есть получить AnnotationMirrors аннотированного элемента и найти интересующие. Я использовал этот вариант, поэтому, если бы мы не вели набор просмотренных клиентов Element, мы бы не знали, что искать.

Теперь, когда я снова об этом думаю, часть вашего вопроса о «правильном пути» более значительна, чем я сначала думал, и ее нельзя опустить. Я просто знаю способ, не обязательно правильный. Этот ответ больше похож на ваше взломанное решение, но с красивой маской.

gthanop 26.06.2024 22:28

Спасибо за этот чрезвычайно подробный ответ! Мне придется рассмотреть это более подробно, когда у меня будет больше времени (заранее извиняюсь, если я не вернусь к этому до следующей недели), однако, возможно, я вижу здесь часть своего недопонимания. Содержит ли RoundEvironment все доступные файлы (или, точнее, все ранее невидимые файлы, что означает, что первый раунд/шаг содержит весь клиентский код с каждым последующим вновь созданным файлом)? Таким образом, возникает необходимость сохранять ссылку на «необработанные файлы» из каждого раунда (с помощью моего хака или вашего набора InterRoundElement)?

cancech 27.06.2024 23:27

Нет проблем, этот ответ было интересно исследовать. Я не уверен, что полностью понял ваш вопрос в комментарии, но я попытался ответить, используя некоторые подсказки (см. раздел «Обновления»).

gthanop 28.06.2024 23:20

Спасибо! Вы точно ответили на мой вопрос :-). Я не смогу по-настоящему вникнуть в ваш код еще несколько дней, но мне определенно нравится этот подход.

cancech 30.06.2024 13:50

Отлично, я рад, что помогло и вам понравилось! По сути, это аналогичная концепция вашего взломанного решения, но вам не нужно сохранять ссылку на RoundEnvironment в последующих раундах. Я понимаю, что вы ни в коем случае не обязаны каким-либо образом реагировать на мои ответы, но я ценю, что вы уже это сделали (по крайней мере, с комментариями), и вам не нужно извиняться передо мной за потраченное время (если вы это имеете в виду) .

gthanop 30.06.2024 19:13

Наконец-то дошли руки до подробного изучения этого, и если я правильно понимаю, то ваш подход: 1) Вручную отслеживает все файлы, найденные в RoundEnvironment, как InterRoundElements. 2) При поиске используемой аннотации он вручную перебирает отслеживаемые вручную InterRoundElements. а не непосредственно RoundEnvironment 3) Выполните «ручную» проверку того, был ли уже обработан InterRoundElement (для данной аннотации?) 4) Повторяйте до тех пор, пока обработка не будет указана как завершенная Правильно ли я понимаю?

cancech 04.07.2024 15:57

Это определенно выглядит чище (менее хакерски), чем мой подход с сохранением RoundEnvironment, тем более что этот подход также будет учитывать сгенерированный код, а не только исходный клиентский код. Просто интересно, как это можно сравнить с точки зрения удобства обслуживания/производительности, учитывая, что это по сути требует повторной реализации функциональности, которая в конечном итоге обеспечивается RoundEnvironment. В целом мне это нравится больше, чем то, что я сделал (тем более, что учет сгенерированного кода потребует сохранения каждого RoundEnvironment), просто болтаю здесь :-)

cancech 04.07.2024 16:06

Исходная проблема, которую вы представляете, выглядит так:

Я хотел бы иметь возможность использовать любой клиент Enum

что действительно невозможно с подписью вашего GenericEnumAnnotation, но если вы измените ее на:

package annotations.soq78648395;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface GenericEnumAnnotation {
    Class<? extends Enum<?>> type();
    String nameOfConstant();
}

...тогда вы сможете проанализировать любой enum и его постоянное значение.

Я понимаю, что это не так удобно, как напрямую использовать константу enum, и вам придется самостоятельно проверять, существует ли данное имя в качестве константы для указанного класса enum.

Ниже приведен пример реализации Processor:

package annotations.soq78648395;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("annotations.soq78648395.GenericEnumAnnotation")
public class GenericEnumAnnotationProcessor extends AbstractProcessor {

    /**
     * For debugging messages.
     * @param tokens
     */
    private void debug(final Object... tokens) {
        System.out.print(">>>>");
        for (final Object token: tokens) {
            System.out.print(' ');
            System.out.print(token);
        }
        System.out.println();
    }
    
    /**
     * Helper for {@code Messager} message printing based on arguments' {@code null} check.
     * @param kind
     * @param message
     * @param element
     * @param annotationMirror
     * @param annotationValue
     */
    private void printMessage(final Diagnostic.Kind kind,
                              final CharSequence message,
                              final Element element,
                              final AnnotationMirror annotationMirror,
                              final AnnotationValue annotationValue) {
        if (message != null && kind != null) {
            final Messager messager = processingEnv.getMessager();
            if (element != null) {
                if (annotationMirror != null) {
                    if (annotationValue != null)
                        messager.printMessage(kind, message, element, annotationMirror, annotationValue);
                    else
                        messager.printMessage(kind, message, element, annotationMirror);
                }
                else
                    messager.printMessage(kind, message, element);
            }
            else
                messager.printMessage(kind, message);
        }
    }
    
    /**
     * Finds and returns the first available {@code AnnotationMirror} of the given {@code Element}
     * with annotation type equal to {@code GenericEnumAnnotation}.
     * @param annotatedElement
     * @return {@code null} if not found.
     * @see <a href = "https://stackoverflow.com/a/10167558/6746785">Partial source</a>.
     */
    private AnnotationMirror findFirstGenericEnumAnnotationMirror(final Element annotatedElement) {
        if (annotatedElement != null)
            for (final AnnotationMirror annotationMirror: annotatedElement.getAnnotationMirrors())
                if (Objects.equals(GenericEnumAnnotation.class.getCanonicalName(), ((TypeElement) annotationMirror.getAnnotationType().asElement()).getQualifiedName().toString()))
                    return annotationMirror;
        return null;
    }
    
    /**
     * Finds and returns the {@code AnnotationValue} of the given annotation mirror which
     * corresponds to the parameter with name equal to the given {@code annotationMirrorValueName}.
     * @param annotationMirror
     * @param annotationMirrorValueName
     * @param considerDefaultValues If {@code true} then return the default {@code AnnotationValue} if the parameter was not specified in the annotation mirror.
     * @return {@code null} if not found.
     * @see <a href = "https://stackoverflow.com/a/10167558/6746785">Partial source</a>.
     */
    private AnnotationValue findAnnotationMirrorValue(final AnnotationMirror annotationMirror,
                                                      final String annotationMirrorValueName,
                                                      final boolean considerDefaultValues) {
        if (annotationMirror != null) {
            final Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = considerDefaultValues
                    ? processingEnv.getElementUtils().getElementValuesWithDefaults(annotationMirror)
                    : annotationMirror.getElementValues();
            for (final Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry: elementValues.entrySet())
                if (Objects.equals(entry.getKey().getSimpleName().toString(), annotationMirrorValueName))
                    return entry.getValue();
        }
        return null;
    }
    
    /**
     * Converts the given annotation value's value to a {@code TypeElement} of kind {@code ENUM}, if applicable.
     * @param annotationValue
     * @return {@code null} if not an enum class.
     * @see <a href = "https://stackoverflow.com/a/10167558/6746785">Partial source</a>.
     */
    private TypeElement asEnumClass(final AnnotationValue annotationValue) {
        if (annotationValue != null) {
            final Object value = annotationValue.getValue();
            if (value instanceof TypeMirror) {
                final Element valueElement = processingEnv.getTypeUtils().asElement((TypeMirror) value);
                if (valueElement != null && ElementKind.ENUM.equals(valueElement.getKind()))
                    return (TypeElement) valueElement;
            }
        }
        return null;
    }
    
    /**
     * Converts the given annotation value's value to a {@code String}, if applicable.
     * @param annotationValue
     * @return {@code null} if not an {@code String}.
     */
    private String asString(final AnnotationValue annotationValue) {
        if (annotationValue != null) {
            final Object value = annotationValue.getValue();
            if (value instanceof String)
                return (String) value;
        }
        return null;
    }
    
    /**
     * @param enumElement A {@code TypeElement} of kind {@code ENUM}.
     * @return
     */
    private List<VariableElement> getEnumConstants(final TypeElement enumElement) {
        if (enumElement == null)
            return Collections.emptyList();
        final Types typeUtils = processingEnv.getTypeUtils();
        final List<VariableElement> fields = ElementFilter.fieldsIn(enumElement.getEnclosedElements());
        final ArrayList<VariableElement> result = new ArrayList<>(fields.size());
        fields.stream()
                .filter(field -> ElementKind.ENUM_CONSTANT.equals(field.getKind()))
                .filter(field -> Objects.equals(typeUtils.asElement(field.asType()), enumElement)) //Source of how to get parameter type for a VariableElement: https://stackoverflow.com/a/7763434/6746785
                .forEach(result::add); //Warning: not intended for parallelStream!...
        result.trimToSize();
        return Collections.unmodifiableList(result);
    }
    
    private VariableElement findFieldByName(final Collection<? extends VariableElement> fields,
                                            final String fieldName) {
        for (final VariableElement field: fields)
            if (field.getSimpleName().toString().equals(fieldName))
                return field;
        return null;
    }
    
    /**
     * Handles {@code Element}s annotated with the generic enum annotation. Modify this according to
     * your requirements.
     * @param annotatedElement The {@code Element} annotated with {@code GenericEnumAnnotation}.
     * @param annotationMirror The mirror of annotating {@code annotatedElement} with {@code GenericEnumAnnotation}.
     * @param annotationValueType The {@code AnnotationValue} for the {@code type} parameter of {@code annotationMirror}.
     * @param annotationValueNameOfConstant The {@code AnnotationValue} for the {@code nameOfConstant} parameter of {@code annotationMirror}.
     * @param enumClass The value of {@code annotationValueType} as a {@code TypeElement} of kind {@code ENUM}.
     * @param enumField The value of {@code annotationValueNameOfConstant} as a {@code VariableElement} of kind {@code ENUM_CONSTANT}.
     */
    private void handleEnumAnnotation(final Element annotatedElement,
                                      final AnnotationMirror annotationMirror,
                                      final AnnotationValue annotationValueType,
                                      final AnnotationValue annotationValueNameOfConstant,
                                      final TypeElement enumClass,
                                      final VariableElement enumField) {
        //The current implementation just prints some messages of the annotation we've found...
        debug("Processing", enumField, "of enum type", enumClass, "for", annotatedElement);
    }
    
    @Override
    public boolean process(final Set<? extends TypeElement> annotations,
                           final RoundEnvironment roundEnv) {
        final Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(GenericEnumAnnotation.class);
        annotatedElements.forEach(annotatedElement -> {
            final AnnotationMirror annotationMirror = findFirstGenericEnumAnnotationMirror(annotatedElement);
            final AnnotationValue annotationValueType = findAnnotationMirrorValue(annotationMirror, "type", true),
                                  annotationValueNameOfConstant = findAnnotationMirrorValue(annotationMirror, "nameOfConstant", true);
            final TypeElement enumClassElement = asEnumClass(annotationValueType);
            final String nameOfConstant = asString(annotationValueNameOfConstant);
            if (enumClassElement != null && nameOfConstant != null) {
                final List<VariableElement> enumConstants = getEnumConstants(enumClassElement);
                if (enumConstants.isEmpty())
                    printMessage(Diagnostic.Kind.ERROR, "No constants for enum " + enumClassElement.getQualifiedName(), annotatedElement, annotationMirror, annotationValueType);
                else {
                    final VariableElement enumField = findFieldByName(enumConstants, nameOfConstant);
                    if (enumField == null)
                        printMessage(Diagnostic.Kind.ERROR, "No constant " + nameOfConstant + " for enum " + enumClassElement.getQualifiedName(), annotatedElement, annotationMirror, annotationValueNameOfConstant);
                    else
                        handleEnumAnnotation(annotatedElement, annotationMirror, annotationValueType, annotationValueNameOfConstant, enumClassElement, enumField);
                }
            }
            else
                printMessage(Diagnostic.Kind.ERROR, "Unknown error obtaining information for " + annotatedElement + ".", annotatedElement, annotationMirror, null);
        });
        return true;
    }
}

... который работает в следующих основных шагах:

  1. Найдите Element, помеченные GenericEnumAnnotation.
  2. Для каждого Element первого шага:
    1. Найдите соответствующий AnnotationMirror и проанализируйте его параметры.
    2. Для enum, полученного в результате параметра typeAnnotationMirror предыдущего шага, найдите все объявленные константы. Если нет, выведите ошибку компиляции и перейдите к следующему Element цикла 2.1.
    3. Из объявленных констант сопоставьте запрошенную с параметром nameOfConstant. Если нет, выведите ошибку компиляции и перейдите к следующему Element цикла 2.1.
    4. Обработайте результаты по мере необходимости.

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

cancech 30.06.2024 13:48

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

gthanop 30.06.2024 18:51

Другие вопросы по теме