Les annotations

images/blackbelt.png

[Note]

Dans ce chapitre le terme "élément" désigne un élément constitutif du langage java: classe, méthode, constructeur, champ, paramètre,…

La technique des annotations relève de la systématisation de la description de meta-informations sur les éléments d’un programme Java.

Dans certaines situations on a besoin d’informations sur des propriétés générales d’un élément Java. Ainsi , par exemple, :

class Salarie implements Comparable<Salarie>, Serializable

ici l’interface Serializable est un marqueur qui permet au dispositif d’entrée/sortie d’objets de savoir qu’il peut linéariser (serialize) un objet de ce type. On a ici une propriété de la classe qui ne concerne pas ses fonctionnalités propres: il s’agit plutôt d’une meta-information exploitée au run-time.

De même

private transient Salarie supérieurHiérarchique ;

ici le mot-clef transient est utilisé pour annoter une propriété particulière de ce champs vis à vis des opérations de linéarisation.

interface CompteDistant extends Remote

L’interface Remote est un marqueur exploité par le générateur de stub.

Toutefois, si on y regarde d’un peu plus près, l’utilisation de l’interface Java comme support de meta-information est à la limite du détournement: une sous-classe d’un objet Serializable n’est pas nécessairement serializable!

L’exploitation de meta-informations est également passée par d’autres moyens détournés:

