Java Annotation Processing and Creating a Builder

1. Въведение

Тази статия е въведение в обработката на анотации на ниво източник на Java и предоставя примери за използването на тази техника за генериране на допълнителни изходни файлове по време на компилацията.

2. Приложения за обработка на анотации

Обработката на анотации на ниво източник се появи за първи път в Java 5. Това е удобна техника за генериране на допълнителни изходни файлове по време на етапа на компилация.

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

Обработката на анотации се използва активно в много повсеместни Java библиотеки, например, за генериране на метакласове в QueryDSL и JPA, за увеличаване на класовете с кодов шаблон в библиотеката Lombok.

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

Забележителното изключение е библиотеката Lombok, която използва обработката на анотации като механизъм за първоначално изтегляне, за да се включи в процеса на компилация и да модифицира AST чрез някои API на вътрешния компилатор. Тази хакерска техника няма нищо общо с предвидената цел на обработката на анотациите и следователно не се обсъжда в тази статия.

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

Обработката на анотациите се извършва в няколко кръга. Всеки кръг започва с компилатора, който търси анотациите в изходните файлове и избира процесорите за анотации, подходящи за тези анотации. Всеки процесор на анотации от своя страна се извиква на съответните източници.

Ако по време на този процес се генерират файлове, се стартира нов кръг с генерираните файлове като вход. Този процес продължава, докато по време на етапа на обработка не се генерират нови файлове.

Всеки процесор на анотации от своя страна се извиква на съответните източници. Ако по време на този процес се генерират файлове, се стартира нов кръг с генерираните файлове като вход. Този процес продължава, докато по време на етапа на обработка не се генерират нови файлове.

API за обработка на анотации се намира в пакета javax.annotation.processing . Основният интерфейс, който ще трябва да внедрите, е интерфейсът на процесора , който има частична реализация под формата на клас AbstractProcessor . Този клас е този, който ще разширим, за да създадем наш собствен процесор за анотиране.

4. Създаване на проекта

За да демонстрираме възможностите за обработка на анотации, ще разработим прост процесор за генериране на плавни конструктори на обекти за коментирани класове.

Ще разделим нашия проект на два модула Maven. Един от тях, анотация процесор модул ще съдържа самия процесор, заедно с пояснението, а друг, на анотация потребител модул, ще съдържа анотиран клас. Това е типичен случай на обработка на анотации.

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

Най-новите версии на библиотеката за автоматично обслужване и приставката maven-compiler-plugin могат да бъдат намерени в хранилището на Maven Central:

 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 … }

Искаме да създадем помощен клас на builder, за да създадем по-лесно клас Person :

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

Този клас PersonBuilder е очевиден избор за едно поколение, тъй като структурата му е напълно дефинирана от методите на Person setter.

Нека създадем анотация @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 анотации.

The @AutoService annotation is a part of the auto-service library and allows to generate the processor metadata which will be explained in the following sections.

@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; } }

You can specify not only the concrete annotation class names but also wildcards, like “com.baeldung.annotation.*” to process annotations inside the com.baeldung.annotation package and all its sub packages, or even “*” to process all annotations.

The single method that we’ll have to implement is the process method that does the processing itself. It is called by the compiler for every source file containing the matching annotations.

Annotations are passed as the first Set annotations argument, and the information about the current processing round is passed as the RoundEnviroment roundEnv argument.

The return boolean value should be true if your annotation processor has processed all the passed annotations, and you don't want them to be passed to other annotation processors down the list.

6.2. Gathering Data

Our processor does not really do anything useful yet, so let’s fill it with code.

First, we’ll need to iterate through all annotation types that are found in the class — in our case, the annotations set will have a single element corresponding to the @BuilderProperty annotation, even if this annotation occurs multiple times in the source file.

Still, it’s better to implement the process method as an iteration cycle, for completeness sake:

@Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); // … } return true; }

In this code, we use the RoundEnvironment instance to receive all elements annotated with the @BuilderProperty annotation. In the case of the Person class, these elements correspond to the setName and setAge methods.

