Navigation

Annotation Processing Tool

Un article de ODcWiki.

Jump to: navigation, search
Image:Auteur.png Auteur : gquintana.


Les annotations ont été introduites avec Java 5 pour pouvoir placer des méta-données sur les classes, attributs, méthodes.... Ces annotations peuvent par la suite être exploitées à l'exécution ou bien à la compilation. C'est cette dernière façon de faire qui motive cet article: Nous allons voir comment traiter les sources Java pour en extraire les (méta-)informations, puis générer du code, de la documentation ou autre...

Depuis Java 5, un outil nommé APT comme Annotation Processing Tool (à ne pas confondre avec le gestionnaire de paquet des distributions Linux Debian, Ubuntu...) a été introduit dans le JDK pour traiter les annotations présentes dans le code source Java. Pour s'interfacer avec lui, il faut développer un processeur d'annotations en utilisant l'API spécifique placée dans le package com.sun.mirror.

Depuis Java 6, un travail de standardisation a été mené (JSR 269), l'API standardisée se retrouve dans les packages javax.annotation.processing et javax.lang.model. Le traitement des annotations est à présent intégrée à la compilation javac.

Pour comprendre le fonctionnement de cet outil, nous allons partir d'un ensemble d'EJB 3 Entités (de simples objets Java avec des annotations JPA) pour en extraire un résumé. Nous verrons les deux variantes:

  • Java 5 + API spécifique com.sun.mirror.* + outil APT
  • Java 6 + API standard javax.* + compilateur JavaC

Sommaire

Création du processeur

Pour écrire un processeur d'annotations, il faudra implémenter/étendre les interfaces/classes suivantes:

Java 5 Java 6+ Description
AnnotationProcessorFactory N'existe pas Fabrique un ou plusieurs AnnotationProcessor, c'est le point d'entrée d'APT.
AnnotationProcessor Processor ou AbstractProcessor Fabrique un visiteur pour traiter les classes, c'est le coeur du traitement des annotations
DeclarationVisitor ElementVisitor Visite (design pattern Visiteur) le code Java de proche en proche.

Variante Java 5

La première étape est d'ajouter dans le class path, le tools.jar qui se trouve dans le répertoire lib du JDK, afin de pouvoir utiliser les classes du package en com.sun.mirror. Ensuite il faudra écrire 3 classes: une fabrique de processeurs, un processeur et un visiteur.

Fabrique de processeurs

public class PrintAnnotationProcessorFactory implements AnnotationProcessorFactory {
	final Collection<String> supportedAnnotationTypes=Arrays.asList("javax.persistence.*");
	public Collection<String> supportedAnnotationTypes() {
		return supportedAnnotationTypes;
	}
	
	final Collection<String> supportedOptions=Collections.emptyList();
	public Collection<String> supportedOptions() {
		return supportedOptions;
	}
 
	public AnnotationProcessor getProcessorFor(
			Set<AnnotationTypeDeclaration> annotationTypeDeclarations, 
			AnnotationProcessorEnvironment environment) {
		return new PrintAnnotationProcessor(annotationTypeDeclarations,environment);
	}
}

L'implémentation d'AnnotationProcessorFactory indique les annotations qu'elle sait traiter et les options qui, passées depuis la ligne de commande, sont supportées. Puis il fabrique le processeur d'annotation auquel il délègue le boulot.

Processeur

public class PrintAnnotationProcessor implements AnnotationProcessor {
	Set<AnnotationTypeDeclaration> annotationTypeDeclarations;
	AnnotationProcessorEnvironment environment;
	
	public PrintAnnotationProcessor(
			Set<AnnotationTypeDeclaration> annotationTypeDeclarations,
			AnnotationProcessorEnvironment environment) {
		this.annotationTypeDeclarations = annotationTypeDeclarations;
		this.environment = environment;
	}
	
	public void process() {
		PrintDeclarationVisitor printDeclarationVisitor=new PrintDeclarationVisitor();
		for(TypeDeclaration typeDeclaration:environment.getSpecifiedTypeDeclarations()) {
			typeDeclaration.accept(printDeclarationVisitor);
		}
	}
}

L'implémentation 'AnnotationProcessor instancie un visiteur qu'elle fait passer dans chacune des déclarations trouvées dans l'environnement.

Les objets Declaration forment une hierarchie de classe qui décrit toutes les possibilités du language Java. Elles contiennent toutes les informations sur le code Java: par exemple les annotations sur une classe, le commentaire JavaDoc sur une méthode, le numéro de ligne d'un attribut.

