null

Как создать Annotation Processor

Что такое Annotation Processor и для чего он нужен

 

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

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

Хорошим примером, чтобы понять для чего нужен процессор аннотаций, будет библиотека lombok. Хоть и ее реализация довольно сложная, но общие принципы базируются на обработке аннотаций в момент компиляции. В общем, lombok позволяет при помощи специальных аннотаций генерировать getter’ы, setter’ы, оборачивать код в try/catch блок и другими способами упрощает жизнь. Весь добавочный код и генерация методов происходит в lombok в процессе компиляции. Конечно, программисты lombok не дураки и они используют не просто только процессор аннотаций, а API-вызовы внутренней реализации компилятора для прямого изменения абстрактного синтаксического дерева программы между чтением исходного кода и выводом скомпилированного байт-кода. Так просто свой lombok при помощи процессора аннотаций не создашь, но это хороший пример, для чего вообще может понадобиться Annotation Processor.
 

Приступим к созданию процессора аннотаций

 

Введение

 

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

 

Аннотация

 

Приступим к написанию аннотации!

Вот пример самой простой аннотации:

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
public @interface SimpleAnnotation {
    boolean someField();
}

Для написание необходимо знать несколько вещей:

Мета-аннтоация Retention. Показывает, как долго необходимо хранить аннтоацию и инициализируется одним из трех значений:

RetentionPolicy.SOURCE - аннотация используется на этапе компиляции и должна отбрасываться компилятором и будет доступна только в исходном коде;
RetentionPolicy.CLASS - аннтоация будет записана в class-файл компилятором, но не должна быть доступна во время выполнения (runtime);
RetentionPolicy.RUNTIME - аннотация будет записана в class-файл и доступна во время выполнения через reflection.


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

ElementType.TYPE, что означает что она может быть объявлена перед классом, интерфейсом или enum.
ElementType.PACKAGE - назначением является целый пакет (package);
ElementType.TYPE - класс, интерфейс, enum или другая аннотация:
ElementType.METHOD - метод класса, но не конструктор (для конструкторов есть отдельный тип CONSTRUCTOR);
ElementType.PARAMETER - параметр метода;
ElementType.CONSTRUCTOR - конструктор;
ElementType.FIELD - поля-свойства класса;
ElementType.LOCAL_VARIABLE - локальная переменная (обратите внимание, что аннотация не может быть прочитана во время выполнения программы, то есть, данный тип аннотации может использоваться только на уровне компиляции как, например, аннотация @SuppressWarnings);
ElementType.ANNOTATION_TYPE - другая аннотация.


Также есть возможность задать сразу несколько типов аннотации. 

 

Процессор

 

Давайте посмотрим на сам процессор. Каждый процессор расширяется от AbstractProcessor следующим образом:
 

public class MyProcessor extends AbstractProcessor {

	@Override
	public synchronized void init(ProcessingEnvironment env){ }

	@Override
	public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

	@Override
	public Set<String> getSupportedAnnotationTypes() { }

	@Override
	public SourceVersion getSupportedSourceVersion() { }

}

init(ProcessingEnvironment env): каждый класс процессора аннотаций должен иметь пустой конструктор. Однако существует специальный метод init() для того, добавлять какую-либо логику при инициализации класса.

process(Set<? extends TypeElement> annotations, RoundEnvironment env): это своего рода метод main() каждого процессора. Здесь вы пишете свой код для сканирования, оценки и обработки аннотаций и создания java-файлов.

getSupportedSourceVersion() или аннотация @SupportedSourceVersion()
используется для указания используемой версии Java, если необходимо придерживаться какой-либо конкретной версии и указания аннотаций

getSupportedAnnotationTypes() или аннотация @SupportedAnnotationTypes({
   // Set of full qullified annotation type names
}) - используется для указания аннотаций, которые будут обрабатываться этим процессором.
 

Пример процессора поближе:

@SupportedAnnotationTypes("annotations.SimpleAnnotation")
public class MyProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        System.out.println("INIT");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("PROCESS START");
        for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(DecompositionAnnotation.class)) {
            System.out.println("----> " + annotatedElement.getSimpleName());
            if (annotatedElement.getKind() != ElementKind.CLASS) {
                    throw new RuntimeException("Only classes can be annotated with @DecompositionAnnotation");
            }
            TypeElement typeElement = (TypeElement) annotatedElement;

            System.out.println(typeElement.getQualifiedName());
        }

        System.out.println("PROCESS END");
        return true;
    }
}

В методе обработки аннотаций process мы получаем все классы, помеченные аннотацией @SimpleAnnotation, проверяем тип полученного элемента и выводим полное имя класса, помеченного этой аннотацией.

Следует обратить внимание, что мы получаем только TypeElement, который представляет элементы типа в исходном коде, такие как классы. Однако TypeElement не содержит информации о самом классе. Из TypeElement можно получить имя класса, но невозможно - информацию о классе, как и о суперклассе. Эта информация доступна через TypeMirror. Вы можете получить доступ к TypeMirror элемента, вызвав element.asType().

 

Регистрация процессора
 

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

  - com
    - example
      - MyProcessor.class

  - META-INF
    - services
      - javax.annotation.processing.Processor

 

Содержимое файла javax.annotation.processing.Processor представляет собой список с полными именами классов для процессоров с новой строкой в ​​качестве разделителя:
 

com.example.MyProcessor

Впрочем, это все можно сделать при помощи @AutoService(MyProcessor.class). Он самостоятельно генерирует файл META-INF/services/javax.annotation.processing.Processor.
 

Сборка

 

Тут речь идти будет только о конфигурации проекта для сборки в мавене.

 

Сборка если процессор и приложение в одном проекте

 

Это только один из вариантов, как сконфигурировать приложение с процессором в мавене. Предупрежу сразу, так как я отошла идеи писать все в одном приложении, и разделила приложение и процессор на 2 модуля, то этот вариант может быть не идеальным. Но он работает :)

Для этого необходимо создать 2 execution:

1. Один для компиляции процессора: в него мы указываем путь до процессора:  


<includes>
    <include>
        annotations/processor/MyProcessor.class
    </include>
</includes>

И указываем то, что не нужно процессить аннотации: 

<compilerArgument>-proc:none</compilerArgument>

2. Второй нужен для полной компиляции проекта. 

 

Таким образом мы сначала компилируем процессор аннотаций, потом сам проект и в конце делаем package, пропуская этап компиляции.

mvn clean compiler:compile@compile-project
mvn package -Dmaven.main.skip

Как выглядит это полностью:
 

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <configuration>
                            <compilerArgument>-proc:none</compilerArgument>
                            <includes>
                                <include>
                                  annotations/processor/MyProcessor.class
                                </include>
                            </includes>
                        </configuration>
                    </execution>
                    <execution>
                        <id>compile-project</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

 

Подключение аннотации, как зависимости

 

Тут все вообще просто. Необходимо добавить ваш процессор в зависимость проекта: 

   <dependencies>
        <dependency>
            <groupId>ru.edu.annotations</groupId>
            <artifactId>annotationprocessor</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

И указать компилятору, что этот процессор надо процессить при сборке.
 

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <annotationProcessors>
                        <annotationProcessor>
                            ru.edu.annotations.processor.SimpleAnnotationProcessor
                        </annotationProcessor>
                    </annotationProcessors>
                </configuration>
</plugin>

 

Вот и всё.