Les annotations de Java 5
Par
Romain Guy
La dernière tendance à la mode en programmation consiste à utiliser des méta données.
Vous pouvez d'ores et déjà les utiliser avec C# et la prochaine version 2.4 de Python nous les promet.
La dernière version de Java, le J2SE 5 SDK 1.5, propose également les méta données, appelées annotations.
Introduction
Les annotations standards et personnalisées
Exploiter vos annotations
Introduction
La définition la plus simple des méta données, et donc des annotations, pourrait se formuler de la sorte :
il s'agit de données servant à décrire d'autres données.
Malgré leur apparition tardive au sein du langage, ces méta données ont en réalité
toujours fait partie intégrante de la plateforme Java.
Le dernier exemple est celui d'XDoclet, largement employé pour la génération de code.
Un autre outil, bien connu de tous les développeurs Java, sert aussi à manipuler des méta données, javadoc.
Habituellement employé pour la rédaction et la génération de documentation,
processus qui s'apparente déjà aux méta données, cet outil dispose d'une balise particulière
répondant directement à notre définition. Cette dernière, intitulée @deprecated,
indique qu'une méthode est obsolète. En l'utilisant, vous pourrez rencontrer des
avertissements lors de la compilation.
Les annotations sont issues de la volonté de standardiser et d'épurer ce système.
Nous avons en effet ici un mélange peu élégant entre les commentaires et le code lui-même.
Dans l'idéal, @deprecated devrait apparaître directement dans le source et non
dans les commentaires.
La solution proposée par Sun est, comme nous allons le voir,
très flexible et adaptée à tous les usages.
L'utilisation de la méta donnée @deprecated de javadoc présente l'un des deux aspects
les plus importants des annotations, à savoir la possibilité d'effectuer des vérifications
à la compilation et celle d'analyser le code.
Le premier aspect peut par exemple servir à vérifier qu'une méthode marquée obsolète
ne soit pas invoquée dans d'autres parties du code.
L'aspect analyse de code est principalement utilisée pour la génération de code
en fonction des méta données. Nous pouvons par exemple imaginer une annotation
indiquant qu'un paramètre d'une méthode ne doit pas être de valeur null.
Des outils de gestion des annotations pourront ensuite générer un code source
contenant des lignes de code supplémentaires garantissant l'assertion.
Une des grandes forces des annotations réside dans la possibilité de pouvoir les
appliquer à toutes les déclarations de votre code : classes, interfaces, méthodes,
membres, constructeurs ... Il suffit pour cela de faire précéder la déclaration de l'annotation.
Dans Java 5 un vocabulaire particulier est utilisé pour désigner les méta données.
Les types d'annotations désignent l'équivalent des classes tandis que les annotations
sont les instances des types d'annotation. Il existe en outre trois catégories de types d'annotations.
La première concernent les marqueurs, ou markers, qui sont des méta données identifiées par un simple nom,
comme @Deprecated ou @Override.
Un type d'annotation commence toujours par une majuscule et une annotation est créée
en précédant le nom du type par @.
La deuxième catégorie comprend les annotations paramétrées.
Celles-ci disposent donc d'une donnée supplémentaire que le programmeur peut choisir.
Si nous disposions d'une annotation servant à définir le niveau de sécurité d'une méthode
nous pourrions par exemple écrire @SecurityLevel("maximum").
Enfin, la dernière catégorie concerne les annotations multi paramétrées,
ou annotations complètes.
Nous pouvons dans ce cas fournir plusieurs paramètres en prenant garde de les nommer.
Nous pourrions par exemple écrire @NotNull(argument="file", message="Le fichier ne peut être null.")
afin d'afficher le message spécifié si l'argument "file" de la méthode prend la valeur null à l'exécution.
Lorsque vous fournissez des paramètres à une annotation, vous pouvez utiliser des tableaux de tailles fixe
à l'aide des accolades. Voici l'exemple d'une annotation Todo très simple associant plusieurs tâches
à l'élément sur lequel elle s'applique :
| exemple @ToDo |
@Todo({ "Vérifier le type de retour.", "Optimiser la boucle."})
public String compute() { ... }
|
Cet exemple fait appel à une annotation personnalisée, créée de toutes pièces.
Avant de nous intéresser à leur réalisation, examinons celles proposées par le SDK.
Les annotations standards et personnalisées
Java 5 propose deux annotations standard.
La première, @Deprecated, est semblable à celle proposée par javadoc.
Il s'agit d'un marqueur que vous pouvez appliquer sur une classe, une interface,
un membre ou une méthode pour le rendre obsolète comme dans l'exemple du listing 1.
| listing 1 |
@Deprecated
public class OldBehavior { }
OldBehavior ob = new OldBehavior();
|
En compilant un tel source, vous obtiendrez un message peu précis du compilateur
indiquant qu'une API obsolète est utilisée.
Pour obtenir plus de détails, ajoutez l'option -Xlint:deprecation sur la ligne
de commande d'invocation de javac.
La seconde annotation standard est également un marqueur et permet d'éviter de
rencontrer des bugs sournois.
En ajoutant @Override devant une déclaration de méthode vous indiquez que celle-ci
est censée surcharger une méthode parente.
Si aucune super méthode correspondante ne peut être trouvée,
une erreur de compilation est lancée.
Ceci peut être très utile pour démasquer des bugs comme celui du listing 2.
Aviez-vous remarqué le caractère manquant ?
| listing 2 |
@Override
public String toSting() {
return "foobar";
}
|
Bien que très utiles ces deux annotations restent très limitées et n'offre que
peu de nouvelles perspectives par rapport à ce que nous pouvions faire jusqu'alors
avec javadoc ou XDoclet. La véritable nouveauté réside dans la possibilité
de pouvoir créer ses propres méta données.
Concrètement, une annotation se rédige comme une interface normale,
à savoir en créant un fichier .java portant le même nom qu'elle.
| listing 3 |
package com.loginmag.tiger;
public @interface Debug { }
import com.loginmag.tiger.*;
@Debug
public void hiddenMethod() { ... }
|
Le listing 3 présente un exemple de type d'annotation Debug,
appartenant au paquet com.loginmag.tiger, et son utilisation.
La déclaration d'une méta donnée est régie par l'utilisation du caractère @ devant
le mot-clé interface. En conservant notre travail tel quel,
nous ne disposons que d'un marqueur.
La déclaration d'annotations paramétrées ou multi paramétrées implique
l'ajout de membres, comme dans le listing 4.
| listing 4 |
public @interface SecurityLevel {
String value();
}
@SecurityLevel("maximum")
public void startNuclearReactor() { ... }
|
En déclarant un membre, suivi de parenthèses, nous ajoutons un paramètre nommé à l'annotation.
Quand ce dernier est unique et intitulé value, comme ici, nous pouvons
l'omettre lors de l'utilisation de l'annotation.
Pour accepter des tableaux comme paramètres, à l'instar de notre exemple Todo précédent,
il nous suffit de déclarer le membre comme un tableau en écrivant String[] value.
Les annotations multi paramétrées peuvent également bénéficier de paramètres optionnels,
c'est-à-dire disposant d'une valeur par défaut, ainsi qu'en atteste le listing 5.
| listing 5 |
public @interface Log {
public enum Level { MESSAGE, WARNING, ERROR };
String message();
String level() default Level.WARNING;
}
|
Si vous souhaitez attribuer une valeur par défaut à un tableau, sachez que
vous ne devez ni ne pouvez utiliser la syntaxe Java classique new Type[] { donnée }
mais celle des annotations qui ne nécessite que les accolades.
Nous écririons par exemple String[] messages() default { "Debug message" }.
Outre les deux annotations standard évoquées précédemment,
Java 5 propose quatre annotations particulières destinées aux types d'annotations.
Elles appartiennent au paquet java.lang.annotation et vous permettent de modifier
le comportement de vos annotations personnalisées. La première d'entre elles
s'appelle @Target et définit les éléments sur lesquels peut s'appliquer votre
type d'annotation. Vous devez pour cela fournir un tableau de valeurs puisées
dans l'énumération ElementType. Ainsi pour définir une annotation applicable
uniquement sur les déclarations de méthodes et les constructeurs vous devrez
écrire @Target({ ElementType.METHOD, ElementType.CONSTRUCTOR }).
Les cibles disponibles sont décrites dans la documentation d'ElementType.
Toute tentative d'utilisation de votre annotation ne respectant par les cibles
se soldera par une erreur de compilation.
La deuxième annotation, @Retention, concerne la rétention des informations
relatives aux annotations. Vous pouvez choisir un comportement parmi ces
trois : l'annotation est conservée dans le bytecode et durant l'exécution
(RetentionPolicy.RUNTIME), l'annotation est conservée dans le bytecode
(RetentionPolicy.CLASS) ou l'annotation n'est conservée que dans le code
source (RetentionPolicy.SOURCE). Ces options influencent directement sur les
outils de manipulation des méta données. Ainsi @Retention(RetentionPolicy.COMPILE)
ne vous permettra pas d'utiliser java.lang.reflect pour trouver l'annotation.
La troisième annotation est @Documented. En l'utilisant, vous indiquez à javadoc
que vous souhaitez voir apparaître l'annotation dans la documentation générée.
En effet, javadoc n'indique pas par défaut quelles annotations sont employées
dans votre code source. Sachez également que l'utilisation de @Documented
impose @Retention(RetentionPolicy.RUNTIME). Enfin, la dernière annotation,
@Inherited, permet d'appliquer automatiquement votre type d'annotation aux
descendants de la classe sur laquelle elle s'applique.
Par exemple, si votre classe Vehicule dispose de l'annotation @Todo,
elle-même @Inherited, alors votre classe Voiture héritant de Véhicule
héritera de @Todo.
Exploiter vos annotations
Une annotation personnalisée, aussi bien pensée soit-elle, ne vous sera d'aucune
utilité si vous ne créez pas les outils capables de l'interpréter.
Nous pourrions faire appel au procéder de reflection, à la manipulation de
bytecode ou encore à un générateur de parser comme JavaCC mais aucune de ces
solutions ne serait véritablement satisfaisante. Les auteurs du SDK 1.5 ont
heureusement pensé à tout en ajoutant apt, pour annotation processing tool,
un dérivé un peu particulier de javac. En pratique, apt vous permet de compiler
vos programmes Java tout en interprétant vos différentes annotations à l'aide
de processeurs spécialisés. Ces processeur sont créés par des fabriques, qu'apt
reconnaît au démarrage, et peuvent générer de nouveaux fichiers sources.
Lorsqu'un processeur génère un nouveau fichier, apt recommence son travail avec
celui-ci jusqu'à ce qu'aucun nouveau fichier n'apparaisse. Nous allons explorer
apt à travers l'exemple de l'annotation @Todo, décrite dans le listing 6.
| listing 6 |
@Retention(RetentionPolicy.SOURCE)
public @interface Todo {
String[] value() default { "Give this @Todo a value." };
|
Pour ce faire, nous allons devoir utiliser quatre sous-paquets de com.sun.mirror :
apt, qui contient les interfaces avec l'outil du même nom,
declaration, qui contient les classes décrivant des déclarations d'entités dans le code source,
type, qui modélise ces entités, et
util qui propose quelques outils.
Nous allons commencer par créer une fabrique de type AnnotationProcessorFactory
qui contient trois opérations, une pour créer le processeur,
et deux pour définir les types d'annotations supportées ainsi que les options
attendues sur la ligne de commande. Le code de la fabrique est très simple et
se trouve dans le fichier TodoAnnotationProcessorFactory.java dans le zip accompagnant cet article (cf. lien en fin d'article). Nous devons ensuite implanter l'interface
AnnotationProcessor pour le processeur lui-même.
La méthode process() est appelée pour l'interprétation des annotations déclarées
comme compréhensible par le processeur.
| listing 7 |
public void process() {
try {
Filer f = this.env.getFiler();
PrintWriter out = f.createTextFile(Filer.Location.SOURCE_TREE, "", new File("todo"), null);
for (TypeDeclaration typeDecl : env.getSpecifiedTypeDeclarations())
typeDecl.accept(DeclarationVisitors.getDeclarationScanner(new TodoVisitor(out), DeclarationVisitors.NO_OP));
out.close();
} catch (Exception e) { }
}
|
Le listing 7 présente notre code qui ouvre un fichier texte appelé "todo" dans
l'arbre des sources et qui le passe en paramètre à des visiteurs.
Notez que nous utilisons une méthode du contexte du processeur pour créer le fichier,
ce qui permet à apt de savoir qu'un document a été généré.
Les visiteurs sont des classes qui permettent de parcourir les déclarations sur
lesquelles elles sont appliquées. Dans notre méthode process() nous appliquons notre
visiteur TodoVisitor sur chaque type. Le visiteur est lui-même encapsulé dans un autre
visiteur fourni par la classe d'outils DeclarationVisitors. La méthode
getDeclarationScanner() nous fournit un visiteur capable de voir les déclarations
contenues dans une déclaration, comme les méthodes dans une classe.
Le premier paramètre définit le visiteur à appeler avant de visiter les déclarations
internes tandis que le second définit le visiteur appelé après visite.
Puisque nous n'avons besoin que d'un seul visiteur, nous utilisons le NO_OP
fourni en standard, qui ne fait rien. Nous pouvons également invoquer
getSourceOrderDeclarationScanner() qui effectue le même travail mais en essayant
de suivre l'ordre du code source au plus près. Notre TodoVisitor ne surcharge
que la seule méthode visitMethodDeclaration() bien que de nombreuses autres
soient disponibles. Chacune de ces méthodes agit comme un gestionnaire d'événement
lorsqu'une déclaration est rencontrée.
| listing 8 |
public void visitMethodDeclaration(MethodDeclaration d) {
Todo n = (Todo) d.getAnnotation(Todo.class);
out.printf("TODO in %s() at line %d:", d.getSimpleName(), d.getPosition().line());
out.println();
for (String value: n.value())
out.println(" " + value);
out.println();
}
|
Le listing 8 présente l'implantation de notre méthode qui écrit dans le fichier
"todo" toutes les valeurs associées à l'annotation @Todo de la méthode.
Nous pouvons maintenant procéder à la compilation du processeur de la classe
d'exemple, présentée dans le listing 9 :
| listing 9 |
public class Computer {
@Todo({"Check annotations.",
"Change the return value."
})
public String compute() {
return "result";
}
@Todo({"Return the value of compute()."
})
public String toString() {
return super.toString();
}
}
|
$ javac -classpath $JAVA_HOME/lib/tools.jar:. TodoAnnotationProcessorFactory.java
$ apt -factory TodoAnnotationProcessorFactory Computer.java
|
Après exécution de la deuxième commande, vous obtiendrez les fichiers Computer.class
et todo, ce dernier contenant la liste des tâches déclarées dans le code source.
Cet exemple n'a fait qu'effleurer les nombreuses fonctionnalités offertes par
l'outil apt et son API Mirror, aussi pourrez-vous consulter leur
documentation pour aller plus loin.
Romain Guy


Ces textes sont disponibles sous licence
Creative Commons Attribution-ShareAlike.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.