Skip to content

38. Compiler, Empaqueter et Exécuter des Modules

Table des matières


Une fois qu’un module est défini avec un fichier module-info.java, il doit être compilé, empaqueté et exécuté à l’aide d’outils conscients des modules.

Cette section explique comment la toolchain Java change lorsque des modules sont impliqués.

38.1 Le Module Path vs le Classpath

JPMS introduit un nouveau concept : le module path.

Il existe aux côtés du classpath traditionnel, mais les deux se comportent de manière très différente.

Aspect Classpath Module path
Structure Liste plate de JAR Modules avec identité
Encapsulation Aucune Forte
Vérification des dépendances Aucune Stricte
Split packages Autorisés Interdits (modules nommés)
Ordre de résolution Dépendant de l’ordre Déterministe

Note

  • Un JAR placé sur le module path est traité comme un module :
    • S il contient un module-info.class, il devient un named module.
    • S il ne contient pas de descripteur de module, il devient un automatic module.
  • Un JAR placé sur le classpath n est pas traité comme un module.
    • À la place, il devient partie du unnamed module, avec toutes les autres entrées du classpath.
  • Un JAR modulaire (c est-à-dire un JAR contenant module-info.class) peut toujours être utilisé comme un JAR régulier.
    • S il est placé sur le classpath au lieu du module path, il est traité comme partie du unnamed module, permettant aux applications non modulaires de l utiliser sans adopter le module system.
  • Split packages :
    • Sont autorisés sur le classpath (plusieurs JAR peuvent contenir des classes dans le même package).
    • Sont interdits pour les named modules ou automatic modules sur le module path.

38.2 Options de Ligne de Commande Relatives aux Modules

Lorsqu’on travaille avec le Java Module System, java et javac fournissent des options spécifiques pour compiler et exécuter des applications modulaires.

Certaines options sont communes, tandis que d’autres sont spécifiques à un outil.

38.2.1 Options Disponibles dans java et javac

Ces options peuvent être utilisées aussi bien lors de la compilation que lors de l’exécution :

  • --module ou -m
    Utilisée pour compiler ou exécuter uniquement le module spécifié.

  • --module-path ou -p
    Spécifie les chemins dans lesquels java ou javac rechercheront les définitions de modules.

Le système de modules Java fournit trois options spéciales en ligne de commande, utilisables avec javac et java, qui permettent de modifier temporairement les règles d’accès entre modules sans changer les fichiers module-info.java. Ces options s’appliquent uniquement à l’exécution courante de la commande et ne modifient pas de manière permanente les descripteurs de modules.

Les trois options sont :

  • --add-reads
  • --add-exports
  • --add-opens

Elles sont généralement utilisées pour les tests, la rétrocompatibilité, les phases de migration, ou lorsque l’on travaille avec des modules tiers que l’on ne peut pas modifier.

Supposons par exemple que moduleA doive accéder aux types publics de moduleB, mais que :

  • moduleA ne déclare pas requires moduleB;
  • moduleB n’exporte pas le package requis vers moduleA

Au lieu de modifier les fichiers module-info.java, il est possible d’accorder temporairement l’accès nécessaire avec :

javac --add-reads moduleA=moduleB \
      --add-exports moduleB/com.modB.package1=moduleA \
      ...

java  --add-reads moduleA=moduleB \
      --add-exports moduleB/com.modB.package1=moduleA \
      ...

Signification des options :

  • --add-reads moduleA=moduleB
    Déclare temporairement que moduleA lit moduleB.
    Cela revient à ajouter requires moduleB; dans le descripteur de moduleA.
    Ainsi, moduleA peut accéder aux packages exportés de moduleB.

  • --add-exports moduleB/com.modB.package1=moduleA
    Exporte temporairement le package com.modB.package1 du module moduleB vers moduleA.
    Cela équivaut à ajouter :
    exports com.modB.package1 to moduleA;
    dans le descripteur de moduleB.

Distinction importante :

  • --add-reads établit la lisibilité au niveau du module.
  • --add-exports accorde l’accès à des packages spécifiques.
  • --add-opens (non illustré ci-dessus) fonctionne comme --add-exports, mais autorise également l’accès par réflexion profonde (deep reflection), souvent nécessaire pour certains frameworks.

Ces options ne modifient pas les métadonnées compilées du module ; elles ajustent simplement le graphe des modules pour l’invocation spécifique de javac ou java.

38.2.2 Options Applicables Uniquement à javac

Ces options s’appliquent uniquement à la phase de compilation :

  • --module-source-path
    (aucun raccourci)
    Utilisée par javac pour localiser les définitions des modules source.

  • -d
    Spécifie le répertoire de sortie dans lequel les fichiers .class seront générés après la compilation.

