Introduction au développement d’un processeur d’annotations en Java

Université de Toulon

LIS UMR CNRS 7020

2024-10-09

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

Sample project

https://github.com/ebpro/sample-annotationprocessor

Qu’est-ce qu’un processeur d’annotations ?

  • Définition simple: Un processeur d’annotations est un outil qui analyse le code source à la recherche d’annotations spécifiques.

  • Fonctionnement: Il intercepte le processus de compilation pour exécuter du code personnalisé en fonction des annotations trouvées.

  • Analogie: C’est comme un “traducteur” qui transforme les annotations en instructions compréhensibles par la machine.

  • Exemple: L’annotation @Override indique que une méthode redéfinie une méthode héritée. Le compilateur utilise cette information pour vérifier la cohérence. Qu’est-ce qu’un processeur d’annotations ?

  • Définition simple: Un processeur d’annotations est un outil qui analyse le code source à la recherche d’annotations spécifiques.

  • Fonctionnement: Il intercepte le processus de compilation pour exécuter du code personnalisé en fonction des annotations trouvées.

  • Analogie: C’est comme un “traducteur” qui transforme les annotations en instructions compréhensibles par la machine.

  • Exemple: L’annotation @Override indique que une méthode redéfinie une méthode héritée. Le compilateur utilise cette information pour vérifier la cohérence.

Pourquoi utiliser les processeurs d’annotations ?

  • Génération de code: Automatisation de tâches répétitives (getters, setters, builders, etc.).
  • Validation de données: Vérification de contraintes à la compilation (non-nullité, plages de valeurs, etc.).
  • Configuration: Paramétrage de composants à partir d’annotations.
  • Documentation: Génération automatique de documentation à partir d’annotations.
  • Intégration avec d’autres outils: Liaison avec des outils de build, de test, etc.

Le cycle de vie d’un processeur d’annotations

  1. Détection des annotations: Le compilateur identifie les éléments annotés.
  2. Invocation du processeur: Le processeur est appelé pour chaque élément annoté.
  3. Analyse des annotations: Le processeur extrait les informations contenues dans les annotations.
  4. Génération de code (optionnel): Le processeur peut générer de nouvelles classes, méthodes ou fichiers.
  5. Intégration dans le résultat de la compilation: Le code généré est intégré au résultat final.

Le framework APT (Annotation Processing Tool)

  • Rôle: Fournit l’infrastructure de base pour développer des processeurs d’annotations.
  • Interfaces clés:
    • Processor: Point d’entrée du processeur.
    • RoundEnvironment: Contient les éléments annotés lors d’un round de traitement.
    • AnnotationMirror: Représente une annotation.
  • Cycle de vie:
    • Initialisation
    • Traitement des annotations
    • Génération de code
    • Nettoyage

Création d’une annotation personnalisée

  • Syntaxe:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Log {
  String value() default "";
}
  • Éléments:
    • @Target: Indique où l’annotation peut être utilisée (méthodes, classes, etc.).
    • @Retention: Détermine la durée de vie de l’annotation.
    • value: Attribut de l’annotation.

Implémentation d’un processeur simple

public class LogProcessor implements Processor {
    // ...
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Log.class)) {
            // Traitement de l'élément annoté (cf. ci-dessous)
        }
        return true;
    }
}

Génération de code de base

// Création d'un fichier source Java
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("GeneratedLogger");
try (Writer writer = sourceFile.openWriter()) {
    writer.write("public class GeneratedLogger {\n");
    // ...
    writer.write("}");
}

Utilisation des éléments de langage Java

  • Types génériques:
    • Créer des processeurs qui fonctionnent avec des types génériques pour une plus grande flexibilité.
    • Exemple : Un processeur qui génère des méthodes equals() et hashCode() pour n’importe quelle classe.
  • Annotations:
    • Utiliser des méta-annotations pour créer des hiérarchies d’annotations.
    • Combiner plusieurs annotations pour exprimer des contraintes complexes.
    • Exemple : Créer une annotation @Validated qui combine des annotations de validation comme @NotNull, @Size, etc.
  • Lambdas:
    • Utiliser des lambdas pour écrire du code plus concis et fonctionnel.

Génération de code complexe

  • Création de structures de données:
    • Générer des classes, des interfaces, des énumérations, etc.
    • Créer des hiérarchies de classes complexes.
    • Exemple : Générer un DAO (Data Access Object)
  • Arbres abstraits syntaxiques (AST):
    • Manipuler le code source à un niveau plus bas.
    • Transformer le code en fonction de règles spécifiques.
  • Templates de code:
    • Utiliser des templates pour générer du code.

Cas d’utilisation concrets

  • Validation de données: Vérifier la cohérence des données avant leur utilisation.
  • Génération de code pour des frameworks: ORM, REST, etc.
  • Configuration de projets: Charger des propriétés à partir d’annotations.
  • Documentation: Générer des rapports, des diagrammes, etc.

Bonnes pratiques et pièges à éviter

  • Modularité: Séparer les différentes responsabilités du processeur.
  • Testabilité: Écrire des tests unitaires pour vérifier le comportement du processeur.
  • Documentation: Documenter clairement le processeur et ses fonctionnalités.
  • Éviter les régressions infinies: Assurez-vous que le processeur ne génère pas de code qui déclenche à nouveau le traitement.