> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Lombok Internal
- URL: https://www.sfeir.dev/back/lombok-internal/
- Published: 2026-06-23T13:52:56.000Z
- Updated: 2026-06-23T13:52:56.000Z
- Description: Lombok n'est pas un processeur d'annotations standard. Il détourne l'AST interne du compilateur avant qu'un seul octet de bytecode ne soit émis et ça change tout. J'ai recréé @Getter et @Logger from scratch pour comprendre exactement comment ça fonctionne.
- Author: Fabrice Yopa
- Tags: Java, annotation, AST, Lombok, Back

## Introduction

Si vous n'avez jamais utilisé `@Data`, `@Getter` ou `@Slf4j`, vous avez probablement passé des années à écrire des getters et des setters à la main. La plupart des projets Java modernes utilisent Lombok, mais la plupart des développeurs ne cherchent jamais à savoir comment ça marche vraiment.

Dans cet article, je plonge dans les mécanismes internes de Lombok : comment il génère du code à la compilation, en me concentrant sur `@Getter` et `@Logger`. Pour comprendre vraiment comment ça marche, j'ai remonté le dépôt jusqu'aux premiers commits, construit un clone minimaliste et noté tout ce qui a cassé en route.

## Comment Lombok fonctionne en interne

Le dépôt Lombok aujourd'hui, c'est un labyrinthe : gestionnaires d'annotations, transformateurs d'AST, chemins séparés pour Eclipse et javac, quinze ans de patches empilés. La branche `master` n'est pas un bon endroit pour commencer.

J'ai donc préféré remonter aux tout premiers commits. À cette époque, la structure était bien plus simple : une poignée de processeurs d'annotations, une séparation encore naissante entre Eclipse et javac, et les premières expérimentations sur l'AST, bien avant que la liste des annotations supportées n'explose.