38.2.3 Options Applicables Uniquement à java

Ces options s’appliquent uniquement à la phase d’exécution :

  • --list-modules
    (aucun raccourci)
    Liste tous les modules observables puis termine.

  • --show-module-resolution
    (aucun raccourci)
    Affiche les détails de la résolution des modules lors du démarrage de l’application.

  • --describe-module ou -d
    Décrit un module spécifié puis termine.

38.2.4 Distinctions Importantes

L’option -d a des significations différentes selon l’outil :

  • Dans javac, -d définit le répertoire de sortie des fichiers de classes compilés.
  • Dans java, -d est un raccourci pour --describe-module.

De plus, -d ne doit pas être confondue avec -D (D majuscule).

  • -D est utilisée lors de l’exécution d’un programme Java pour définir des propriétés système sous forme de paires nom-valeur sur la ligne de commande.
java -Dconfig.file=app.properties com.example.Main

Dans cet exemple, -Dconfig.file=app.properties définit une propriété système accessible à l’exécution via System.getProperty("config.file").


38.3 Compiler un Module Unique

Pour compiler un module, vous devez spécifier le chemin des sources du module et le répertoire de destination.

javac -d out \
src/com.example.hello/module-info.java \
src/com.example.hello/com/example/hello/Main.java

Une approche plus évolutive utilise --module-source-path.

javac --module-source-path src \
      -d out \
      $(find src -name "*.java")

Note

--module-source-path indique à javac où trouver plusieurs modules à la fois.


38.4 Compiler des Modules Multiples Interdépendants

Lorsque des modules dépendent les uns des autres, leurs dépendances doivent être résolubles à la compilation.

--module-path mods (répertoire d’exemple contenant des modules interdépendants) doit contenir des JAR modulaires déjà compilés ou des répertoires de modules compilés (chacun avec son propre module-info.class).

javac -d out \
--module-source-path src \
--module-path mods \
$(find src -name "*.java")

Ici :

  • --module-source-path localise les arbres de sources des modules
  • --module-path fournit des modules déjà compilés

38.5 Empaqueter un Module dans un JAR Modulaire

Après la compilation, les modules sont généralement empaquetés sous forme de fichiers JAR.

Un JAR modulaire contient un module-info.class à sa racine.

Si module-info.class est présent, le JAR devient automatiquement un module nommé et son nom est pris depuis le descripteur (et non depuis le nom du fichier).

jar --create \
--file mods/com.example.hello.jar \
--main-class com.example.hello.Main \
-C out/com.example.hello .

Note

Un JAR avec module-info.class est un module nommé, pas un module automatique. Lorsqu’un JAR contient un module-info.class, son nom de module est pris depuis ce fichier et n’est pas déduit du nom du fichier.


38.6 Exécuter une Application Modulaire

Pour exécuter une application modulaire, vous utilisez le module path et spécifiez le nom du module.

java --module-path mods \
--module com.example.hello/com.example.hello.Main

Vous pouvez raccourcir cela en utilisant les options -p et -m.

java -p mods -m com.example.hello/com.example.hello.Main

Note

Lors de l’utilisation de modules nommés, le classpath est ignoré pour la résolution des dépendances entre modules.


38.7 Explication des Directives de Module

Le fichier module-info.java contient des directives qui décrivent les dépendances, la visibilité et les services.

Chaque directive a une signification précise.

38.7.1 requires

La directive requires déclare une dépendance vers un autre module.

Sans elle, les types du module dépendant ne peuvent pas être utilisés.

module com.example.app {
    requires com.example.lib;
}

Effets de requires :

  • La dépendance doit être présente à la compilation et à l’exécution
  • Les packages exportés du module requis deviennent accessibles

38.7.2 requires transitive

requires transitive expose une dépendance aux modules en aval.

Il propage la lisibilité.

module com.example.lib {
    requires transitive com.example.util;
    exports com.example.lib.api;
}

Signification :

  • Tout module qui requiert com.example.lib lit automatiquement com.example.util
  • Les appelants n’ont pas besoin de déclarer requires com.example.util explicitement

Note

Cela est similaire aux « dépendances publiques » dans d’autres systèmes de modules.

Lisible ≠ exporté : une dépendance transitive n’exporte pas automatiquement vos packages.

38.7.3 exports

exports rend un package accessible aux autres modules.

Seuls les packages exportés sont visibles à l’extérieur du module.

module com.example.lib {
    exports com.example.lib.api;
}

Les packages non exportés restent fortement encapsulés.

