Accès rapide :
Définition d'une nouvelle annotation
Syntaxe générale
Les différents niveaux de rétention
Ajout d'une cible à une annotation
Ajout d'attributs à une annotation
Manipulation de notre annotation
Travaux pratiques
Le sujet
La correction
Il est possible de coder ses propres d'annotations. On procède à ce type de définitions quand on cherche à développer son propre framework de test, de persistance ou autre.
Une annotation se définit via la construction @interface
. On doit donner un nom à l'annotation : les conventions de nommage à respecter sont les
mêmes que pour les classes ou les interfaces.
Voici un premier exemple de définition d'annotation : cette annotation pourrait être utilisée par un framework de test similaire à JUnit (ce que nous étudierons
dans le chapitre suivant).
1 2 3 4 5 6 |
package fr.koor.sample; public @interface TestMethod { } |
Il existe différents niveaux de « rétention » : la rétention d'une annotation correspond, dans une certaine mesure, à sa durée de vie.
On spécifie la rétention d'une annotation en lui ajoutant l'annotation @Retention
(oui, une annotation peut être appliquée à une autre annotation).
Voici un exemple de définition de la rétention d'une annotation.
1 2 3 4 5 6 7 8 9 |
package fr.koor.sample; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention( value = RetentionPolicy.RUNTIME ) public @interface TestMethod { } |
@Retention
ne possède qu'un seul et unique attribut. Si tel est le cas, vous pouvez omettre le nom
de cet unique attribut. En conséquence, le code suivant est strictement équivalent.
1 2 3 4 5 6 7 8 9 |
package fr.koor.sample; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention( RetentionPolicy.RUNTIME ) public @interface TestMethod { } |
Et voici la liste des différents niveaux de rétention supportés par Java.
RetentionPolicy.CLASS : l'annotation est stockée dans le fichier .class
lors de la compilation, mais elle ne sera par chargée par
la JVM en mémoire.
RetentionPolicy.RUNTIME : l'annotation est stockée dans le fichier .class
lors de la compilation et elle sera chargée en mémoire par
la JVM lors de l'utilisation de la classe. Il sera donc possible de retrouver ces annotations durant l'exécution de la JVM : cela se fera grâce au moteur
de réflexion du langage Java. Il s'agit clairement du type de rétention le plus utile.
RetentionPolicy.SOURCE : l'annotation est perdue lors de la compilation. Elle sert donc uniquement à la compréhension du code.
Il existe des annotations standards de ce type et par exemple, l'annotation @Override
qui indique que la définition de méthode associée est une
redéfinition (et donc, qu'il y a un lien d'héritage ou d'implémentation d'interface).
Il est possible de définir sur quel type d'éléments on peut appliquer une annotation (classes, méthodes, attributs, ...). Pour ce faire, il faut ajouter
l'annotation @Target
à votre annotation. L'annotation @Target
n'accepte qu'un seul et unique attribut nommé value
:
en fait, c'est lui qui permet de spécifier la cible.
1 2 3 4 5 6 7 8 9 10 11 12 |
package fr.koor.sample; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.METHOD ) public @interface TestMethod { } |
@TestMethod
à autre chose qu'une méthode,
une erreur de compilation sera produite.
Voici la liste des cibles d'annotations supportées par Java :
TYPE : indique que l'annotation peut s'employer sur une classe, une interface, une annotation ou un type énuméré.
1 2 3 |
@MyAnnotation public class MyClass { } |
FIELD : indique que l'annotation peut s'employer sur un attribut ou sur une constante d'un type énuméré.
1 2 3 4 |
public class MyClass { @MyAnnotation private int myField; } |
METHOD : indique que l'annotation peut s'employer sur une méthode (abstraite ou non, native ou non, statique ou non).
1 2 3 4 |
public class MyClass { @MyAnnotation public void myMethod() {}; } |
PARAMETER : indique que l'annotation peut s'employer sur la définition d'un paramètre de méthode.
1 2 3 |
public class MyClass { public void myMethod( @MyAnnotation int myParam ) {}; } |
CONSTRUCTOR : indique que l'annotation peut s'employer sur la déclaration d'un constructeur.
1 2 3 4 |
public class MyClass { @MyAnnotation public MyClass() {}; } |
LOCAL_VARIABLE : indique que l'annotation peut s'employer sur une déclaration de variable locale à une méthode.
1 2 3 4 5 |
public class MyClass { public void myMethod() { @MyAnnotation int myVar = 10; }; } |
ANNOTATION_TYPE : indique que l'annotation en cours de définition peut s'employer sur une autre annotation.
1 2 3 |
@MyAnnotation public @interface OtherAnnotation { } |
PACKAGE : indique que l'annotation peut s'employer sur une déclaration de paquetage.
1 2 |
@MyAnnotation package fr.koor.demo; |
TYPE_PARAMETER (Java SE 8.0) : indique que l'annotation peut s'employer sur une déclaration de type générique.
1 |
public class MyClass<@MyAnnotation T> {} |
TYPE_USE (Java SE 8.0) : indique que l'annotation peut s'employer sur n'importe quelle utilisation d'un type Java.
1 |
public class MyClass extends @MyAnnotation OtherClass { } |
MODULE (Java SE 9.0) : indique que l'annotation peut s'employer sur une définition de module.
1 2 3 |
@MyAnnotation module fr.koor.mymodule { } |
Il est aussi possible de définir plusieurs cibles pour une annotation. C'est par exemple le cas de l'annotation @Deprecated
qui peut être
apposée sur un grand nombre d'éléments.
1 2 3 4 5 6 7 8 |
import static java.lang.annotation.ElementType.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE}) public @interface Deprecated { // ... } |
Une annotation est une méta-données qu'on va appliquer à un élément de votre programme. Parfois, cette méta-donnée est complexe et il est nécessaire de
spécifier plusieurs de ses caractéristiques. Pour ce faire, une annotation supporte des attributs. Par exemple, l'annotation @Target
vue dans
la section précédente, possède un attribut value
nous permettant de lister l'ensemble des cibles auxquelles il est potentiellement possible
d'appliquer l'annotation.
Dans le cas de notre annotation @TestMethod
, on va partir du principe qu'il est possible de s'attendre à une éventuelle remontée d'exception.
Nous allons aussi permettre la définition d'un temps d'exécution maximal de notre méthode de test. Voici comment ajouter ces deux attributs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package fr.koor.sample; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.METHOD ) public @interface TestMethod { Class<? extends Throwable> expected(); long timeout(); } |
Il est aussi possible de définir des valeurs par défaut sur les attributs d'une annotation. Il ne sera ainsi pas obligatoire de spécifier une valeur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package fr.koor.sample; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.METHOD ) public @interface TestMethod { /** Permet de spécifier un éventuel type d'exception attendue */ Class<? extends Throwable> expected() default NoExceptionExpected.class; /** Un temps maximal d'exécution de notre scénario de test */ long timeout() default -1; /** Un type que nous associerons à une situation ou aucune exception n'est attendue. */ public static class NoExceptionExpected extends Throwable {} } |
Voici maintenant un exemple de code permettant de savoir si une méthode porte une annotation particulière. Cela se fait grâce au moteur de réflexion du langage. Une fois une annotation obtenue, il est possible de la manipuler comme un objet Java classique. Il faut néanmoins noter la syntaxe particulière permettant l'accès aux attributs de l'annotation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package fr.koor.sample; import java.lang.reflect.Method; import fr.koor.sample.TestMethod.NoExceptionExpected; public class Demo { @TestMethod public void firstMethod() { System.out.println( "Is a test method" ); } public void secondMethod() { System.out.println( "Is not a test method" ); } @TestMethod( timeout = 5 ) public void thirdMethod() { System.out.println( "Is a test method" ); } @TestMethod( expected = ArithmeticException.class, timeout = 1 ) public void fourthMethod() { System.out.println( "Is a test method" ); } public static void main( String [] args ) throws Exception { // On récupère les méta-données de la classe Demo Class<Demo> metadata = Demo.class; // On parcourt toutes les méthodes de la classe Demo, y compris le main. for ( Method method : metadata.getDeclaredMethods() ) { // On vérifie la présence de notre annotation TestMethod annotation = method.getAnnotation( TestMethod.class ); if ( annotation == null ) continue; // On affiche le nom de la méthode ainsi que les attributs de l'annotation. System.out.printf( "%s - %s - max duration = %d\n", method.getName(), annotation.expected() != NoExceptionExpected.class ? annotation.expected().getName() : "no exception expected", annotation.timeout() ); } } } |
Voici les résultats qui s'afficheront si vous exécutez ce programme.
firstMethod - no exception expected - max duration = -1 thirdMethod - no exception expected - max duration = 5 fourthMethod - java.lang.ArithmeticException - max duration = 1
Veuillez créer une nouvelle annotation, appelée TestClass
permettant de marquer les classes de tests. Cette annotation doit définir un seul et
unique attribut appelé category
(il s'agit d'une simple chaîne de caractères) : il permettra de classifier les classes de test.
Dans un second temps, reprendre le code de la classe Demo
vue ci-dessus, et ajouter l'annotation sur cette classe.
Ensuite, vérifier que l'instance metadata
détecte bien la présence de notre annotation.
TestMethod
pour bâtir un petit framework de tests unitaires.
A vous de jouer et essayez de ne pas regarder la correction trop vite ;-)
Voici le code de l'annotation TestClass
demandée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package fr.koor.sample; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention( RetentionPolicy.RUNTIME ) @Target( ElementType.TYPE ) public @interface TestClass { String category() default "default"; } |
Pour utiliser cette classe, vous pouvez compléter la classe de démonstration, vue précédemment, ainsi :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
package fr.koor.sample; import java.lang.reflect.Method; import fr.koor.sample.TestMethod.NoExceptionExpected; @TestClass( category = "demo" ) public class Demo { @TestMethod public void firstMethod() { System.out.println( "Is a test method" ); } public void secondMethod() { System.out.println( "Is not a test method" ); } @TestMethod( timeout = 5 ) public void thirdMethod() { System.out.println( "Is a test method" ); } @TestMethod( expected = ArithmeticException.class, timeout = 1 ) public void fourthMethod() { System.out.println( "Is a test method" ); } public static void main( String [] args ) throws Exception { // On récupère les méta-données de la classe Demo Class<Demo> metadata = Demo.class; // Vérification de la présence de notre annotation sur la classe. if ( metadata.getDeclaredAnnotation( TestClass.class ) == null ) { System.err.println( "@TestClass annotation is not present" ); System.exit( -1 ); } // On parcourt toutes les méthodes de la classe Demo, y compris le main. for ( Method method : metadata.getDeclaredMethods() ) { // On vérifie la présence de notre annotation TestMethod annotation = method.getAnnotation( TestMethod.class ); if ( annotation == null ) continue; // On affiche le nom de la méthode ainsi que les attributs de l'annotation. System.out.printf( "%s - %s - max duration = %d\n", method.getName(), annotation.expected() != NoExceptionExpected.class ? annotation.expected().getName() : "no exception expected", annotation.timeout() ); } } } |
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :