CodeGym /Cours /Java Server /Dépendances directes et transitives

Dépendances directes et transitives

Java Server
Niveau 5, Leçon 0
Disponible

1. Les dépendances : pas une liste, mais un arbre

Si tu pensais qu'une ligne avec une bibliothèque signifie exactement une bibliothèque dans le projet, voici une petite mais utile déception. Dans l'écosystème Java, les bibliothèques ne vivent presque jamais seules — comme une bonne série où chaque personnage a son spin-off et des amis d'un univers parallèle. Donc, tout fonctionne ainsi : tu ajoutes une ligne dans dependencies, et Gradle tire quelques JARs supplémentaires. C'est le fonctionnement normal de la gestion des dépendances, pas des « déchets inutiles ».

Le mot le plus important de cette leçon : transitivité. En termes simples, une dépendance transitive est la dépendance d'une dépendance. Autrement dit, la bibliothèque que tu as choisie dit elle-même : « Pour que je fonctionne, j'ai aussi besoin de ces gens-là ». Gradle lit cela, construit un graphe (arbre) et assemble finalement le classpath.

Le projet sait déjà se compiler et se lancer, mais ce n'est pas suffisant. Tant que tu ne vois pas quelles bibliothèques sont vraiment arrivées dans le classpath et selon quelles règles, le baseline reste fragile : quelque chose fonctionne, mais expliquer pourquoi est difficile. Alors on ne s'intéresse plus seulement au lancement, mais aussi à l'origine des dépendances.

Commençons par l'exemple le plus clair possible. Dans build.gradle.kts, on peut écrire ceci :

dependencies {
    // Dépendance directe : tu demandes explicitement à Gradle d'ajouter Logback au projet.
    // Gradle tirera ensuite ce que Logback a déclaré comme ses propres dépendances.
    implementation("ch.qos.logback:logback-classic:1.5.32")
}

En apparence, c'est une seule dépendance. Mais en pratique, logback-classic ne mène pas une vie solitaire : elle a besoin de logback-core, et pour fonctionner via l'API logging standard — de slf4j-api. Donc l'ensemble final de bibliothèques « dans le projet » sera plus large qu'une seule ligne. C'est pourquoi aujourd'hui on apprend à penser non pas en liste, mais en arbre.

2. Dépendance directe : ta demande à Gradle

Quand tu écris une dépendance dans build.gradle.kts, tu fais une demande tout à fait formelle : « Mon projet a besoin de ce module dans telle version ». C'est la dépendance directe — celle que tu as ajoutée manuellement. Elle est visible dans le dépôt et dans l'historique Git, et la responsabilité en est ta, pas celle de Gradle ni de l'IDE. Donc les dépendances directes sont les données d'entrée pour la gestion des dépendances.