Visiteur

public class PrintDeclarationVisitor extends SimpleDeclarationVisitor {
	@Override
	public void visitClassDeclaration(ClassDeclaration classDeclaration) {
		if (classDeclaration.getAnnotation(Entity.class)!=null) {
			System.out.println("Entity "+classDeclaration.getSimpleName()
				+": "+classDeclaration.getDocComment());
			for(FieldDeclaration fieldDeclaration:classDeclaration.getFields()) {
				fieldDeclaration.accept(this);
			}
			System.out.println();
		}
	}
	@Override
	public void visitFieldDeclaration(FieldDeclaration fieldDeclaration) {
		System.out.println("- Field "+fieldDeclaration.getSimpleName()
			+": "+fieldDeclaration.getDocComment());
		super.visitFieldDeclaration(fieldDeclaration);
	}	
}

L'implémentation de DeclarationVisitor (étends SimpleDeclarationVisitor par souci de simplicité) contient une méthode pour chaque type de Declaration qu'elle peut potentiellement visiter. Dans chacune d'elles, on choisit s'il faut continuer la visite (de la classe vers les méthodes, des méthodes vers les paramètres...) ou bien s'arrêter.

Variante Java 6+

Processeur

@SupportedAnnotationTypes({"javax.persistence.*"})
@SupportedOptions({"outputFile"})
public class PrintAnnotationProcessor extends AbstractProcessor {
	private ProcessingEnvironment environment;
	@Override
	public void init(ProcessingEnvironment environment) {
		this.environment = environment;
	}
	
	public boolean process(
			Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
		PrintElementVisitor printElementVisitor=new PrintElementVisitor(environment);
		for(Element element:roundEnvironment.getElementsAnnotatedWith(Entity.class)) {
			element.accept(printElementVisitor,null);
		}
		return false;
	}
}

Le processeur implémente Processor (étends AbstractProcessor pour faire simple). Les annotations @SupportedAnnotationTypes et @SupportedOptions sur le processeur remplacent les méthodes du même nom dans la fabrique de processeurs de l'exemple Java 5.

Une hiérarchie d'Element remplace la hiérarchie de Declaration de l'API Java 5.

Visiteur

public class PrintElementVisitor implements ElementVisitor<Void, Void> {
	ProcessingEnvironment environment;
	public PrintElementVisitor(ProcessingEnvironment environment) {
		this.environment=environment;
	}
	@Override
	public Void visit(Element e,Void p) {
		switch(e.getKind()) {
		case CLASS:
			visitClass(e,p);
			break;
		case FIELD:
			visitField(e,p);
			break;
		default:
			visitUnknown(e, p);
		}
		return null;
	}
	private void visitClass(Element classElement,Void p) {
		if (classElement.getAnnotation(Entity.class)!=null) {
			System.out.println("Entity "+classElement.getSimpleName()+": "
				+environment.getElementUtils().getDocComment(classElement));
			for(Element enclosedElement:classElement.getEnclosedElements()) {
				enclosedElement.accept(this,p);
			}
			System.out.println();
		}
	}
	private void visitField(Element fieldElement,Void p) {
		System.out.println("Field "+fieldElement.getSimpleName()+": "
			+environment.getElementUtils().getDocComment(fieldElement));
	}
	(...)
}

Le visiteur implémente ElementVisitor, l'absence de classe de mère pour implémenter les méthodes de base rend le code plus verbeux. Notez aussi que certains éléments du langage (package contenant, commentaires JavaDoc..) ne sont accessibles que via une classe utilitaire nommée ElementUtils. Enfin, les paramètres de l'interface ElementVisitor permettent de typer un paramètre et une valeur de retour.

Lancement du processeur

En ligne de commande

Dans Java 5, il faut passer par l'outil apt. A partir de Java 6, le compilateur javac se charge du traitement des annotations:

apt -factory com.objetdirect.apt.PrintAnnotationProcessorFactory src/com/objetdirect/entity/*
 
javac  -processor com.objetdirect.apt.PrintAnnotationProcessor -d classes src/com/objetdirect/entity/*

Les options prises en compte par APT sont similaires à celles de JavaC, à tel point que, par défaut, APT compile les sources en même temps qu'il traite les annotations! Voici les principales options disponibles:

Java 5 APT Java 6 JavaC Description
-classpath chemin ou -cp chemin Endroit où se trouvent les classes à traiter, et éventuellement le processeur d'annotations
-factorypath chemin -processorpath chemin Endroit où se trouvent les classes du processeur d'annotations
-factory Classe -processor Classe Nom de l'AnnotationProcessorFactory ou du à utiliser
-d dossier Endroit où seront placées les classes compilées
-s dossier Endroit où seront placées les sources générées
-source version Version des sources (1.5, 1.6...)
-A[clef[=valeur]] Options spécifiques pour un processeur d'annotation
-nocompile N'existe pas Ne compile pas les sources (ne génère pas les .class)

Avec Ant

L'invocation de l'outil APT depuis Ant est intégrée en standard, les options sont les mêmes que dans la ligne de commande:

<target name="apt" depends="javac">
	<apt classpathref="class.path"
		factorypathref="factory.class.path"
		srcdir="src" destdir="classes" preprocessdir="generated-src"
		compile="true"
		factory="com.objetdirect.apt.PrintAnnotationProcessorFactory"
	/>
</target>

Les options pour indiquer le processeur d'annotations à utiliser (processor, processorpath...) ne sont pas accessibles dans Ant 1.7, mais il est quand même possible de se débrouiller sans:

<target name="javac">
	<javac srcdir="src" destdir="classes" classpathref="class.path">
		<compilerarg line="-processor com.objetdirect.apt.PrintAnnotationProcessor"/>
	</javac>
</target>

Accéder à l'environnement

Lors de l'invocation un objet environnement est passé au processeur d'annotations, il permet d'accéder au contexte d'appel de l'outil.

Java 5 Java 6+ Description
AnnotationProcessorEnvironment ProcessingEnvironment Accéde et manipule l'environnement de l'outil
Filer Filer Accessible depuis l'environnement, il permet l'écriture de fichiers
Messager Messager Accessible depuis l'environnement, il l'affichage de traces, de message d'alerte ou d'erreur

Les options

On peut récupérer les paramètres d'appel de l'outil en utilisant la méthode getOptions() de l'environnement, elle renvoit une Map<String,String> contenant les options. Les options commençant par -A ne sont pas interprétées par les outils apt et javac et sont réservées pour configurer un processeur d'annotations de manière spécifique. Autrement dit on est libre de mettre ce que l'on veut dedans et paramétrer un le processeur à sa guise.

Variante Java 5

Si on appelle:

apt -classpath CP -factory F -AoutputFile=output.txt -nocompile ..

On récupère une Map contenant toutes les options

Clef Valeur
-classpath CP
-factory F
-AoutputFile=output.txt null
-nocompile null

Que l'on accède de la manière suivante:

String classpath=environment.getOptions().get("-classpath");

Variante Java 6+

Si on appelle:

javac -classpath CP -processor P -AoutputFile=output.txt  ..

On obtient une Map contenant uniquement les options spécifiques

Clef Valeur
outputFile output.txt

Que l'on accède de la manière suivante:

String outputFile=environment.getOptions().get("outputFile");

A condition d'avoir mis une annotation @SupportedOptions({"outputFile"}) sur le processeur.

Les fichiers

L'environnement donne accès à un objet Filer qui permet de créer des fichiers sources ou compilés, textuels ou binaires qui seront gérées par APT. Il renvoit des OutputStream ou des Writer prêts à être remplis.

// Java 5
environment.getFiler().createTextFile(
    Location.SOURCE_TREE,                            // (1)
    typeDeclaration.getPackage().getQualifiedName(), // (2)
    new File("out.txt"),                             // (3)
    "UTF-8"); // Jeu de caractères
 
// Java 6+
environment.getFiler().createResource(
    StandardLocation.SOURCE_OUTPUT,                  // (1)
    environment.getElementUtils().getPackageOf(element).getQualifiedName(), // (2)
    "out.txt"                                        // (3)
).openWriter();
  1. Dossier source (option -s) ou classes (option -d) dans lequel sera placé le fichier généré
  2. Package où sera mis le fichier
  3. Nom du fichier produit

Rien n'empêche non plus de créer des fichiers par les moyens classiques, ni même d'écrire dans une base de données...

Les traces

De la même manière que précédemment, on trouve dans l'environnement un objet Messager qui permet d'émettre des messages d'information, d'alerte ou d'erreur. C'est une façon de produire des logs qui s'apparente à Log4J ou java.util.logging:

// Java 5
environment.getMessager().printWarning("Attention option invalide");
 
// Java 6+
environment.getMessager().printMessage(Kind.WARNING, "Attention option invalide");

Ici non plus, rien n'empêche d'utiliser les API de logging habituelles.

Liens

Boîte à outils
Dernière modification de cette page le 6 juillet 2009 à 14:08