38.7.4 exports ... to (Exports Qualifiés)

Un export qualifié restreint l’accès à des modules spécifiques.

module com.example.lib {
    exports com.example.internal to com.example.friend;
}

Seuls les modules listés peuvent accéder au package exporté.

38.7.5 opens

opens permet un accès réflexif profond à un package.

Il est principalement utilisé par des frameworks utilisant la réflexion.

module com.example.app {
    opens com.example.app.model;
}

Note

opens NE rend PAS un package accessible à la compilation. Il affecte uniquement la réflexion à l’exécution.

38.7.6 opens ... to (Opens Qualifiés)

Vous pouvez restreindre l’accès réflexif à des modules spécifiques.

module com.example.app {
    opens com.example.app.model to com.fasterxml.jackson.databind;
}

Note

opens affecte la réflexion ; exports affecte la compilation et la visibilité des types.

38.7.7 Table des Directives Principales

Directive But
requires Déclarer une dépendance
requires transitive Propager une dépendance
exports Exposer un package
exports ... to Exposer à des modules spécifiques
opens Autoriser la réflexion à l’exécution
opens ... to Restreindre l’accès réflexif

38.7.8 Exports vs Opens — Accès à la Compilation vs à lExécution

Visibilité Compilation ? Réflexion à l’exécution ?
exports Oui Non
opens Non Oui
exports ... to Oui (modules limités) Non
opens ... to Non Oui (modules limités)

Important

JPMS ajoute un module path, mais le classpath existe toujours. Ils peuvent coexister, mais les modules nommés ont la priorité.


38.8 Modules Nommés, Automatiques et Unnamed

JPMS supporte différents types de modules afin de permettre une migration progressive depuis le classpath.

JPMS doit interopérer avec du code legacy.

Pour supporter l’adoption progressive, la JVM reconnaît trois catégories différentes de modules.

38.8.1 Modules Nommés

Un module nommé possède un module-info.class et une identité stable.

  • Encapsulation forte
  • Dépendances explicites
  • Support complet JPMS

38.8.2 Modules Automatiques

Un JAR sans module-info placé sur le module path devient un module automatique.

Son nom est dérivé du nom du fichier JAR.

  • Lit tous les autres modules
  • Exporte tous les packages
  • Pas d’encapsulation forte

Note

Les modules automatiques existent pour faciliter la migration. Ils ne conviennent pas comme conception à long terme.

38.8.3 Module Unnamed

Le code sur le classpath appartient au module unnamed.

  • Lit tous les modules nommés
  • Tous les packages sont ouverts
  • Ne peut pas être requis par des modules nommés

Note

Le module unnamed préserve le comportement legacy du classpath.

38.8.4 Résumé Comparatif

Type de module module-info présent ? Encapsulation Lit
Named Oui Forte Déclarés seulement
Automatic Non Faible Tous les modules
Unnamed Non Aucune Tous les modules

38.9 Approche Top-Down et Bottom-Up pour modulariser une application

Lors de la migration d’une application existante (non modulaire) vers le Java Platform Module System (JPMS), deux stratégies principales peuvent être adoptées : top-down et bottom-up.
Ces deux approches nécessitent une compréhension claire des interactions entre les modules nommés, les modules automatiques et le module non nommé.

38.9.1 Approche Top-Down

Dans une approche top-down, on commence par modulariser le module principal de l’application, puis on migre progressivement ses dépendances.

38.9.1.1 Règles fondamentales

  1. Un JAR placé sur le module path devient un module automatique.
  2. Son nom est déterminé soit :
    • À partir de l’entrée Automatic-Module-Name dans le manifeste,
    • Soit dérivé du nom du fichier JAR (les tirets sont remplacés par des points et les numéros de version sont ignorés).
      Exemple :
      mysql-connector-java-8.0.11.jarmysql.connector.java
  3. Un module automatique :

    • Exporte tous ses packages.
    • Lit tous les autres modules.
  4. Un JAR placé sur le classpath appartient au module non nommé.

  5. Le module non nommé :
    • Exporte tous ses packages.
    • Peut lire tous les autres modules.
  6. Cependant, il n’a pas de nom ; aucun module ne peut donc déclarer requires à son égard.

  7. Les modules nommés explicitement (avec un fichier module-info.java)

  8. Peuvent déclarer des dépendances à l’aide de :
    requires some.module;
    
  9. Peuvent dépendre :
    • D’autres modules nommés
    • De modules automatiques
  10. Ne peuvent pas dépendre du module non nommé (puisqu’il n’a pas de nom).

Conséquence importante :

