Annotations en Java

Université de Toulon

LIS UMR CNRS 7020

2024-10-04

Source
Branch
  • develop (ba91d87)
  • 2024/10/04 11:54:15
Java
  • OpenJDK Temurin-21.0.4+7
  • Apache Maven 3.9.9
Docker
  • Client: 27.3.1 - buildx v0.17.1 - compose v2.29.7
  • Server: 27.3.1

Annotations en Java : Des métadonnées pour votre code

Les annotations en Java sont des métadonnées que l’on associe à différentes entités du code (classes, méthodes, attributs, etc.) afin de fournir des informations supplémentaires à propos de celui-ci.

  • À quoi servent les annotations ?

    • Documentation: Améliorez la lisibilité de votre code en fournissant des explications contextuelles.
    • Configuration: Paramétrez le comportement de vos applications sans modifier le code source.
    • Traitement du code source: Automatisez des tâches comme la génération de code, la création de tests ou la validation de contraintes.
    • Intégration avec des outils: Facilitez l’utilisation de frameworks et d’outils externes.
  • Syntaxe de base

    • Une annotation se définit avec le caractère @ suivi de son nom. Par exemple :
@SuppressWarnings("unused")
private int maVariable;

Annotations standards

Il existe plusieurs annotations standards couramment utilisées en Java :

Définition d’une annotation

Une annotation se déclare dans un fichier qui a le même nom comme une classe ou une interface.

public @interface MyAnnotation { }

Elle peut alors être utilisée partout sur une classe.

@MyAnnotation
public class MyClass {
    @MyAnnotation
    private int x;
    
    @MyAnnotation
    public void m(@MyAnnotation int y) {
        @MyAnnotation
        String m="test";
    }
}

Paramètres d’une annotation

Les paramètres d’une annotation, appelés éléments, déterminent les informations supplémentaires que l’on peut associer à un élément de code. Ces éléments peuvent être de différents types :

  • Types primitifs: (int, double, boolean, etc.)
  • Types référence: (String, objets)
  • Types de classe: (Class)
  • Annotations: (pour créer des annotations composées)
  • Enumérations: (pour des valeurs prédéfinies)
  • Tableaux: de tous les types précédents

Définition d’une annotation

public @interface MyAnnotation {
    String message(); // Message associé à l'annotation
    int size() default 10; // Taille maximale (valeur par défaut : 10)
    Digit digit() default Digit.UN; // Choix entre UN et DEUX (par défaut : UN)
    Class<?>[] classes() default {}; // Tableau de classes associées

    enum Digit {UN, DEUX} // Valeurs possibles pour le chiffre
}

et utilisation sur une classe ou une méthode

@MyAnnotation(message="hello", digit=MyAnnotation.Digit.UN, classes={String.class, Arrays.class})
public class MyClass {
    @MyAnnotation(message="bye")
    public void x() {}
}

Value

Si leur seul attribut obligatoire s’appelle value, il est possible d’omettre son nom lors de l’annotation.

public @interface MyAnnotation {
    String value();
    int size() default 10;
}
@MyAnnotation("hello")
public class MyClass { }

Portée d’une annotation

L’annotation @Target définit la portée d’une annotation en spécifiant les éléments de programme (classes, méthodes, champs, paramètres, etc.) auxquels elle peut être appliquée. Les valeurs possibles pour @Target sont : ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE, TYPE_PARAMETER.

Par exemple, en utilisant @Target(ElementType.METHOD), on indique que l’annotation ne peut être appliquée qu’à des méthodes.

@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD})
public @interface MyAnnotation {
    int value() default 0;
}
public class MyClass {    
    @MyAnnotation
    int x;
    public void m() {@MyAnnotation int x;}
}
@MyAnnotation
public class MyClass {    
    int x;
}
CompilationException: 
@MyAnnotation
annotation interface not applicable to this kind of declaration

Annotations répétables 1/2

Par défaut, une annotation ne peut être appliquée qu’une seule fois à un élément.

  • Solution : Utiliser un wrapper d’annotation pour regrouper plusieurs instances de la même annotation.
@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD})
public @interface MyAnnotations {
    MyAnnotation[] value();
}
public class MyClass {    
    @MyAnnotations({@MyAnnotation(1),@MyAnnotation(2)})
    int x;
}

Annotations répétables 2/2

ou les autoriser à être répétables grâce à l’annotation @Repeatable, évitant ainsi la déclaration explicite d’un tableau d’annotations.

@Repeatable(MyAnnotations.class)
@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD})
public @interface MyAnnotation {
    int value() default 0;
}
public class MyClass {    
    @MyAnnotation(1)
    @MyAnnotation(2)
    int x;
}

Annotations de package

Pour annoter un package, utilisez le fichier spécial package-info.java.

Exemple:

java filename="package-info.java" @SuppressWarnings("unused") package com.example;

Durée de vie et héritage des annotations

  • @Retention Détermine à quel moment du cycle de vie d’une application une annotation est accessible.
Valeur Description
SOURCE Utilisée uniquement par le compilateur.
CLASS Conservée dans le fichier .class.
RUNTIME Conservée à l’exécution.
  • @Inherited Indique si une annotation déclarée sur une classe est également présente sur ses sous-classes.

  • @Documented Indique qu’une annotation doit être incluse dans la documentation générée par le javadoc.

@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
  public enum Status {IN_PROGRESS, DONE} 
  Status status() default MyAnnotation.Status.DONE;
}
public class MyClass {    
    @MyAnnotation
    public void m1() {System.out.println("Hello1");}

    @MyAnnotation(status=MyAnnotation.Status.IN_PROGRESS)
    public void m2() {System.out.println("Hello2");}
    
    @MyAnnotation(status=MyAnnotation.Status.DONE)
    public void m3() {System.out.println("Hello3");}

    public void m4() {System.out.println("Hello4");}
}

Utilisation par instrospection (1/2)

Les annotations peuvent être utilisées à l’exécution grâce à l’introspection, à condition d’avoir la rétention RUNTIME.

  • Class Class: Représente une classe en mémoire.
  • Méthodes d’introspection: getMethod(), getField(), getConstructor(), etc. permettent d’accéder aux membres d’une classe.
  • Annotations: Les annotations présentes sur ces membres peuvent être récupérées.
  • Actions programmables: Une fois qu’un élément annoté a été identifié, on peut :
    • Invoquer une méthode: Exécuter le code contenu dans une méthode.
    • Accéder à une valeur: Récupérer la valeur d’un champ.
    • Créer une instance: Instancier un objet à partir d’un constructeur.

L’exemple ci-dessous affiche les méthodes de la classe précédente annotées avec @MyAnnotation.

// Récupère une instance de la classe MyClass
Class aClass = new MyClass().getClass();

// Récupère toutes les méthodes déclarées dans MyClass et les filtre pour ne garder que celles annotées avec @MyAnnotation
Arrays.stream(aClass.getDeclaredMethods())
    .filter(m->m.getAnnotation(MyAnnotation.class)!=null)
    .map(m->"La méthode %s a comme statut %s.".formatted(m.getName(),m.getAnnotation(MyAnnotation.class).status()))
    .forEach(System.out::println);
La méthode m2 a comme statut IN_PROGRESS.
La méthode m1 a comme statut DONE.
La méthode m3 a comme statut DONE.

Utilisation par instrospection (2/2)

Il est aussi possible d’invoquer dynamiquement ces méthodes.

// Invokes methods annotated with @MyAnnotation (status=DONE) on the provided object

MyClass myObject = new MyClass();

Arrays.stream(myObject.getClass().getDeclaredMethods())
        .filter(m -> m.getAnnotation(MyAnnotation.class) != null && 
                    m.getAnnotation(MyAnnotation.class).status().equals(MyAnnotation.Status.DONE))
        .forEach(m -> {
            try {
                m.invoke(myObject);
            } catch (Exception e) {
                System.err.println("Error invoking method " + m.getName() + ": " + e.getMessage());
            }
        });
Hello1
Hello3