De Annotation Processing Tool (APT)

15 September 2008 17:10 Michael van Oers Algemeen, Java

Het is nu bijna 4 jaar geleden dat in Java 5 de ondersteuning voor annotations ingebouwd is. Sindsdien zijn er legio toepassingen ontwikkeld die het leven van de programmeur een stuk eenvoudiger maken en daarmee veraangenamen. Denk alleen maar aan de Hibernate annotations waarmee een groot deel van de XML configuratie kan worden geëlimineerd of het gemak waarmee met annotations een webservice opgezet kan worden.

Maar hoe werken die annotations nu eigenlijk en wat kun je ermee? Dit artikel zal aan de hand van een simpele case een inleiding geven in het opzetten en gebruik van annotations. Vervolgens zal de nadruk gelegd worden op de pre-processing mogelijkheden in combinatie met APT (Annotation Processing Tool).

Let op, vanwege problemen met WordPress, de tool waarmee deze blog geschreven is, is het momenteel niet mogelijk stukken sourcecode netjes opgemaakt in deze blog te tonen.

De case

De case die binnen dit artikel uitgewerkt zal worden betreft het ontwikkelen van een annotation die het mogelijk maakt een EJB3 entity bean te voorzien van (SQL) commentaar. Met behulp van APT zullen deze annotations vervolgens verwerkt worden tot een SQL script.

Wat achtergrond informatie en een voorbeeld:

Sommige DBMS’s, waaronder Oracle en PostgreSQL, ondersteunen de mogelijkheid om ondermeer aan tabellen en kolommen commentaar toe te voegen. Hiervoor wordt het volgende SQL statement gebruikt :

COMMENT ON <TABLENAME>[.<FIELDNAME>] IS <COMMENT>;

De standaard JPA (Java Persistence API) annotations bieden geen mogelijkheid tot het toevoegen van commentaar, vandaar dat we die zelf zullen moeten ontwikkelen.

Een EJB3 entity bean voorzien van SQL commentaar annotations kan er als volgt uit gaan zien.

@Entity
@Table(name = "BLOG_ENTRY")
@SQLComment("Defines a single Blog entry.")
public class BlogEntry {
   @Id
   @Column(name = "ID", nullable = false)
   @SQLComment("The primary key.")
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Integer id;

   @ManyToOne
   @JoinColumn(name = "AUTHOR_ID", nullable = false)
   @SQLComment("Reference to the author of this blog.")
   private BlogAuthor author;

   @Temporal(TemporalType.TIMESTAMP)
   @Column(name = "CREATION_DATE", nullable = false)
   @SQLComment("Date of creation of this blog.")
   private Date date;

   @Column(name = "TITLE", nullable = false, length = 50)
   @SQLComment("Title of this blog.")
   private String title;

   @Lob
   @Column(name = "TEXT", nullable = false)
   @SQLComment("The contents.")
   private String text;

En voor de volledigheid ook even de BlogAuthor bean

@Entity
@Table(name = "BLOG_AUTHOR")
public class BlogAuthor {
   @Id

@Column(name = "ID", nullable = false) @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; @Column(name = "NAME", nullable = false, length=50) private String name;

Wanneer de BlogEntry bean met APT verwerkt wordt moet dit resulteren in het volgende SQL-script :

COMMENT ON BLOG_ENTRY IS 'Defines a single Blog entry.';
COMMENT ON BLOG_ENTRY.ID IS 'The primary key.';
COMMENT ON BLOG_ENTRY.AUTHOR_ID IS 'Reference to the author of this blog.';
COMMENT ON BLOG_ENTRY.CREATION_DATE IS 'Date of creation of this blog.';
COMMENT ON BLOG_ENTRY.TITLE IS 'Title of this blog.';
COMMENT ON BLOG_ENTRY.TEXT IS 'The contents.';

De SQLComment Annotation

De eerste stap is natuurlijk het ontwikkelen van de annotation. Deze kan erg eenvoudig blijven aangezien alleen het commentaar gedefinieerd hoeft te worden, alle overige informatie met betrekking tot tabel- en kolomnamen wordt immers al via de reeds aanwezige JPA annotations vastgelegd.

package com.finalist.blog.annotation;

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

@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface SQLComment {
   String value();
}

Met @Target(<ElementType…>) wordt beschreven voor welke elementen de SQLComment annotation gebruikt kan worden. In dit geval kan de annotation alleen gebruikt worden op Class en Field niveau (resp. ElementType.TYPE en ElementType.FIELD). Wanneer de annotation op enig ander element toegepast wordt zal dit resulteren in een compiler error.
Met @Retention wordt aangegeven wat de levensduur van de annotation is. Er zijn hier 3 waarden mogelijk :