@BuilderProperty annotation's user could erroneously annotate methods that are not actually setters. The setter method name should start with set, and the method should receive a single argument. So let’s separate the wheat from the chaff.

In the following code, we use the Collectors.partitioningBy() collector to split annotated methods into two collections: correctly annotated setters and other erroneously annotated methods:

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);
    

Here we use the Element.asType() method to receive an instance of the TypeMirror class which gives us some ability to introspect types even though we are only at the source processing stage.

We should warn the user about incorrectly annotated methods, so let’s use the Messager instance accessible from the AbstractProcessor.processingEnv protected field. The following lines will output an error for each erroneously annotated element during the source processing stage:

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

Of course, if the correct setters collection is empty, there is no point of continuing the current type element set iteration:

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

If the setters collection has at least one element, we’re going to use it to get the fully qualified class name from the enclosing element, which in case of the setter method appears to be the source class itself:

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

The last bit of information we need to generate a builder class is a map between the names of the setters and the names of their argument types:

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

6.3. Generating the Output File

Now we have all the information we need to generate a builder class: the name of the source class, all its setter names, and their argument types.

To generate the output file, we’ll use the Filer instance provided again by the object in the AbstractProcessor.processingEnv protected property:

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

The complete code of the writeBuilderFile method is provided below. We only need to calculate the package name, fully qualified builder class name, and simple class names for the source class and the builder class. The rest of the code is pretty straightforward.

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. Running the Example

To see the code generation in action, you should either compile both modules from the common parent root or first compile the annotation-processor module and then the annotation-user module.

The generated PersonBuilder class can be found inside the annotation-user/target/generated-sources/annotations/com/baeldung/annotation/PersonBuilder.java file and should look like this:

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. Alternative Ways of Registering a Processor

To use your annotation processor during the compilation stage, you have several other options, depending on your use case and the tools you use.

8.1. Using the Annotation Processor Tool

The apt tool was a special command line utility for processing source files. It was a part of Java 5, but since Java 7 it was deprecated in favour of other options and removed completely in Java 8. It will not be discussed in this article.

8.2. Using the Compiler Key

The -processor compiler key is a standard JDK facility to augment the source processing stage of the compiler with your own annotation processor.

Note that the processor itself and the annotation have to be already compiled as classes in a separate compilation and present on the classpath, so the first thing you should do is:

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

Then you do the actual compilation of your sources with the -processor key specifying the annotation processor class you’ve just compiled:

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

To specify several annotation processors in one go, you can separate their class names with commas, like this:

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

8.3. Using Maven

The maven-compiler-plugin allows specifying annotation processors as part of its configuration.

Here’s an example of adding annotation processor for the compiler plugin. You could also specify the directory to put generated sources into, using the generatedSourcesDirectory configuration parameter.

Note that the BuilderProcessor class should already be compiled, for instance, imported from another jar in the build dependencies:

   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. Adding a Processor Jar to the Classpath

Instead of specifying the annotation processor in the compiler options, you may simply add a specially structured jar with the processor class to the classpath of the compiler.

To pick it up automatically, the compiler has to know the name of the processor class. So you have to specify it in the META-INF/services/javax.annotation.processing.Processor file as a fully qualified class name of the processor:

com.baeldung.annotation.processor.BuilderProcessor

You can also specify several processors from this jar to pick up automatically by separating them with a new line:

package1.Processor1 package2.Processor2 package3.Processor3

If you use Maven to build this jar and try to put this file directly into the src/main/resources/META-INF/services directory, you’ll encounter the following error:

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

This is because the compiler tries to use this file during the source-processing stage of the module itself when the BuilderProcessor file is not yet compiled. The file has to be either put inside another resource directory and copied to the META-INF/services directory during the resource copying stage of the Maven build, or (even better) generated during the build.

The Google auto-service library, discussed in the following section, allows generating this file using a simple annotation.

8.5. Using the Google auto-service Library

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

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

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

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

В тази статия демонстрирахме обработка на анотации на ниво източник, използвайки пример за генериране на клас Builder за POJO. Представихме и няколко алтернативни начина за регистриране на процесори за анотиране във вашия проект.

Изходният код на статията е достъпен на GitHub.