Annotation Processing Tool
Présentation
Annotation Processing Tool existe depuis Java Standard Edition 5.0.
L'éxécutable se situe dans le répertoire /bin du SDK5.0.
APT permet le traitement avant compilation des sources Java. C'est une API réflective basée sur les sources .java.
L'API n'a qu'une vue read-only des sources originales. En aucun cas APT ne peux modifier les sources elle-mêmes. Par contre il est possible de générer des sources ou autres fichiers (au format XML par exemple).
L'utilisation d'APT nécessite la librairie tools.jar dans lib/ du SDK1.5.
Fonctionnement
APT fonctionne en utilisant le design pattern Visitor afin de parcourir les fichiers source .java. . Ce design pattern est souvent utiliser par les compilateurs ou en langage objet quand on veux dissocier la structure d'une collection d'objet, des opérations qu'on peut effectuer sur cette même collection.
Plus d'information sur le design pattern Visitor ici (javaworld.com).
APT utilise trois interfaces :
- AnnotationProcessorFactory (fournit des objets AnnotationProcessor)
- AnnotationProcessor (fournit le traitement des sources)
- DeclarationVisitor (traite les déclarations des sources et est appelé par l'AnnotationProcessor)
Exemple
Cette exemple illustre par la pratique le fonctionnement d'APT. Ici le but est de générer de nouvelle classe .java à partir des sources originales. L'exemple traite une annotation qui ne se nomme @NotNullParameter qui ne prends pas de paramêtres et s'appliquent à des méthodes.
package assertion; import java.lang.annotation.ElementType; import java.lang.annotation.Target; @Target(ElementType.METHOD) public @interface NotNullParameter { }L'objectif est de générer des nouvelles sources dont du code a été inséré. Une méthode annotée avec @NotNullParameter contiendra une vérification en début de traitement et lévera une exception NullParameterException.
AnnotatioProcessorFactory
Cette factory ne nous retournera qu'un seul type de AnnotationProcessor.package assertion; import java.util.Arrays; 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.declaration.AnnotationTypeDeclaration; public class AssertionProcessorFactory implements AnnotationProcessorFactory{ private CollectionIl est possible de passer des arguments à apt et de traiter différement le contenu en fonction des arguments. De plus apt cherche à initialiser une factory en fonction des types d'annotations supportés par la factory. (Il est donc possible d'utiliser plusieurs AnnotationProcessorFactory !).supportedTypes = Collections.unmodifiableCollection(Arrays.asList( NotNullParameter.class.getName())); private Collection supportedOptions = Collections.unmodifiableCollection(Arrays.asList("")); public Collection supportedOptions() { return supportedOptions; } public Collection supportedAnnotationTypes() { return supportedTypes; } public AnnotationProcessor getProcessorFor(Set arg0, AnnotationProcessorEnvironment arg1) { return new AssertionProcessor(arg1); } }
Ici la factory renverra un AssertionProcessor.
La méthode getProcessorFor est appelé par apt une fois validé les types d'annotations supportées (par la méthode supportedAnnotationTypes() ) et une fois validé les options (par la méthode supportedOptions() ).
APT initialisera l'environnement dans un objet de type AnnotationProcessorEnvironment. Il permet en autres d'écrire des messages dans la console système lançant APT.
AnnotationProcessorFactory
APT exécutera la méthode process de l'AnnotationProcessor renvoyé par la factory.
Cette méthode lance le traitement des sources. C'est à partir d'ici que commence le traitement des sources.
Dans notre exemple, on parcourt simplement les sources .java une par une afin de les traiter avec le design pattern Visitor.
package assertion; import com.sun.mirror.apt.AnnotationProcessor; import com.sun.mirror.apt.AnnotationProcessorEnvironment; import com.sun.mirror.declaration.TypeDeclaration; public class AssertionProcessor implements AnnotationProcessor{ AnnotationProcessorEnvironment env; public AssertionProcessor(AnnotationProcessorEnvironment env){ this.env=env; } public void process() { for(TypeDeclaration decl : env.getSpecifiedTypeDeclarations()){ decl.accept(new AssertionMethodVisitor(env)); } } }Remarque : Il n'est pas obligatoire de créer un nouveau Visiteur à chaque passage...l'exemple reste trivial.
DeclarationVisitor
L'interface DeclarationVisitor déclare beacoup de méthodes qui permettent de traiter chaque type de déclarations. Nous n'uliserons que celles qui nous interessent pour notre exemple.
L'object Messager est récuperé depuis l'AnnotationProcessorEnvironment. Il permet d'écrire des messages sur la console.
package assertion; import java.io.*; import java.lang.reflect.*; import java.util.*; import com.sun.mirror.apt.*; import com.sun.mirror.declaration.*; import com.sun.mirror.type.*; import com.sun.mirror.util.*; import com.sun.tools.apt.mirror.type.*; public class AssertionMethodVisitor implements DeclarationVisitor{ private AnnotationProcessorEnvironment env; private Messager m; public AssertionMethodVisitor(AnnotationProcessorEnvironment env){ this.env=env; m=this.env.getMessager(); } public void visitDeclaration(Declaration arg0) {} public void visitPackageDeclaration(PackageDeclaration arg0) {} public void visitMemberDeclaration(MemberDeclaration arg0) {} public void visitTypeDeclaration(TypeDeclaration arg0) {} public void visitEnumDeclaration(EnumDeclaration arg0) {} public void visitInterfaceDeclaration(InterfaceDeclaration arg0) {} public void visitAnnotationTypeDeclaration(AnnotationTypeDeclaration arg0) {} public void visitFieldDeclaration(FieldDeclaration arg0) {} public void visitEnumConstantDeclaration(EnumConstantDeclaration arg0) {} public void visitExecutableDeclaration(ExecutableDeclaration arg0) {} public void visitConstructorDeclaration(ConstructorDeclaration arg0) {} public void visitAnnotationTypeElementDeclaration(AnnotationTypeElementDeclaration arg0) {} public void visitParameterDeclaration(ParameterDeclaration arg0) {} public void visitTypeParameterDeclaration(TypeParameterDeclaration arg0) {} public void visitClassDeclaration(ClassDeclaration arg0) { //visit all methods boolean flag=false; for(MethodDeclaration method:arg0.getMethods()){ //un exemple pour inserer uen erreur //m.printError(arg0.getPosition(),"teteststst"); if(method.getAnnotation(NotNullParameter.class)!=null){ flag=true; method.accept(this); } } if(flag==false){ //si fichier non traité, on recopie le fichier dans le repertoire generated File old = arg0.getPosition().file(); File generated = new File("generated/"+old.getName()); try { BufferedWriter out = new BufferedWriter(new FileWriter(generated)); BufferedReader in = new BufferedReader(new FileReader(old)); String line = in.readLine(); while(line!=null){ out.write(line+"\n"); line=in.readLine(); } in.close(); out.close(); } catch (IOException e) { // TODO Ouais bon OK ca va ...c'est une démo techno ! e.printStackTrace(); } } } public void visitMethodDeclaration(MethodDeclaration arg0) { if(arg0.getAnnotation(NotNullParameter.class) != null){ File f = arg0.getPosition().file().getAbsoluteFile(); int position = arg0.getPosition().line(); //on lit le fichier source .java try { BufferedReader in = new BufferedReader(new FileReader(f)); Listdata = new ArrayList (); String line = in.readLine(); int i=1; while(line!=null){ //on insere des lignes de code dans le .java généré if(i==position+1){ for(ParameterDeclaration param:arg0.getParameters()){ if(param.getType() instanceof ClassTypeImpl){ data.add("if (" + param.getSimpleName() +"==null) throw new NullPointerException();"); }else{ m.printNotice(param.getPosition(),"Annotation non applicable à un type primitif !"); } } }else{ data.add(line); line = in.readLine(); } i++; } in.close(); //on écrit dans le fichier généré BufferedWriter out = new BufferedWriter(new FileWriter(new File("generated/"+f.getName()))); for(String l:data){ out.write(l+"\n"); } out.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } }} }
Remarque : Cette exemple est trivial afin d'illustrer l'utilisation d'APT. L'insertion de code est quelquechose de dangereux si on ne se fixe pas des rêgles de codage stricte afin d'éviter les effets de bords. En effet, le visiteur est limité ! Quand il visitera une méthode il n'ira pas plus loin que les paramêtres, et il n'est pas possible d'explorer le contenu de la méthode ! L'insertion de code y est donc très dangereuse même en faisant attention aux placements des accolades...La programmation par aspect grâce à APT s'en voit donc limiter.
Conclusion
Au moment de l'écriture de cet article, APT reste jeune et méconnu du monde des développeurs java. En autres, il n'est pas encore (ou très mal) intégré au IDE actuel tels que NetBeans ou Eclipse. De plus l'API est assez obsure sans exemple concret.
Cependant il sera inclu directement dans le processus de compilation du bytecode dans le futur Java Mustang (Java SE6).