  • RetentionPolicy.SOURCE
    De annotation wordt bij compilatie niet in de classfile opgenomen.
  • RetentionPolicy.CLASS
    De annotation wordt bij compilatie in de classfile opgenomen, maar niet in de JVM beschikbaar gesteld.
  • RetentionPolicy.RUNTIME
    De annotation wordt bij compilatie in de classfile opgenomen en in de JVM door middel van reflection beschikbaar gesteld.

Aangezien er geen reden is om het SQL commentaar in de classfiles op te nemen is hier RetentionPolicy.SOURCE gebruikt.

De Annotation Processing Tool (APT)

Nu de SQLComment annotation gedefinieerd is wordt het tijd om deze annotations te verwerken. Hiervoor wordt gebruik gemaakt van APT, een tool die standaard bij de JDK (vanaf 1.5) meegeleverd wordt. Zoals de naam al duidelijk maakt is het voornaamste doel van APT het verwerken van annotations in sourcecode.

Het moge duidelijk zijn dat APT zonder aanpassingen niets kan met de zojuist gecreëerde SQLComment annotation, het is aan de developer om het gewenste gedrag voor de annotation te definiëren. APT biedt door middel van de implementatie van een AnnotationProcessorFactory de mogelijkheid de ondersteuning voor “custom-made” annotations toe te voegen uit te breiden.

De volgende stap zal dus zijn om een AnnotationProcessorFactory voor onze SQLComment annotation te implementeren.

De SQLCommentAnnotationProcessorFactory

De implementatie van een custom factory voor APT is relatief eenvoudig. De enige vereiste is dat de factory de AnnotationProcessorFactory interface implementeert. Voor de SQLComment annotation zal de factory als volgt geïmplementeerd worden :

package com.finalist.blog.annotationprocessor;

import java.util.Collection;
import java.util.Collections;
import java.util.Set;

import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.AnnotationProcessorFactory;
import com.sun.mirror.apt.AnnotationProcessors;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;

public class SQLCommentAnnotationProcessorFactory implementsAnnotationProcessorFactory {

   public AnnotationProcessor getProcessorFor(
                     Set<AnnotationTypeDeclaration> declarations,
                     AnnotationProcessorEnvironment environment) {
      AnnotationProcessor result = AnnotationProcessors.NO_OP;
      if (!declarations.isEmpty()) {
         result = new SQLCommentAnnotationProcessor(environment);
      }
      return result;
   }

   public Collection<String> supportedAnnotationTypes() {
      return Collections.singletonList("com.finalist.blog.annotation.SQLComment");
   }

   public Collection<String> supportedOptions() {
      return Collections.emptyList();
   }
}

Deze factory instantieert een AnnotationProcessor, in dit geval een SQLCommentAnnotationProcessor. Meer over AnnotationProcessors verderop in dit artikel. Voor de volledigheid eerst de implementatie van de SQLCommentAnnotationProcessor :

package com.finalist.blog.annotationprocessor;

import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
import com.sun.mirror.declaration.Declaration;

public class SQLCommentAnnotationProcessor implements AnnotationProcessor {

   private AnnotationProcessorEnvironment environment;
   private AnnotationTypeDeclaration commentDeclaration;

   public SQLCommentAnnotationProcessor(AnnotationProcessorEnvironment env) {
      environment = env;
      commentDeclaration = (AnnotationTypeDeclaration) environment.getTypeDeclaration(
               com.finalist.blog.annotation.SQLComment.class.getName());
   }