Un module nommé peut lire un module automatique, mais il ne peut pas lire le module non nommé.

38.9.1.2 Implications pratiques

Supposons :

  • JAR de l’application = A
  • A dépend directement de B
  • B dépend de C

Si vous modularisez A en premier :

  • A doit déclarer requires B;
  • Donc B doit être placé sur le module path (module nommé ou automatique)
  • Si B devient un module nommé :
  • C doit également être placé sur le module path (nommé ou automatique)

Ainsi, dans une migration top-down :

  • On commence par la couche application.
  • On modularise progressivement les dépendances vers l’extérieur.
  • Les modules automatiques sont souvent utilisés temporairement pendant la transition.

38.9.1.3 Résumé des règles d’accès

Type de module Exporte Peut lire
Module nommé Exportations déclarées uniquement Modules requis uniquement
Module automatique Tous les packages Tous les modules
Module non nommé Tous les packages Tous les modules

Important

  • Les modules automatiques et non nommés sont permissifs.
  • Les modules nommés imposent des règles explicites de dépendance et d’export.

38.9.2 Approche Bottom-Up

Dans une approche bottom-up, on commence par modulariser les bibliothèques de plus bas niveau, puis on progresse vers les modules de niveau supérieur, jusqu’à l’application principale.

38.9.2.1 Stratégie principale

On convertit d’abord les bibliothèques fondamentales en modules nommés correctement définis avec un descripteur explicite module-info.java.

Ensuite :

  • Les modules qui en dépendent sont modularisés.
  • Enfin, l’application principale devient elle aussi un module nommé.

Cette approche met l’accent sur :

  • Des relations requires explicites
  • Des exports maîtrisés
  • Une encapsulation forte dès le départ

38.9.2.2 Avantages architecturaux

Comparés aux modules automatiques :

  • Les modules nommés n’exportent que ce qui est explicitement déclaré.
  • Ils ne lisent pas implicitement tous les autres modules.
  • Les frontières d’encapsulation sont clairement définies.

La modularisation bottom-up conduit généralement à :

  • Un graphe de dépendances plus propre
  • Une meilleure maintenabilité
  • Des frontières de modules plus solides

38.9.3 Comparaison conceptuelle

Top-Down

  • On commence par l’application principale.
  • Les dépendances sont modularisées selon les besoins.
  • On s’appuie souvent temporairement sur des modules automatiques.
  • Migration initiale plus rapide.

Bottom-Up

  • On commence par les bibliothèques cœur.
  • Les descripteurs de modules sont définis strictement dès le départ.
  • La migration progresse vers le haut.
  • Architecture modulaire plus disciplinée et robuste.

38.9.4 Perspective de migration

En pratique, les projets réels combinent souvent les deux stratégies :

  • Une migration top-down permet d’activer rapidement l’exécution modulaire.
  • Une phase de raffinement bottom-up remplace ensuite les modules automatiques par des modules nommés correctement définis.

Cette approche hybride permet une adoption progressive du JPMS tout en renforçant progressivement l’encapsulation et la clarté architecturale.


38.10 Inspection des Modules et des Dépendances

38.10.1 Décrire les Modules avec java

java --describe-module java.sql

Cela affiche exports, requires et services d’un module.

38.10.2 Décrire les JAR Modulaires

jar --describe-module --file mylib.jar

38.10.3 Analyser les Dépendances avec jdeps

jdeps analyse statiquement les dépendances de classes et de modules.

jdeps myapp.jar
jdeps --module-path mods --check my.module

Pour détecter l’utilisation d’API internes du JDK :

jdeps --jdk-internals myapp.jar

jlink construit un runtime Java minimal contenant uniquement les modules requis par une application.

jlink
--module-path $JAVA_HOME/jmods:mods
--add-modules com.example.app
--output runtime-image

Avantages :

  • runtime plus petit
  • démarrage plus rapide
  • aucun module JDK inutilisé

38.12 Créer des Applications Autonomes avec jpackage

jpackage construit des installateurs spécifiques à la plateforme ou des images applicatives.

jpackage
--name MyApp
--input mods
--main-module com.example.app/com.example.Main

jpackage peut produire :

  • .exe / .msi (Windows)
  • .pkg / .dmg (macOS)
  • .deb / .rpm (Linux)

38.13 Résumé Final JPMS en Pratique

  • JPMS introduit une encapsulation forte et des dépendances fiables
  • Les modules remplacent les conventions fragiles du classpath
  • Les services permettent des architectures découplées
  • Les modules automatiques et le module unnamed supportent la migration
  • jlink et jpackage permettent des modèles modernes de déploiement