Déclaration de module Java avec module-info.java
Déclarez un module Java avec module-info.java — requires, exports, opens, uses, provides.
Un module est déclaré dans un fichier source spécial, module-info.java, placé à la racine de l'arborescence source du module (à côté du package principal, et non à l'intérieur). Il se compile en module-info.class. Ce fichier ne contient aucun code ordinaire — seulement un bloc module listant des directives qui décrivent la frontière du module.
La structure du fichier
module com.acme.orders {
requires com.acme.common; // I depend on this module
requires transitive java.sql; // ...and so does anyone who requires me
exports com.acme.orders.api; // public to everyone
exports com.acme.orders.spi to com.acme.web; // public to one module only
opens com.acme.orders.model; // deep reflective access (e.g. for Jackson)
uses com.acme.orders.PricingRule; // I consume this service
provides com.acme.orders.PricingRule // I supply an implementation
with com.acme.orders.StandardPricing;
}Le nom du module (com.acme.orders) est un identifiant pointé, suivant la convention du préfixe DNS inversé des packages qu'il contient. Ce n'est pas un package — c'est son propre espace de noms, et deux modules ne peuvent pas partager un package.
requires — déclarer les dépendances
requires <module> signifie « j'ai besoin des packages exportés par ce module pour compiler et s'exécuter. » Le résolveur échoue au démarrage si un module requis est absent. Deux modificateurs importants :
requires transitive— réexporte la dépendance. Tout module qui vous requiert la lit automatiquement aussi. Utilisez-le lorsque les types d'un module requis apparaissent dans vos propres signatures publiques (une méthode retournant unjava.sql.Connectionoblige les appelants à voirjava.sql).requires static— une dépendance uniquement à la compilation, optionnelle à l'exécution (pour les processeurs d'annotations, les intégrations optionnelles).
java.base est requis implicitement ; vous ne l'écrivez jamais.
exports — déclarer votre API publique
exports <package> rend les types public de ce package visibles aux autres modules. Tout ce qui n'est pas exporté est fortement encapsulé — invisible même s'il est public. La forme qualifiée, exports <package> to <module>, <module>, restreint la visibilité à une liste d'autorisation nommée, utile pour les packages SPI partagés uniquement entre vos propres modules.
Notez que exports est par package, jamais récursif : exporter com.acme.api n'exporte pas com.acme.api.internal.
opens — autoriser la réflexion profonde
exports accorde un accès à la compilation aux membres public. Cela n'accorde pas d'accès réflexif aux membres non publics. Des frameworks comme Jackson, Hibernate et Spring utilisent setAccessible(true) pour atteindre les champs private — cela nécessite opens :
opens <package>— accorde un accès réflexif à l'exécution (y compris aux membresprivate) à tous les modules.opens <package> to <module>— qualifié, uniquement aux modules nommés.open module com.acme.orders { … }— ouvre chaque package (un outil de migration radical).
La distinction est importante : vous utilisez exports pour un package d'API, mais vous utilisez opens pour un package de classes de données sur lesquelles vous voulez qu'un sérialiseur effectue de la réflexion sans en faire partie de votre API à la compilation.
uses / provides — les services
Ces directives configurent le patron ServiceLoader : uses <Service> déclare que vous consommez une interface de service, et provides <Service> with <Impl> déclare une implémentation. Ils ont leur propre chapitre — voir Services de module — notez simplement ici qu'ils vivent dans le même descripteur.
Exemple pratique : construire un descripteur avec l'API
Vous écrivez normalement module-info.java et laissez javac produire le descripteur. Mais la même structure est disponible de manière programmatique via ModuleDescriptor.newModule(...), qui est un miroir fidèle de la syntaxe des directives — en construire un est la façon la plus claire de voir ce que chaque directive devient.
Ce qu'il faut retenir de l'exécution :
- Les noms des méthodes du constructeur correspondent un à un aux directives :
.requires(...),.exports(...),.opens(...),.uses(...),.provides(...). En relisant le résultat, le descripteur contient exactement les informations d'unmodule-info.java— preuve que le fichier est de la pure métadonnée, pas du code exécutable. - Le
requiredejava.sqlest imprimé avec un modificateur[TRANSITIVE]tandis quecom.acme.commonest imprimé sans. Ce modificateur est ce qui réexportejava.sqlaux modules en aval ; le require simple garde la dépendance privée à ce module. - Les deux exports sont imprimés différemment :
com.acme.orders.apicomme « (to all) » etcom.acme.orders.spicomme « to [com.acme.web] ». Un export qualifié porte sa liste d'autorisation cible dans le descripteur — le résolveur l'applique, donc aucun autre module ne peut lire le package SPI. opensapparaît dans sa propre section, séparée desexports. Le descripteur maintient l'exposition à la compilation et l'accès réflexif à l'exécution comme des faits distincts, c'est pourquoi un sérialiseur a besoin deopensmême si le package est déjà exporté.usesetprovidessont enregistrés juste à côté du reste — les déclarations de service font partie de la frontière du module, pas un fichier de configuration séparé comme c'était le cas sur le classpath (META-INF/services). Services de module transforme ces directives en unServiceLoaderfonctionnel.
Erreurs courantes
- Placer
module-info.javaà l'intérieur d'un répertoire de package — il doit se trouver à la racine source. - Confondre
exports(à la compilation, membres publics) avecopens(à l'exécution, tous les membres). UneJsonMappingExceptionconcernant un champ inaccessible signifie presque toujours unopensmanquant. - Oublier
requires transitivelorsque vos méthodes publiques exposent les types d'un autre module, obligeant chaque appelant à ajouter le require manuellement.
Le chapitre suivant, Types de module, revient sur les trois types de module — nommé, automatique et non nommé — et comment une application partiellement modulaire continue de fonctionner pendant que vous migrez vers les modules. Pour une vue d'ensemble de la raison d'être du système de modules, voir l'introduction aux modules.