   public void process() {
      for (Declaration decl : environment.getDeclarationsAnnotatedWith(commentDeclaration)) {
         System.out.println(String.format("[%10s] @ %s ", decl.getSimpleName(), decl.getPosition()));
      }
   }
}

Even een korte uitleg over 3 methods die voor iedere AnnotationProcessorFactory geïmplementeerd moeten worden.

  • Collection<String> supportedAnnotationTypes()
    Hiermee wordt aangegeven voor welke annotations deze factory gebruikt kan worden, in dit geval alleen de SQLComment annotation.
  • Collection<String> supportedOptions()
    Hiermee is het mogelijk aan te geven welke factory specifieke (commandline)opties ondersteund worden. In dit geval worden geen opties ondersteund, maar het is denkbaar dat er opties toegevoegd kunnen worden die het output format (TXT, SQL) of het SQL dialect vastleggen.
  • AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration>, AnnotationProcessorEnvironment)
    Hier wordt de daadwerkelijke AnnotationProcessor aangeroepen. Op basis van de meegegeven AnnotationTypeDeclarations en de AnnotationProcessorEnvironment wordt bepaald welke AnnotationProcessor gebruikt dient te worden. Aangezien slechts de SQLComment annotation ondersteund wordt kan er zonder controle een SQLCommentAnnotationProcessor teruggegeven worden.

In bovenstaande situatie wordt voorlopig een dummy implementatie van de SQLCommentAnnotationProcessor teruggegeven, deze implementatie zal verder in dit artikel vervangen en uitgebreid worden door een implementatie met meer functionaliteit.

Wanneer de betrokken files gecompileerd zijn kan APT voor het eerst op de BlogEntry.java file losgelaten worden. (Let op, voor compilatie is in ieder geval de ejb3-persistence.jar nodig.)

Na het uitvoeren van :

apt -classpath .;ejb3-persistence.jar -factory com.finalist.blog.annotationprocessor.
SQLCommentAnnotationProcessorFactory com/finalist/blog/entity/BlogEntry.java

is onderstaande het resultaat :

warning: Annotation types without processors: [javax.persistence.Entity, javax.persistence.
Table, javax.persistence.Id, javax.persistence.Column, javax.persistence.GeneratedValue, ja
vax.persistence.ManyToOne, javax.persistence.JoinColumn, javax.persistence.Temporal, javax.
persistence.Lob]
[      text] @ ./src/com/finalist/blog/entity/BlogEntry.java:46
[      date] @ ./src/com/finalist/blog/entity/BlogEntry.java:37
[    author] @ ./src/com/finalist/blog/entity/BlogEntry.java:32
[     title] @ ./src/com/finalist/blog/entity/BlogEntry.java:41
[ BlogEntry] @ ./src/com/finalist/blog/entity/BlogEntry.java:22
[        id] @ ./src/com/finalist/blog/entity/BlogEntry.java:27
1 warning

Op zich niet echt spannend, maar het is een begin. De warning die getoond wordt is het gevolg van het feit dat er (JPA) annotations gevonden zijn die niet door onze AnnotationProcessorFactory opgepakt worden.

De volgende stap is nu de SQLCommentAnnotationFactory te vervangen door een implementatie die daadwerkelijk de beoogde output genereert.

De SQLCommentAnnotationProcessor

Om een eigen annotation processor te schrijven is het voldoende de AnnotationProcessor interface (en bijbehorende process() method) te implementeren. De voorgaande paragraaf toont de implementatie van een erg simpele en weinig functionele annotation processor. Voor de implementatie van de in dit artikel beoogde functionaliteit is iets meer werk nodig, maar alles blijft desondanks toch verrassend eenvoudig en begrijpelijk.

package com.finalist.blog.annotationprocessor;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;

import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.apt.Filer;
import com.sun.mirror.declaration.AnnotationMirror;
import com.sun.mirror.declaration.AnnotationTypeDeclaration;
import com.sun.mirror.declaration.AnnotationTypeElementDeclaration;
import com.sun.mirror.declaration.AnnotationValue;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.FieldDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.util.DeclarationVisitor;
import com.sun.mirror.util.DeclarationVisitors;
import com.sun.mirror.util.SimpleDeclarationVisitor;

public class SQLCommentAnnotationProcessor implements AnnotationProcessor {