Dans notre ReadLater Starter, les dépendances directes peuvent ressembler à ceci (simplifié, pour parler du fond sans discuter de l'esthétique) :

dependencies {
    // Nécessaire pour ton code directement : tu importes org.slf4j.* et tu compiles avec
    implementation("org.slf4j:slf4j-api:2.0.17")

    // Nécessaire seulement à l'exécution : c'est l'implémentation du logging (SLF4J binding)
    runtimeOnly("ch.qos.logback:logback-classic:1.5.32")

    // Exemple d'une autre dépendance directe : sérialisation/désérialisation JSON
    implementation("tools.jackson.core:jackson-databind:3.0.4")
}

Il est important de saisir une idée pratique ici. Une dépendance directe n'est pas nécessairement « tout ce qui se retrouvera dans le projet ». C'est plutôt la liste de ce que tu as demandé et dans quelle configuration. Ensuite, Gradle commence à « développer » cette demande : il regarde les métadonnées de chaque bibliothèque et découvre quels artéfacts supplémentaires sont nécessaires pour elle-même.

Et encore un point que les débutants confondent étonnamment souvent. Les dépendances directes ne sont pas des plugins. Un plugin s'ajoute dans le bloc plugins, et une dépendance dans le bloc dependencies. Si tu as jamais essayé de « connecter Logback » via plugins { ... }, félicitations : tu n'es pas seul. C'est une quête très populaire pour les débutants. Mais le sens est différent : un plugin modifie le build, une dépendance finit dans le classpath de l'application.

3. Dépendance transitive : les demandes des bibliothèques

Imagine maintenant que chaque bibliothèque est un petit projet avec ses propres besoins. Les auteurs de Logback ne veulent pas copier tout SLF4J à l'intérieur de Logback — et pourquoi le feraient-ils. Les auteurs de Jackson ne veulent pas non plus mettre toutes les parties possibles du monde JSON dans un seul JAR. Donc une bibliothèque est publiée avec des métadonnées qui disent : « Pour m'utiliser, tire aussi ceci ». Gradle sait lire ces métadonnées et construire le graphe des dépendances.

Donc on a deux niveaux :

Dépendance directe : ce que tu as explicitement indiqué dans dependencies.
Dépendance transitive : ce qui est arrivé « en bonus », parce que tes dépendances directes en ont besoin.

Revenons à Logback. Si on dessine ça « dans le style Gradle », on obtient à peu près cette image (dans les rapports de dépendances, ça ressemble beaucoup à ça) :

# La ligne du haut est ce que tu as déclaré explicitement (dépendance directe).
# Tout ce qui est en dessous est ce que Gradle a tiré par les métadonnées (dépendances transitives).
ch.qos.logback:logback-classic:1.5.32
+--- ch.qos.logback:logback-core:1.5.32
\--- org.slf4j:slf4j-api:2.0.17

La ligne du haut est ce que tu as ajouté directement. Tout ce qui est en dessous, avec des indentations et des branches, ce sont les dépendances transitives. Et remarque un détail psychologiquement important : si tu vois soudainement slf4j-api dans le projet, c'est le plus souvent non pas une erreur ni « l'IDE a téléchargé des trucs inutiles », mais une transitivité normale.

Pour que cette idée devienne vraiment tangible, on la montre aussi sous forme de schéma. La logique est simple : l'application dépend de logback-classic directement, et logback-classic dépend à son tour d'autres modules transitivement.

flowchart LR
    %% Exemple d'arbre : la flèche de l'application est une dépendance directe, en dessous — les transitives
    App["ReadLater Starter"] -->|"dépendance directe"| Classic["logback-classic"]
    Classic -->|"transitivement"| Core["logback-core"]
    Classic -->|"transitivement"| Slf4j["slf4j-api"]

Ce schéma est le sens principal de la leçon en une image. Et oui, si tu penses maintenant : « Ça a l'air logique, mais pourquoi c'est important pour moi ? » — c'est une question normale. La réponse est simple : au runtime, tu n'as pas « une bibliothèque », mais toute une « équipe », et parfois quelqu'un d'inattendu se retrouve dans cette équipe. D'où la prochaine étape pratique : il faut savoir lire l'arbre des dépendances dans la sortie de Gradle, et non le deviner de mémoire.

4. Lire l'arbre des dépendances

Quand tu vois la sortie de dependencies pour la première fois, tu as facilement l'impression : « J'ai ouvert la porte d'un lave-linge, et 120 chaussettes sont tombées, et aucune n'est à moi ». C'est normal : l'arbre des dépendances a l'air bruyant. Mais on peut le lire assez tranquillement si on se rappelle quelques règles. Aujourd'hui, on installe justement cette habitude — sans essayer de plonger tout de suite dans l'« enfer des dépendances ».

La façon de lire la plus pratique : aller de haut en bas et toujours se poser une seule question : « qui a amené qui ? ». Les nœuds du haut sont ce que tu as déclaré directement (ou ce que les plugins et configurations ont déclaré, mais on ne touche pas à ça pour l'instant). Tout ce qui est imbriqué sous un nœud, c'est ses « passagers ». Les indentations montrent le niveau d'imbrication, c'est-à-dire la profondeur des « amis d'amis ».

Encore une idée importante : l'arbre dépend de la configuration. On connaît déjà les mots implementation et runtimeOnly. Dans le contexte de la transitivité, ça signifie : si une dépendance est déclarée comme runtimeOnly, ses dépendances transitives vont en premier dans le classpath runtime. Et si une dépendance est déclarée comme implementation, elle et ses dépendances transitives vont dans le classpath de compilation. Utile de s'en souvenir pour ne pas s'étonner que « le runtime a une chose et la compilation voit autre chose ».

Comparer les dépendances directes et transitives dans un petit tableau est pratique — pas pour la forme, mais pour que le cerveau arrête de se mélanger :

Ce qu'on compare Dépendance directe Dépendance transitive
Qui l'a « commandée » Toi, dans dependencies { ... } Une autre bibliothèque
Où elle est déclarée Dans ton build.gradle.kts Dans les métadonnées du module (POM / Gradle Module Metadata, etc.)
Pourquoi elle est nécessaire À ton code ou à ton runtime Pour que la bibliothèque que tu as connectée fonctionne
Pourquoi elle est « apparue soudainement » Parce que tu l'as ajoutée Parce que ta bibliothèque ne démarre/ne compile pas sans elle

Et encore un avertissement tranquille : parfois la même bibliothèque apparaît dans l'arbre plusieurs fois par différents chemins. Cela signifie que différentes dépendances « veulent » la même chose. Bonne nouvelle — on peut économiser de l'espace !

Mauvaise nouvelle — leurs versions peuvent différer. Quelle version choisir et comment résoudre les conflits, c'est un sujet distinct, et aujourd'hui on n'y va pas délibérément. Notre objectif est plus modeste : apprendre à voir l'origine des bibliothèques, pas à diriger tout cet orchestre tout de suite.

5. Mini-expérience : transitivité et configurations

On va maintenant faire une petite expérience de compréhension. Elle relie bien le sujet du jour à ce qu'on sait déjà des configurations de dépendances. L'idée n'est pas de casser le projet (bien que ce soit aussi une tradition), mais de voir : les dépendances transitives arrivent non pas dans le vide, mais dans des classpaths spécifiques — compile et runtime.

Supposons qu'on a ce morceau de build.gradle.kts (l'idée est plus importante que le contenu exact) :

dependencies {
    // Dépendance directe compile-time : sans elle, le code qui importe org.slf4j.* ne compile pas
    implementation("org.slf4j:slf4j-api:2.0.17")

    // Dépendance seulement pour l'exécution : l'implémentation du logging est nécessaire au runtime, mais pas obligatoire pour la compilation
    runtimeOnly("ch.qos.logback:logback-classic:1.5.32")
}

Et ce code minimal qui utilise l'API SLF4J. Il vit dans notre application (par exemple dans ReadLaterApplication) et se compile comme une classe Java ordinaire :

package com.example.readlater;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReadLaterApplication {
    // On prend le logger via la façade SLF4J : à la compilation, slf4j-api est nécessaire
    private static final Logger log = LoggerFactory.getLogger(ReadLaterApplication.class);

    public static void main(String[] args) {
        // Au runtime, l'implémentation concrète/binding (par ex. Logback) sera prise en charge par le classpath
        log.info("ReadLater Starter is starting..."); // (sortie approximative : INFO ... ReadLater Starter is starting...)
    }
}

Maintenant, le moment important. Si tu supprimes la dépendance directe slf4j-api (c'est-à-dire que tu supprimes la ligne implementation("org.slf4j:slf4j-api:...")), beaucoup s'attendent à : « Pas grave, Logback le tirera quand même transitivement ». Et c'est là qu'apparaît la connexion entre le sujet du jour et runtimeOnly.

logback-classic est bien lié à SLF4J, et slf4j-api arrive souvent comme dépendance transitive. Mais puisque logback-classic est déclaré comme runtimeOnly, tout ce qui « vient avec lui » va en premier dans le classpath runtime, et non dans le classpath de compilation. Le résultat est simple et un peu décevant : le code cesse de se compiler, parce que le compilateur ne voit pas org.slf4j.LoggerFactory.

En pratique, tu obtiens une erreur du genre « package org.slf4j does not exist » ou « cannot find symbol LoggerFactory ». Et ce n'est pas de la magie, mais une mécanique honnête : la dépendance directe contrôle où et à quel stade la bibliothèque est disponible, et la transitivité « obéit » à cette configuration.

La morale de cette expérience est tout à fait pratique. Même si une bibliothèque peut arriver transitivement, on la déclare parfois quand même explicitement : par exemple, parce qu'on l'utilise dans le code à la compilation ou qu'on veut fixer explicitement la version. Mais le sujet « comment fixer les versions des dépendances transitives et que faire en cas de conflits » est déjà le niveau de complexité suivant, et on n'y saute pas encore.

À ce stade, il suffit de prononcer avec assurance la phrase : « J'ai des dépendances directes, et elles amènent des transitives ; l'ensemble final des bibliothèques est un arbre ». Ensuite, il faut une compétence purement pratique : regarder cet arbre avec la commande Gradle, et non le deviner.

6. Erreurs typiques avec les dépendances transitives

Maintenant que l'idée d'« arbre » est là, il est utile de passer tranquillement en revue les pièges habituels. Ils sont généralement liés non pas à des cas complexes, mais à un modèle mental incorrect : la personne est sincèrement convaincue de comprendre les dépendances, puis passe quelques heures à se disputer avec la réalité, Gradle et l'univers. Ces erreurs sont faciles à repérer à l'avance si on sait où regarder et ce qui est normal.

Erreur n°1 : croire que le projet ne contient que ce qui est écrit dans dependencies.
Cette façon de penser fait ressembler tout rapport de dépendances à « Gradle a téléchargé quelque chose par lui-même ». En réalité, tu ne vois presque toujours que la pointe de l'iceberg : la dépendance directe est ta demande, et les dépendances transitives sont les composants sans lesquels la bibliothèque ne fonctionne pas.

Erreur n°2 : paniquer face aux bibliothèques « en trop » et chercher immédiatement l'IDE coupable.
L'IDE peut vraiment suggérer beaucoup de choses, mais elle n'invente généralement pas les dépendances. Si un JAR est arrivé dans le projet, il est arrivé le plus souvent parce qu'un autre JAR l'a déclaré dans ses métadonnées. Gradle lit ces métadonnées (POM, descriptions de modules, etc.), et c'est pour ça que l'arbre grandit.

Erreur n°3 : ne pas lier la transitivité aux configurations (implementation vs runtimeOnly).
Parfois on croit que « si la bibliothèque est dans le projet, alors elle est disponible partout ». Mais Gradle n'a pas cet état flou : il y a le classpath de compilation et le classpath runtime, et où vont les dépendances transitives dépend de la façon dont la dépendance directe est déclarée. C'est pourquoi on peut accidentellement supprimer le slf4j-api « explicite » et obtenir une erreur de compilation, même si « il doit bien être quelque part ».

Erreur n°4 : essayer de guérir un arbre de dépendances incompréhensible en ajoutant encore plus de dépendances « au hasard ».
On a vraiment envie d'agir comme dans les mèmes : « ça ne marche pas — ajoute encore une ligne ». Le problème c'est que l'arbre devient encore plus épais, et la cause encore moins visible. Il est bien plus utile de d'abord comprendre qui a amené qui, et seulement ensuite prendre une décision. C'est précisément ça qu'on apprend aujourd'hui : lire l'origine des bibliothèques, pas jouer à la loterie avec les dépendances.

Erreur n°5 : penser que les dépendances transitives sont des « déchets inutiles » qu'on peut simplement ignorer.
Parfois les dépendances transitives ne sont vraiment pas utilisées directement par ton code, mais elles sont utilisées par les bibliothèques que tu as connectées. Si on les considère bêtement comme « en trop », on arrive facilement à un état où le projet compile, mais à l'exécution il plante avec une étrange erreur ClassNotFoundException. Au niveau de notre sujet actuel, il suffit de retenir : la transitivité n'est pas un hasard, mais un moyen pour les bibliothèques de déclarer honnêtement leurs exigences.

1
Mission
Java Server, niveau 5, leçon 0
Bloqué
Dépendance directe pour l'API de journalisation
Dépendance directe pour l'API de journalisation
1
Mission
Java Server, niveau 5, leçon 0
Bloqué
SLF4J transitif via Logback
SLF4J transitif via Logback
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION