AnnotationProcessor - Basics

Seit dem JDK 1.5 gibt es die Annotationen (JSR 175) das Compiler Plugin Annotation Processor. Das Plugin ermöglicht es einem Annotationen bei dem Übersetzungsvorgang auszuwerten. Die Einsatzmöglichkeiten sind vielfältig und reichen von Fehler-/Warnungsunterdrückung beim Kompilieren bis hin zum Generieren von Quelltext. Letzteres werden wir uns hier nun ansehen, da man es im API-Design sehr gut einsetzen kann.

Eine der Einschränkungen, die man von Beginn an mit berücksichtigen sollte, ist: Der mit der Ziel-Annotation versehene Quelltext kann nicht modifiziert werden. Ebenfalls kann man generierte Klassen zu diesem Zeitpunkt mittels Reflection noch nicht untersuchen.

Allerdings kann man Annotation-Processing geschachtelt verwenden. Ein Beispiel werde ich hier verwenden.

Projektaufbau

Ich verwende Maven um den Build-Prozess zu definieren. Mit anderen Werkzeugen sollte es ebenfalls gehen, werde ich hier aber nicht berücksichtigen.

Die Annotationen, die später verwendet werden sollen, kommen in ein eigenes Modul. Damit kann man ausschließlich diese in den Ziel-Modulen verwenden. Der benötigte Processor kommt in ein weiteres Modul, das eine Abhängigkeit zu dem Annotations-Modul enthält. Nachfolgend sind die notwendigen pom.xml - Dateien zu sehen. Die Verzeichnisstruktur ist der Konvention entsprechend aufgebaut.

Parent pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.rapidpm.demo</groupId>
        <artifactId>genericbasics</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>processors</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.rapidpm.demo</groupId>
            <artifactId>annotations</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- Auto service annotation processor -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc2</version>
            <!--<optional>true</optional>-->
        </dependency>
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Annotationen Modul - pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.rapidpm.demo</groupId>
        <artifactId>genericbasics</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>annotations</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Processor Modul - pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.rapidpm.demo</groupId>
        <artifactId>genericbasics</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>processors</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.rapidpm.demo</groupId>
            <artifactId>annotations</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- Auto service annotation processor -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc2</version>
            <!--<optional>true</optional>-->
        </dependency>
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Annotationen

Nun definieren wir die Annotation in dem Modul annotations. In diesem Fall erstellen wir eine einfache Annotation @HelloWorld.

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

Die Annotation selbst ist annotiert mit

  • @Target(ElementType.TYPE), dieses bewirkt, dass die Annotation nur auf Klassenebene verwendet werden kann.
  • @Retention(RetentionPolicy.SOURCE), was zur Folge hat, dass die Annotation zur Laufzeit nicht mehr vorhanden ist. Es kann also nicht mittels Reflection darauf reagiert werden.

Processors

Der nächste Schritt besteht darin, die Verarbeitung dieser Annotation zu implementieren. Hierzu wird in dem Modul processors eine Klasse mit dem Namen AnnotationProcessor erzeugt, die von dem AbstractProcessor ableitet. Wir können nun verschiedene Methoden überschreiben und müssen mindestens die Methode process(..) implementieren.

public abstract boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv);

In diesem Beispiel wird eine Minimalimplementierung demonstriert. In der init(..) Methode werden die übergebenen Parameter lediglich als Klassenattribute für eine spätere Verwendung zwischengelagert. Die Methode getSupportedSourceVersion() kann eingesetzt werden, um die Verwendung dieser Verarbeitung auf bestimmte Java-Versionen zu begrenzen. Diese Angabe ist auch als Annotation an dem AnnotationProcessor selbst möglich. Spannend wird es erst bei der Implementierung der Methode process(..).

@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {

  private Types typeUtils;
  private Elements elementUtils;
  private Filer filer;
  private Messager messager;

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    Set<String> annotations = new LinkedHashSet<String>();
    annotations.add(HelloWorld.class.getCanonicalName());
    return annotations;
  }

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    typeUtils = processingEnv.getTypeUtils();
    elementUtils = processingEnv.getElementUtils();
    filer = processingEnv.getFiler();
    messager = processingEnv.getMessager();
  }

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for(Element annotatedElement : roundEnv.getElementsAnnotatedWith(HelloWorld.class)){
      System.out.println("annotatedElement = " + annotatedElement);
    }
    return true;
  }

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }
}

Hier bekommt man alle Informationen zu der Stelle, an der die jeweilige Annotation gefunden worden ist. Die Informationen sind ein wenig geschachtelt in der Instanz RoundEnvironment roundEnv vorhanden. Bei den ersten Versuchen wird man hier sicherlich mittels Versuch und Irrtum Erfahrungen sammeln. Wir wollen hier an der Stelle lediglich auf der Kommandozeile die gefundene Stelle ausgeben um zu prüfen ob es dem entspricht was wir erwarten.

Die Klasse AnnotationProcessor ist selbst mit der Annotation @AutoService(Processor.class) versehen. Das hat folgende Aufgabe: Normalerweise muss man einen AnnotationProcessor unter META-INF/services selber registrieren. Da dieses aber fehleranfällig ist, kann man auf das Projekt Google - Auto zurückgreifen. Die Annotation selbst startet einen AnnotationProcessor, der die entsprechende Datei (javax.annotation.processing.Processor) unter META-INF/services anlegt und den Processor dort registriert.

Es kann also ein AnnotationProcessor im AnnotationProcessor verwendet werden ;-) Um den AnnotationProcessor nun verwenden zu können ist nur noch ein mvn clean install notwendig.

Die Verwendung

Nun haben wir alles zusammen um ein HelloWorld zu realisieren. Wir erzeugen in dem Modul usinggenerated die Klasse HelloWorldAnnotation und annotieren diese mit @HelloWorld. Ein nachfolgendes mvn clean install lässt zwischen den Maven-Meldungen die folgende Meldung erscheinen:

annotatedElement = org.rapidpm.demo.annotationprocessing.HelloWorldAnnotation

Damit sehen wir, dass die Annotation ausgewertet worden ist.

results matching ""

    No results matching ""