   private AnnotationProcessorEnvironment environment;
   private AnnotationTypeDeclaration commentDeclaration;
   private AnnotationTypeDeclaration tableDeclaration;
   private AnnotationTypeDeclaration columnDeclaration;
   private AnnotationTypeDeclaration joinColumnDeclaration;

   public SQLCommentAnnotationProcessor(AnnotationProcessorEnvironment env) {
      environment = env;
      commentDeclaration = (AnnotationTypeDeclaration) environment
            .getTypeDeclaration(SQLComment.class.getName());
      tableDeclaration = (AnnotationTypeDeclaration) environment
            .getTypeDeclaration(javax.persistence.Table.class.getName());
      columnDeclaration = (AnnotationTypeDeclaration) environment
            .getTypeDeclaration(javax.persistence.Column.class.getName());
      joinColumnDeclaration = (AnnotationTypeDeclaration) environment
            .getTypeDeclaration(javax.persistence.JoinColumn.class.getName());
   }

   public void process() {
      PrintWriter pw = null;
      try {
         // creeer de output file.
         pw = environment.getFiler().createTextFile(Filer.Location.SOURCE_TREE,
                  "", new File("comments.sql"), null);

        // instantieer de visitor en koppe deze aan de sourcecode scanner
         SQLCommentAnnotationVisitor SCAV = new SQLCommentAnnotationVisitor(pw);
         DeclarationVisitor scanner = DeclarationVisitors
                  .getSourceOrderDeclarationScanner(SCAV, DeclarationVisitors.NO_OP);

         // verwerk alle classes waar SQLComment annotations gevonden zijn.
         for (TypeDeclaration declaration : environment.getTypeDeclarations()) {
            declaration.accept(scanner); 
         }
      } catch (IOException e) {
         // ......
      } finally {
         if (pw != null) {
            pw.close();
         }
      }
   }

   private String getAnnotationValue(AnnotationMirror mirror, String property) {
      for (Map.Entry<AnnotationTypeElementDeclaration, AnnotationValue> entry : mirror
                  .getElementValues().entrySet()) {
         if (property.equals(entry.getKey().getSimpleName())) {
            return (String) entry.getValue().getValue();
         }
      }
      return null;
   }

   private class SQLCommentAnnotationVisitor extends SimpleDeclarationVisitor {
      private String tableName;
      private PrintWriter writer;

      public SQLCommentAnnotationVisitor(PrintWriter pw) {
         this.writer = pw;
      }

      /*
      * Verwerkt annotaties op Class niveau.
      */
      public void visitClassDeclaration(ClassDeclaration declaration) {
         String comment = null;

         // bekijk alle beschikbare annotations
         for (AnnotationMirror mirror : declaration.getAnnotationMirrors()) {

            // Is het een @Table annotation, dan de tabelnaam (name) ophalen.
            if (mirror.getAnnotationType().getDeclaration().equals(tableDeclaration)) {
               tableName = getAnnotationValue(mirror, "name");
            }

            // Is het een @SQLComment annotation, dan het commentaar (value) ophalen.
            if (mirror.getAnnotationType().getDeclaration().equals(commentDeclaration)) {
               comment = getAnnotationValue(mirror, "value");
            }
         }

         // en het script wegschrijven indien nodig.
         if (tableName != null && comment != null)
            writer.println(String.format("COMMENT ON %s IS '%s';",tableName, comment));
      }

