Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

Les annotations de Java 5

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.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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
Sélectionnez

@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
Sélectionnez

@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
Sélectionnez

@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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

@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
Sélectionnez

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
Sélectionnez

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
Sélectionnez

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();
  }
}
 
Sélectionnez

$ 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.

Téléchargez les sources des exemples

Romain Guy

Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
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.