Pour retrouver ces commits sans passer une heure à faire `git log --reverse`, j'ai développé [**GitGenesis**](https://gitgenesis.vercel.app/?ref=sfeir.dev), un petit outil qui isole les premiers commits de n'importe quel dépôt GitHub. Pour Lombok, [les résultats](https://gitgenesis.vercel.app/?q=projectlombok/lombok&ref=sfeir.dev) pointent directement vers le commit initial de Reinier Zwitserloot, à l'époque où `@Getter` passait du concept au code fonctionnel.

L'analyse de ces premiers diffs m'a beaucoup appris : chaque annotation Lombok se résume, au fond, à un processeur d'annotations qui réécrit l'AST (Abstract Syntax Tree) avant même que `javac` ait fini son travail.

### Le pipeline de compilation

Lombok s'accroche au processus de compilation de `javac` via l'API de traitement des annotations :

```text
Code Source (.java) → Compilateur → Processeur d'Annotations → Code Généré → Bytecode (.class)

```

Avant d'aller plus loin, voici les termes techniques qui reviennent constamment quand on lit le code source :

- **Processeurs d'annotations (Annotation processors) :**  des classes implémentant `javax.annotation.processing.Processor`.
- **Tours de traitement (Processing rounds)** : le compilateur exécute les processeurs en boucle jusqu'à ce qu'aucune nouvelle annotation n'apparaisse.
- **Manipulation de l'AST** : au lieu d'émettre de nouveaux fichiers `.java`, Lombok greffe directement des nœuds supplémentaires (méthodes, champs) dans le `JCTree`.
- **Bytecode :** une fois l'AST modifié, javac le compile comme n'importe quelle autre source.

Ce point est central : Lombok ne génère aucun fichier `PersonGetters.java` sur le disque. Il insère les méthodes directement dans l'AST de la classe. Elles atterrissent dans `Person.class` avec des numéros de ligne corrects et une vérification complète des types.

## Construire un mini-Lombok

Lire le code source, c'est bien. Voir ce qui casse quand on essaie de reproduire le même mécanisme, c'est beaucoup plus instructif.

J'ai donc décidé de recréer deux annotations :

- `@Getter` : génère un getter public pour un champ annoté.
- `@Logger` : injecte la déclaration `private static final Logger log` sur la classe.
- `@Benchmark:` petit bonus pour ceux qui vont regarder le code source.

Pour structurer cela, j'ai opté pour un projet Maven à deux modules :

- `mini-lombok-processor` : la logique des annotations et des processeurs.
- `mini-lombok-demo` : une classe `Person` qui les utilise, sans aucun getter écrit à la main.

```text
lombok-internal/
├── mini-lombok-processor/
│   └── src/main/java/me/fayolabs/minilombok/
│       ├── Getter.java
│       ├── GetterProcessor.java
│       ├── Logger.java
│       └── LoggerProcessor.java
└── mini-lombok-demo/
    └── src/main/java/me/fayolabs/demo/
        ├── Person.java   (@Logger + @Getter, sans getters manuels)
        └── Main.java

```

Le code complet est disponible sur mon GitHub : [lombok-internal](https://github.com/fabriceyopa/lombok-internal?ref=sfeir.dev).

Sur le papier, l'approche semble simple. C'est pourtant là que les murs ont commencé à apparaître.

## Les obstacles rencontrés

### Obstacle #1 : L'API standard ne permet pas de modifier des classes existantes

Ma première idée était simple : étendre `AbstractProcessor`, repérer les champs annotés, puis générer les getters. L'API standard semblait faire l'affaire :

```java
@SupportedAnnotationTypes("me.fayolabs.minilombok.Getter")
public class GetterProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) {
            // C'est bien beau de générer le getter... mais où le placer ?
        }
        return true;
    }
}

```

Le problème : `AbstractProcessor` ne permet **que la création de nouveaux fichiers**. L'interface `Filer` propose `createSourceFile()` et `createClassFile()`, mais il n'existe pas de `modifySourceFile()`. Les sources existantes sont en lecture seule.

Un processeur naïf émettrait donc un fichier annexe `PersonGetters.java` ou `Person_Generated.java`. Mais Lombok, lui, injecte ses getters directement dans `Person.class`. Pour faire de même, il faut aller plus bas.

### La solution : Manipuler l'AST directement

Les bibliothèques comme `ByteBuddy`, `ASM` ou `Javassist` interviennent sur les fichiers `.class` une fois la compilation terminée. Lombok, lui, corrige l'AST en pleine compilation, avant que le moindre `bytecode` soit généré. C'est cette différence qui donne l'impression que les méthodes générées étaient présentes dans le code source dès le départ.

J'ai donc suivi la même voie : la manipulation directe de l'`AST` de javac.

Lombok s'infiltre dans `com.sun.tools.javac.*` pour muter l'AST directement en mémoire, juste avant que javac n'émette le bytecode. Si vous ajoutez un nœud à cet arbre, javac le compile. C'est aussi simple que ça.

| Classe                | Son rôle                                                                  |
| --------------------- | ------------------------------------------------------------------------- |
| JCTree.JCClassDecl    | Nœud de déclaration d'une classe.                                         |
| JCTree.JCMethodDecl   | Nœud de méthode.                                                          |
| JCTree.JCVariableDecl | Nœud de champ ou de paramètre.                                            |
| TreeMaker             | Factory pour instancier de nouveaux nœuds AST.                            |
| Names                 | Gère l'internement des identifiants (évite d'utiliser des String brutes). |

La ligne clé de toute cette mécanique :

```java
classNode.defs = classNode.defs.prepend(getter);

```

`JCClassDecl.defs` contient tous les membres d'une classe (champs, méthodes, constructeurs). Comme c'est une instance immuable de `com.sun.tools.javac.util.List`, il faut la remplacer par une nouvelle liste qui inclut la méthode générée. Une fois le traitement terminé, javac compile cet AST modifié, et le getter se retrouve dans le fichier `.class` comme s'il avait toujours été là.

Pour accéder à ces fonctionnalités de bas niveau, il faut d'abord déballer (*unwrap*) le `ProcessingEnvironment` standard pour récupérer son implémentation interne javac :

```java
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);

    JavacProcessingEnvironment javacEnv =
        (JavacProcessingEnvironment) unwrap(processingEnv);

    Context context = javacEnv.getContext();
    this.treeMaker = TreeMaker.instance(context);
    this.names     = Names.instance(context);
    this.trees     = Trees.instance(processingEnv);
}

```

L'appel à `unwrap()` est nécessaire parce que des outils comme IntelliJ ou Gradle enveloppent souvent le `ProcessingEnvironment` dans un proxy dynamique. Lombok gère ça en remontant récursivement un champ `delegate` à travers la hiérarchie des classes via la réflexion Java.

Une fois le `TreeMaker` en main, construire un getter ressemble à assembler un arbre :

```java
private JCTree.JCMethodDecl createGetter(JCTree.JCVariableDecl field) {
    // Équivalent à : public FieldType getFieldName() { return this.fieldName; }
    return treeMaker.MethodDef(
        treeMaker.Modifiers(Flags.PUBLIC),
        names.fromString(toGetterName(field.name.toString())),
        field.vartype,
        List.nil(), List.nil(), List.nil(),
        treeMaker.Block(0L, List.of(
            treeMaker.Return(
                treeMaker.Select(treeMaker.Ident(names._this), field.name)
            )
        )),
        null
    );
}

```

Un détail important : appelez `treeMaker.at(classNode.pos)` avant d'instancier vos nouveaux nœuds. C'est ce qui définit correctement les positions dans le fichier source. Sans cela, le compilateur pointe des erreurs à des endroits complètement aléatoires.

Toutes ces API internes résident dans le module `jdk.compiler`, verrouillé par défaut depuis Java 9\. Il faut donc ajouter les directives `--add-exports` suivantes :

```xml
<!-- mini-lombok-processor/pom.xml -->
<compilerArgs>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
    <arg>--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
</compilerArgs>

```

Ajoutez aussi `-proc:none` lors de la compilation du module processeur lui-même , sinon il tente de traiter ses propres sources de manière récursive, et vous obtiendriez une belle boucle infinie. 

### Obstacle #2 : IntelliJ affiche des erreurs en rouge sur le code généré.

`mvn install` passe sans problème. Mais à l'ouverture de la démo dans IntelliJ, surprise : la classe `Person` essaie d'utiliser la variable `log` injectée par `@Logger`, et l'IDE répond :

```
Cannot resolve symbol 'log'

```

Maven compile correctement. L'éditeur, lui, est perdu.

La raison : `IntelliJ` n'utilise pas javac pour son analyse en temps réel. Il s'appuie sur son propre moteur, le `PSI` (*Program Structure Interface*), qui lit strictement le texte brut des fichiers sources. Puisque `log` n'est pas écrit dans `Person.java`, le PSI le signale comme manquant.

Les processeurs classiques qui appellent `Filer.createSourceFile()` produisent de vrais fichiers `.java` sur le disque, que le PSI peut lire. Mais les processeurs qui modifient l'AST à la volée ne laissent aucune trace sur le disque, leurs modifications existent uniquement en mémoire, pendant la durée de la compilation.

C'est exactement pour cela que Lombok fournit un plugin IntelliJ aussi massif. Il n'exécute pas le processeur d'annotations en arrière-plan, il dit explicitement au moteur PSI ce que chaque annotation va produire. Il lui chuchote : *`@Getter` présent ici → attends-toi à une méthode `getXxx()` à l'exécution*. C'est une base de code entièrement séparée, à maintenir en synchronisation parfaite avec le processeur principal.

Face à ce problème avec mini-Lombok, trois options s'offrent à vous :

- Vivre avec les lignes rouges : le projet compile et s'exécute correctement en ligne de commande, et cela illustre très concrètement le problème.
- Déléguer les builds de l'IDE à Maven (voir l'obstacle #3) : le bouton *Run* fonctionne alors, même si l'éditeur continue à se plaindre.
- Développer un vrai plugin IntelliJ : mais c'est un projet à part entière, bien au-delà du cadre de cette expérimentation.

### Obstacle #3 : IntelliJ et `--add-exports` refusent de coopérer

En essayant de lancer `Main` depuis le bouton `*Run* d'IntelliJ`, nouvelle erreur :

```
java: exporting a package from system module jdk.compiler is not allowed with --release

```

IntelliJ utilise par défaut son propre compilateur interne, pas Maven. Lors de ce processus, il passe automatiquement `--release 21`, ce qui entre en conflit avec `--add-exports` sur les modules système. De plus, les arguments définis dans `pom.xml` sont ignorés.

### La solution : déléguer à Maven

Il suffit d'aller dans **Settings > Build, Execution, Deployment > Build Tools > Maven > Runner > cocher "Delegate IDE build/run actions to Maven"**.

L'action *Run* invoque alors `mvn exec:java` en arrière-plan, et tous les flags définis dans `pom.xml` sont appliqués.

Un détail subtil : dans `mini-lombok-demo/pom.xml`, l'argument `--add-exports` doit être précédé du préfixe `-J` :

```xml
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>

```

Ce préfixe transmet le flag directement à la JVM qui exécute javac, plutôt qu'au compilateur lui-même. Notre processeur d'annotations s'exécute à l'intérieur de cette JVM hôte, il a besoin de ces droits d'accès lors de son exécution. Sans le `-J`, le flag n'affecterait que la compilation du code utilisateur, pas l'environnement d'exécution de javac.

C'est aussi pour ça que `--release` est absent du pom — il est incompatible avec `--add-exports` sur les modules internes. On utilise à la place :

```xml
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>

```

### Le résultat final

Une fois ces trois obstacles franchis, voilà ce que donne le clone en action :

```java
@Logger
public class Person {

    @Getter private String name;
    @Getter private int age;
    @Getter private String email;

    public Person(String name, int age, String email) { ... }

    public void introduce() {
        log.info("Salut, je suis " + name);  // 'log' injecté par @Logger
    }
    // Aucun getter écrit à la main.
}

```

Aucun fichier source généré sur le disque, aucune classe utilitaire parasite. Les méthodes et champs injectés vivent dans `Person.class` , invisibles dans les sources, mais bien là à l'exécution.

## Ce que cette expérience révèle sur Lombok

**Lombok n'est pas un processeur d'annotations standard.** JSR-269 a été conçue pour générer de nouveaux fichiers, un point c'est tout. Lombok choisit de modifier des nœuds AST existants à la volée, ce qui détourne complètement l'API de son objectif initial. C'est brillant, et un peu fou.

**Le plugin IntelliJ représente une charge de travail entièrement distincte.** Il oblige à réimplémenter toute la logique de génération, cette fois pour le moteur PSI. Chaque nouvelle annotation dans Lombok exige une double mise à jour : le processeur Java d'un côté, le plugin de l'éditeur de l'autre.

**Les API internes de javac sont instables par nature.** Les paquets sous `com.sun.tools.javac.*` n'offrent aucune garantie de compatibilité entre versions de Java. C'est pourquoi l'équipe Lombok maintient une collection d'adaptateurs (*shims*) pour chaque version majeure. Bâtir un outil sur ces fondations implique un fardeau de maintenance permanent.

À très bientôt et bon code !