      public void visitFieldDeclaration(FieldDeclaration declaration) {
         // als er geen @Table annotation is dan valt er niets te doen.
         if (tableName != null) {
            String comment = null;
            String columnName = null;

            // bekijk alle beschikbare annotations
            for (AnnotationMirror mirror : declaration.getAnnotationMirrors()) {

               // Is het een @Column annotation, dan de veldnaam (name) ophalen.
               if (mirror.getAnnotationType().getDeclaration().equals(columnDeclaration)) {
                  columnName = getAnnotationValue(mirror, "name");
               }

               // Is het een @JoinColumn annotation, dan de veldnaam (name) ophalen.
               if (mirror.getAnnotationType().getDeclaration().equals(joinColumnDeclaration)) {
                  columnName = getAnnotationValue(mirror, "name");
               }

               // Is het een @SQLComment annotation, dan het commentaar (value) ophalen.
               if (mirror.getAnnotationType().getDeclaration().equals(commentDeclaration)) {
                  comment = getAnnotationValue(mirror, "value");
               }
            }
            // en het  script wegschrijven indien nodig.
            if (columnName != null && comment != null)
               writer.println(String.format("COMMENT ON %s.%s IS '%s';",tableName, columnName, comment));
         }
      }
   }
}

De eenvoud van de implementatie schuilt in het feit dat in de verwerking van de annotations binnen APT gebruik is gemaakt van het Visitor Pattern. Wanneer een sourcefile door APT verwerkt wordt zal voor iedere declaratie (package, class, field, method etc.) in de desbetreffende method in de visitor implementatie worden aangeroepen. Het is natuurlijk ook mogelijk eigenhandig de declaration boomstructuur van een sourcefile te doorlopen, maar dit is onnodig complex en in de regel niet noodzakelijk. Voor het gemak biedt APT standaard een ‘do-nothing’ declaration visitor implementatie genaamd SimpleDeclarationVisitor welke hier gebruikt is als basis voor de SQLCommentAnnotationVisitor.

In dit geval kan de implementatie zich beperken tot het ‘overriden’ van methods voor declaraties van het type ClassDeclaration en FieldDeclaration, dit zijn immers precies de declaraties die door de SQLComment annotation worden ondersteund.

De belangrijkste stappen binnen bovenstaande implementatie zullen hierna kort beschreven worden :

  • Binnen de process() method wordt allereerst een outputfile aangemaakt. De APT omgeving biedt een Filer waarmee het mogelijk is verschillende typen outputfiles aan te maken. In dit geval volstaat het aanmaken van een standaard textfile, maar het is ook mogelijk om onder andere sourcefiles of classfiles aan te maken.
  • De SQLCommentAnnotationVisitor wordt geïnstantieerd en gekoppeld aan een scanner die de de gegeven sourcefiles van begin tot eind doorloopt.
  • Vervolgens worden alle van SQLComment annotations voorziene (class)declaraties gescand met behulp van de eerder aangemaakte Visitor implementatie.

De implementatie van de verschillende visit……Decalaration-methods in de SQLCommentAnnotationVisitor zijn erg eenvoudig. Voor beide methods geldt dat alle beschikbare annotations voor de gevonden declaration worden doorlopen waarbij de voor de implementatie benodigde annotations en waarden uitgelezen worden.

Conclusie

Aan de hand van deze eenvoudige case blijkt dat het werken met APT en het verwerken van source-level annotations helemaal niet zo complex is. Natuurlijk is dit een erg eenvoudige case en is slechts een fractie van de mogelijkheden van APT en gerelateerde onderwerpen beschreven. Wanneer men verder in dit onderwep duikt wordt aangeraden om gebruik te maken van de eerder beschreven visitor-pattern oplossing omdat dat een goede afscherming biedt van de, toch wel, complexere onderliggende declaratie boomstructuur.

2 reacties »

  1. Wat je in de meeste frameworks ziet is dat alle annotations worden gedeclareerd met een runtime-retention, waarna er standaard reflectionmethodes (isAnnotationPresent, getAnnotation) worden gebruikt. Hierbij gaat een aardigheid van APT verloren (je stipt hem al even aan): je kunt ook Java-bestanden genereren, die in een volgende pass weer worden verwerkt. Wie tijd over heeft zou dus xdoclet opnieuw kunnen bouwen met annotations.

    Een nadeel van APT is dat je erg veel code nodig hebt om ook maar de simpelste dingen te kunnen doen. En als je dingen als Java- of HTML-bestanden gaat genereren moet je spontaan weer aan je eerste servlets denken: je code staat vol met printlns. Beide problemen worden opgelost door apt-jelly (http://apt-jelly.sourceforge.net/), wat je ook mooi de gelegenheid geeft weer eens met Jelly te werken :)

    Levi Hoogenberg September 15, 2008 18:51

  2. Really good work about this website was done. Keep trying more – thanks!

    Yahoouj February 23, 2010 6:29

Reageer

RSS feed for comments on this post · TrackBack URI