/**
@deprecated This method is inherently unsafe.

Ici c’est le commentaire javadoc qui est exploité par le compilateur!

Avant la version 1.5 il manquait à Java un mécanisme d’annotation qui fasse partie du langage, qui soit régulier et consistant. (Le mécanisme des Beans , bien que traitant aussi de meta-informations, est extérieur à la classe et a des usages spécialisés, il faudrait le pousser à d’extrèmes limites pour obtenir un mécanisme général de meta-information sur le code).

Structures de données

Une annotation doit véhiculer un ensemble de données structurées. Il y a donc un type correspondant à une sémantique et l'élément Java est annoté avec une instance de ce type.

[Note]

En théorie il s’agit de méta-informations qui n’affectent pas directement la sémantique du code exécutable quoiqu’elles puissent être exploitées par des outils comme des générateurs de codes (ou de données quelconques) qui pourront être exploités en parallèle avec le programme annoté.

Les limites de cette influence sur la sémantique restent en pratique floues: on pourrait imaginer une annotation pour décrire la propriété "Serializable".

Le type annotation est une simple structure de donnée. On a donc un ensemble de données avec un dispositif syntaxique qui permet de décrire les valeurs et des accesseurs qui permettent de consulter ces valeurs d’une manière cohérente avec la consultation standard des objets Java.

public class Personne {
   @Association (// annotation non standard
      type=COMPOSITION,
      relation="habite",
      arité={"1","1"}
   ) Adresse adresse ;
        °°°
}

et consultation depuis un code d’introspection:

   Field fieldAdr = //recherche d'un Field par introspection
   Association assoc = fieldAdr.getAnnotation(Association.class) ;
   EnumTypeAssoc tp = assoc.type() ;

Nous verrons ultérieurement que les types possibles pour les données membres sont en nombre restreint

Rétention des données

La structure de données attachée à un élement Java peut être insérée dans le code binaire de la classe pour être consultée au runtime. Cette insertion n’est toutefois pas nécessaire s’il s’agit par exemple de données exploitables seulement au moment de la compilation. La définition d’une annotation comprend donc différents statuts de conservation des données:

  • SOURCE : le compilateur ne met pas les données correspondantes dans le binaire. Données typiquement exploitées par le compilateur lui-même (ou par des outils dynamiquement associés: "pluggable annotation processor" implantant l’interface javax.annotation.processing.Processor). Des outils (complexes) d’analyse statique des sources peuvent également traiter ces annotations.
  • CLASS : (comportement par défaut) Le compilateur met les données dans le binaire mais celles-ci ne sont pas nécessairement conservées par la machine virtuelle à l’exécution. Données exploitées par des outils qui utilisent les fichiers ".class" pour générer des données ou du code (peu courant!).
  • RUNTIME : Les données dans le binaire sont conservées par la machine virtuelle pour consultation au runtime. Données exploitées dynamiquement par programme (java.lang.reflect)

La politique de rétention choisie sera décrite dans la définition du type de l’annotation.

Entités annotables

Une annotation est syntaxiquement placée comme un modificateur (comme public ou final) -pour harmoniser les conventions de codage on les placera avant tous les autres modificateurs-.

On peut annoter:

  • Les déclarations d'éléments de "premier niveau": classes, interfaces, types énumérés et définitions d’annotations elle-mêmes.
  • Les membres d’instance et membres statiques: champs (y compris les constantes membres d’un énuméré), méthodes, classes membres, etc.
  • Les constructeurs.
  • Les paramètres des constructeurs, des méthodes et des lambda-expressions
  • Des utilisations de littéraux types (ex. instanceof @NotNull String )
  • Les paramètres type.
  • Les variables locales: toutefois on ne peut avoir ici que des annotations de portée SOURCE.
  • Les packages. Le principe recommandé est d’avoir dans le répertoire du package un fichier package-info.java qui ne contient qu’une déclaration de package annotée. Il est de plus souhaité que la documentation générale du package exploitéé par javadoc soit transférée dans ce fichier (qui remplace donc l’ancien fichier package.html).
  • Les modules: dans la déclaration du module dans module-info.java.

Quelques annotations standard

  • java.lang.Deprecated : s’applique à tout élément de programme pour indiquer au compilateur qu’il est déconseillé d’utiliser cet élément dans un code. C’est une annotation de portée SOURCE qui admet éventuellement des paramètres: since=ChaineNumeroVersion et forRemoval (booléen par défaut false) qui indique que l'élément est susceptible de ne plus être utilisé dans des versions ultérieures.

    /** utiliser plutôt java.util.logging */
    @Deprecated(since="3.4", forRemoval=true) public void tracer() {
    °°°°
  • java.lang.Override : annotation marqueur de portée SOURCE réservée à l’annotation de méthodes. Si la méthode d’instance ainsi annotée ne spécialise pas une méthode de la super-classe le compilateur signale une erreur (depuis 1.6 cette annotation est aussi utilisée pour marquer l’implantation d’une méthode définie par une interface):

    public class Personne {
    °°°
    @Override public boolean equals(Personne lautre){
    // ERREUR COMPILATION : public boolean equals(Object o)
  • java.lang.SuppressWarnings : de portée SOURCE et s’applique à tout élément annotable. Indique au compilateur de ne pas prévenir en cas d’inconsistance détectée dans le code. Les arguments (il peut y en avoir plusieurs) indiquent par exemple: "deprecation" (ne pas signaler l’usage d’un élément obsolete parceque nous l’utilisons à dessein), "unchecked" (inconsistance volontaire d’utilisation d’un type paramétré). La liste des arguments dépend du compilateur utilisé (pour le compilateur SUN passer la commande javac -Xlint).

    Possibles<?> cache ;
       °°°°
    // dans une méthode avec un paramètre-type T
          @SuppressWarnings("unchecked")
       Possibles<T> resultat = (Possibles<T>)cache ;
  • java.lang.annotation.Inherited meta-annotation: annotation marqueur de portée RUNTIME réservée à l’annotation d’une autre Annotation (appelons la Xann). Lorsqu’un programme recherchera dynamiquement Xann comme annotation d’une classe il inspectera automatiquement la super-classe s’il ne la trouve pas -et ainsi de suite- (cet effet n’est obtenu que si Xann annote une classe).

Définition des annotations

Soit à définir une annotation de nom Association :

package meta ;
import java.lang.annotation.* ;

@Documented
@Target( {ElementType.FIELD} )
@Retention(RetentionPolicy.RUNTIME)
public @interface Association {
   EnumTypeAssoc type() default EnumTypeAssoc.ASSOCIATION ;
   String relation() ;
   String[] arité() ;
   enum EnumTypeAssoc {COMPOSITION, AGGREGATION, ASSOCIATION};
}

L’annotation est définie au moyen de la déclaration @interface (ce nouveau type hérite automatiquement de l’interface java.lang.annotation.Annotation -pas d’autre héritage possible-)

Elle contient un ensemble de déclarations des accesseurs de la structure de données (pseudo-méthodes sans argument). Elle peut contenir aussi des déclarations de constantes ou de type "interne": classe, interface, type énuméré, annotation.

Elle est elle-même annotée.

Utilisation de la structure de données

  • Les types sont limités : types scalaires primitifs, String, Class, types énumérés, types annotation et tableaux de types précédents. Lorsque l’on utilise le type la définition de la structure de données ne comporte que des constantes de compile-time (pas de new):

    @Association (
       type=COMPOSITION, // import static ....
       relation="habite",
       arité={"1","1"} //syntaxe particulière des tableaux
    )
  • Il est possible de définir une valeur par défaut (ce qui dispense éventuellement de déclarer une valeur pour ce champ dans la structure de données littérale);
[Note]Détail complexe

L’utilisation d’une constante de type Class avec une annotation de retention SOURCE peut poser des problèmes: la classe n’est peut être pas accessible à ce moment précis du traitement au compile-time.

Voir dans le module java.compiler la documentation de javax/lang/model/element/Element.html#getAnnotation(java.lang.Class) et la notion de TypeMirror.

Les méta-annotations

  • java.lang.annotation.Documented: indique à javadoc -ou à des outils similaires- d’insérer la documentation de cette annotation dans la documentation de l’API.
  • java.lang.annotation.Target: indique au compilateur le sous-ensemble des éléments annotables (MODULE, PACKAGE, TYPE, ANNOTATION_TYPE, FIELD, METHOD, CONSTRUCTOR, PARAMETER, TYPE_PARAMETER, TYPE_USE, LOCAL_VARIABLE).

    exemple de TYPE_USE: instanceof @NotNull Thing

    PARAMETER peut aussi s’appliquer aux paramètres anonymes d’une lambda-expression.

  • java.lang.annotation.Retention: indique au compilateur (ou au chargeur de classe) la politique de conservation des données de l’annotation (SOURCE, CLASS, RUNTIME).

On notera que dans l’exemple ces deux méta-annotations sont utilisées avec une syntaxe simplifiée (ne comportant pas de nom de champ).

Déclarations simplifiées

Il existe quelques facilités syntaxiques pour simplifier l'écriture des annotations.

Dans l’exemple précédent:

@Documented

est une simplification de l'écriture :

@Documented()

(Cette simplification est également admise si tous les champs de l’annotation prennent leur valeur par défaut).

@Retention(RUNTIME)

est une simplification de l'écriture :

@Retention(value=RUNTIME)

Le champ "value" est, par convention, celui qui peut être omis dans une annotation avec une seule valeur (il peut être de n’importe quel type légal : ici RetentionPolicy)

@Target(FIELD)

est une simplification de :

@Target(value={FIELD})

(ici value est de type ElementType[] )

Consultation par introspection

Un type Annotation est une interface et sa classe Class répond au test:

public boolean isAnnotation()

Les éléments annotés implantent l’interface java.lang.reflect.AnnotatedElement qui décrit les services d’accès aux annotations comme :

boolean isAnnotationPresent(
Class<? extends Annotation> annotationType);
<T extends Annotation> T getAnnotation(Class<T> annotationType);
Annotation[] getAnnotations();
°°°

Un cas particulier est celui de la consultation des annotations sur les paramètres des méthodes et constructeurs. Les classes Method et Constructor disposent de la méthode :

public Parameter[] getParameters()

Chaque objet Parameter pouvant décrire des annotations.