Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | Elémentaire |
Les modules permettent d'organiser le code afin de le déployer et de déclarer les dépendances entre eux chacun dans leurs fichiers de description respectifs. Un module est un artefact pris en charge par la JVM au même titre que les packages, les classes, les interfaces, ...
Les modules sont un nouveau type d'éléments dans le langage Java. Avant Java 9, les classes sont regroupées dans des packages. A partir de Java 9, il est possible de regrouper des packages dans un module. Un module Java contient donc un ou plusieurs packages qui vont ensemble. Un module peut être une partie d'une application, une API de la plate-forme Java ou une API tierce.
Un module doit respecter plusieurs caractéristiques :
Les modules permettent de renforcer l'intégrité de la plateforme et des applications.
Les modules mettent en oeuvre une encapsulation plus forte en permettant de déclarer explicitement les packages qui seront exportés et donc utilisable par d'autres modules. Les autres packages qui ne sont pas exportés ne seront accessibles que par le module lui-même.
Les modules ajoutent de nouvelles règles de visibilité et donc d'accès au langage Java. Dans un module, seules les classes publiques dans des packages exportés sont utilisables par d'autres modules. Cela change profondément la visibilité public historique.
Le système de modules définit les règles de lisibilité et d'accessibilité aux modules qui reposent sur deux concepts :
La description d'un module permet de définir le ou les modules dont il dépend. Cela permet d'obtenir une configuration plus fiable des modules. Cette configuration peut avoir plusieurs utilités : par exemple cela permet à la JVM de vérifier au démarrage que tous le modules requis sont présents dans le module-path.
Le système de modules propose aussi un support pour des services avec un couplage faible. Les services sont des fonctionnalités fournies et utilisées en utilisant l'interface java.util.ServiceLoader.
Un module nommé doit obligatoirement contenir un descripteur de module. C'est un fichier nommé module-info.java à la racine des sources du module qui contient des informations de configuration concernant le module :
Un module possède obligatoirement un nom, soit implicite ou explicite.
Par défaut, un module est hermétique :
Ce chapitre contient plusieurs sections :
Un module Java est un artefact qui possède un nom et contient des packages. Le code est organisé en packages qui contiennent des types (classes, interfaces, énumérations, annotations, ...) et éventuellement des ressources. Chaque module nommé doit posséder un descripteur de module.
Ainsi, un module est une façon de regrouper des fichiers .class et des ressources, ajoutant un niveau d'agrégation supérieur à celui des packages. Du code Java est regroupé dans différentes structures :
Un module est donc un artefact qui contient :
Un module permet de regrouper des packages dans la hiérarchie de répertoires correspondantes contenant les fichiers .class issus de la compilation.
Les packages contenus dans un module sont identiques aux packages historiquement utilisés en Java depuis sa création. Dans un module, les packages ont un rôle supplémentaire : ils sont utilisés pour déterminer quels sont les éléments publics qui seront accessibles en dehors du module.
Un module ne peut pas contenir d'autres modules.
Physiquement en Java, la forme la plus simple d'un module est une archive jar qui contient à sa racine un fichier module-info.class issu de la compilation du fichier module-info.java.
Chaque module doit avoir ses sources dans son propre répertoire et avoir un fichier module-info.java à la racine de l'arborescence de ses sources.
Le répertoire des sources peut ne contenir que le code d'un module : c'est par exemple le format utilisé par les projets Maven.
Résultat : |
src
|--module-info.java
|--com
| |--jmdoudoux
| |--module
| |--MaClasse.java
|
Le répertoire des sources peut contenir le code de plusieurs modules : dans ce cas, le code de chaque module doit être dans un sous-répertoire dédié chacun contenant un fichier module-info.java. C'est par exemple le format utilisé pour les sources du JDK.
Une bonne pratique est alors de nommé de répertoire du chaque module avec le nom du module correspondant.
Résultat : |
src
|--fr.jmdoudoux.monmodulea
| |--module-info.java
| |--com
| |--jmdoudoux
| |--modulea
| |--MaClasseA.java
|--fr.jmdoudoux.monmoduleb
| |--module-info.java
| |--com
| |--jmdoudoux
| |--moduleb
| |--MaClasseB.java
|
Les méta-données d'un module sont fournies dans un descripteur de modules. Ces méta-données doivent répondre à plusieurs questions :
Le fichier qui va contenir ces informations est le descripteur de module. Il permet de fournir des éléments concernant la description d'un module notamment :
Cette description peut aussi être complété avec des informations plus spécifiques
Chaque module possède un descripteur de module. Ce descripteur est un fichier source Java dont le nom est obligatoirement module-info.java. Ce fichier doit être compilé pour obtenir un fichier module-info.class.
Un descripteur de module utilise une syntaxe spécifique utilisant des mots clés contextuels de la forme :
[open] module <nom-module> {
[export <nom-package> [to <nom-module>]]*
[requires [transitive] <nom-module>] *
[opens <nom-packa> [to <nom-module]]*
[provides <type-service> with <nom-classe>]*
[uses <type-service>]*
}
La première chose à définir est le nom du module. Le nom d'un module est important car c'est ce nom qui sera utilisé pour exprimer qu'il est une dépendance d'un autre module.
Le nom d'un module est composé d'un ou plusieurs identifiants valide en Java chacun séparé par un caractère point. Ce nom doit respecter les mêmes règles que celles du nom des packages notamment les éléments qui composent le nom doivent être des identifiants Java valides.
Le choix du nom d'un module devrait prendre en compte au moins trois contraintes :
Si vous contrôlez tous les modules qui compris tous les modules qui sont des dépendances, alors il est possible de nommer les modules à sa guise et de changer leur nom à tout moment.
Si le module doit être publié pour être utilisé par d'autres, alors il faut garantir une certaine unicité sur le nom du module. Il y a deux manières principales d'y parvenir :
L'utilisation du nom de domaine inversé comme préfixe garantie une unicité plus forte mais c'est aussi très verbeux. Mais les informations les moins utiles se trouvent au début du nom (par exemple com, net, fr, ...). De plus, un changement du nom de domaine implique de renommer le module ce qui risque de compliquer la gestion des dépendances qui utilise le nom des modules.
Actuellement la pratique la plus répandue est que le nom du module est tout ou partie du nom d'un package qu'il contient. Généralement le nom du package utiliser le nom de domaine inversé. La raison est la même : garantir une certaine unicité dans le nom de chaque module.
Cependant, le fait d'utiliser les mêmes conventions de nommage pour le nom des modules et des packages peut prêter à confusion d'autant que le fichier de description d'un module contient des noms de modules et des noms de packages.
Chaque module doit avoir un nom qui doit être unique parmi ceux présents dans le modulepath d'une application. Généralement le nom du module choisi correspond à tout ou partie du nom d'un des packages contenu dans le module.
Le nom du module ne devrait pas inclure le groupId s'il est construit avec Maven car un module est plus abstrait que l'artefact qui le définit.
Il n'est pas recommandé de terminer le nom d'un module par un chiffre : dans ce cas, le compilateur émet un avertissement.
Résultat : |
C:\java\workspace\src>javac module-info.java
module-info.java:1: warning: [module] module name component java9 should avoid terminal digits
module fr.jmdoudoux.dej.java9 {
^
1 warning
|
Le descripteur de module contient des méta-données concernant le module. Un module peut dans son descripteur :
Le descripteur de module doit obligatoire être nommé module-info.java et être situé à la racine de l'arborescence des répertoires source du module. Pour des raisons de compatibilité, le nom du descripteur de module contient un tiret, ce qui n'est pas légal pour un identifiant Java. Ce choix avait déjà été utilisé pour les fichiers package-info.java pour les mêmes raisons.
C'est un fichier source Java qui sera compilé et inclus dans le module.
Le fichier module-info.java doit être compilé par le compilateur javac comme tout autre fichier source Java pour obtenir un fichier module-info.class.
Bien que cela soit un fichier .java, la syntaxe utilisée dans ce fichier n'est pas du code Java mais une syntaxe particulière permettant de décrire le module.
La syntaxe générale de la déclaration d'un module est de la forme :
{Annotation} [open] module Identifiant {.Identifiant} {
{ Directives }
}
Sa forme la plus basique contient le mot clé contextuel module suivi du nom du module suivi d'une paire d'accolades.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.monapp {
}
|
Cette forme basique définit simplement un module en lui indiquant un nom. Un descripteur de module doit obligatoirement définir le nom du module.
Les informations de description relatives au module sont fournies entre les accolades en utilisant des directives :
Directive |
Rôle |
requires |
Indiquer une dépendance du module. Ce mot clé doit être suivi du nom du module dépendant |
transitive |
S'utilise après le mot clé requires pour indiquer que tous les modules qui auront ce module en dépendances auront aussi implicitement le module précisé après transitive en dépendance |
exports |
Définir un package comme étant exposé en dehors du module ; Permet d'indiquer que les classes publiques du package précisé seront accessibles à l'extérieure du module |
opens |
Indiquer que les éléments du package précisé seront accessibles à l'exécution par l'API Introspection quelque soit leur niveau de visibilité et autorise le chargement de chargement de ressources contenues dans le package |
open |
Indiquer sur un module qu'il est possible de faire de l'introspection sur des éléments et de permettre un chargement des ressources de tous les packages de celui-ci |
uses |
Indiquer que le module utilise le service précisé sous la forme du nom pleinement qualifié de sa classe ou interface |
provides ... with ... |
Indiquer que le module fournit une implémentation du service précisé grâce à la classe dont le nom pleinement qualifié suit with |
La description d'un module utilise une syntaxe particulière avec ces mots clés contextels :
Syntaxe |
Rôle |
module nom.du.module |
Déclarer un module dont le nom est nom.du.module Il est important de choisir judicieusement le nom d'un module car c'est l'identifiant du module. Le système de module s'appuie sur le nom d'un module. Les noms en conflit ou qui changent causent des problèmes, il est donc important que le nom d'un module soit :
Obligatoire Syntaxe :
|
requires nom.du.module |
Définir que le module à besoin du module nom.du.module comme dépendance. Cela permet au module d'accéder à tous les types public des packages exportés par le module cible Les dépendances doivent être déclarées explicitement en utilisant la directive requires suivi du nom du module. Ces dépendances peuvent être des modules du JDK ou des dépendances tierces. Il existe deux exceptions :
Un des objectifs de JPMS est d'assurer une configuration fiable. La compilation et le lancement d'une application échouent si un module requis avec le bon nom n'est pas trouvé. Syntaxe :
|
requires transitive nom.du.module |
Chaque module qui dépend du module dépend automatiquement du module nom.du.module |
exports nom.du.package |
Exporter le package nommé nom.du.package : les types public définis dans le package seront utilisables par d'autres modules. Seules les classes public des packages exportés sont accessibles en dehors d'un module. Pour être accessible en dehors du module, une classe doit :
Si ces règles ne sont pas respectées alors une erreur survient aussi bien à la compilation qu'à l'exécution. La visibilité public change donc avec Java 9 lors de la mise en oeuvre des modules. Jusqu'à Java 8 inclus, une classe public est accessible par toutes les autres classes du classpath. A partir de Java 9, par défaut une classe publique n'est accessible que par les autres classes du module. Pour pouvoir être utilisée à l'extérieur du module, le package contenant la classe public doit être explicitement exporté. Les classes qui ne sont pas public d'un package exportés ne sont pas accessibles à l'extérieur du module. Syntaxe :
|
exports nom.du.package to nom.du.module |
Exporter le package nommé nom.du.package : les types public définit dans le package seront utilisable uniquement par le module nommé nom.du.module |
uses nom.du.type |
Définir le module comme étant un consommateur du service défini par le type nom.du.type |
provides nom.du.type with nom.du.type.impl |
Enregistrer la classe nom.du.type.impl comme étant le fournisseur d'une implémentation pour le service de type nom.du.type |
opens nom.du.package |
Autoriser les autres modules à utiliser la réflexion sur les types du package nom.du.package ou de charger des resources qu'il contient |
opens nom.du.package to nom.du.module |
Autoriser uniquement le module nommé nom.du.module à utiliser la réflexion sur les types du package nom.du.package ou de charger des resources qu'il contient |
Module, requires, exports, opens, ... ne sont pas ajoutés dans la liste des mots clés réservés du langage Java. Ils sont considérés comme des « mots clés contextuels (contextual keywords) » car ils ne sont utilisables en tant que mots clés que dans un descripteur de module, même si c'est un fichier Java.
Il est donc possible d'utiliser module, requires, exports, opens, ... comme identifiants dans du code source Java.
Les IDE qui proposent un support pour Java 9 propose une assistance plus ou moins élaborée pour faciliter la rédaction et la maintenance des fichiers module-info.java.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.util {
requires com.google.guava;
exports fr.jmdoudoux.dej.util;
}
|
L'exemple ci-dessus définit un module :
La directive exports permet de préciser le nom d'un package qui sera accessible par d'autres modules.
Par défaut, aucune classe n'est accessible en dehors du module, même si celles-ci sont déclarées avec le modificateur de visibilité public. Pour permettre à des classes publiques d'être accessibles en dehors du module, il faut explicitement exporter le ou les packages contenant ces classes.
La directive exports précise le nom d'un package du module à exporter : les modules qui auront le module en dépendance auront accès aux classes public et protected et à leur membre public et protected. Cela permet aussi un accès par introspection à ces types et membres public.
Pour exporter un package, il faut utiliser une syntaxe composée du mot clé contextuel exports suivi du nom du package suivi du caractère point-virgule.
La syntaxe générale est de la forme :
export nom_du_package to nom_module [, nom_module]* ;
La directive exports définit un package comme étant exposé en dehors du module. Tous les types public du package pourront être utilisés par un module qui dépend du module.
Le descripteur de module peut avoir aucune, une ou plusieurs directives exports.
Il n'est pas nécessaire d'exporter tous les packages. Par contre, pour utiliser un type public définit dans un package d'un module, ce package doit être exporté.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.monapp {
exports fr.jmdoudoux.dej.monapp.main;
}
|
La référence fournie après l'instruction exports ne peut être qu'un seul nom de package.
Les sous-packages d'un package exportés ne sont pas accessibles : il faut exporter explicitement tous les sous-packages concernés un par un.
Il est possible d'utiliser des exports qualifiés : dans ce cas, seuls le ou les modules précisés auront accès aux classes publiques du package. Cela permet de renforcer encore plus l'encapsulation.
Pour utiliser une exportation qualifiée, il faut utiliser le mot clé exports suivi du nom du package suivi du mot clé to suivi du module concerné suivi du caractère point-virgule. Plusieurs modules peuvent être précisés en les séparant par une virgule.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.monapp {
exports fr.jmdoudoux.dej.monapp.utils to fr.jmdoudoux.dej.monapp.service;
}
|
Dans l'exemple ci-dessus, seul le module fr.jmdoudoux.dej.monapp.service peut avoir accès aux classes publiques du package fr.jmdoudoux.dej.monapp.utils.
Un module a fréquemment besoin d'utiliser des classes d'autres modules. Dans ce cas, les modules requis deviennent des dépendances du module. Un module peut ainsi dépendre d'un ou plusieurs autres modules. Il est obligatoire de définir toutes ces dépendances explicitement dans le descripteur de module.
La gestion des dépendances dans le système de modules repose sur trois concepts :
Elle se concrétise explicitement dans le descripteur de module grâce à l'utilisation d'une directive requires suivi du nom du module concerné. Si un module A dépend d'un module B :
La dépendance d'un module vers un autre peut prendre deux formes :
En général, l'utilisation d'une dépendance transitive est recommandée, si un module exporte un package contenant un type dont la signature ou la valeur de retour fait référence à un package dans un second module, alors la déclaration du premier module doit inclure une dépendance publique requise sur le second. Cela garantit que les autres modules qui dépendent du premier module seront automatiquement capables de lire le second module et, par conséquent, d'accéder à tous les types public des packages exportés par ce module.
Avec JPMS, un module doit lire un autre module pour pouvoir utiliser son API en déclarant sa dépendance.
Une dépendance est exprimée par une directive requires suivi du nom d'un module, indépendamment du fait que ce module existe ou non. Elle permet au module d'avoir accès aux types public des packages exportés par le module précisé.
Le descripteur de module peut utiliser deux directives pour déclarer les dépendances d'un module :
Important : il n'est pas permis d'avoir des dépendances circulaires entre les modules. En d'autres termes, si le module A nécessite le module B, alors le module B ne peut pas également nécessiter le module A. Cette dépendance cyclique peut aussi inclure plus de deux modules. Dans tous les cas, le graphe de dépendance des modules doit être un graphe acyclique.
La définition d'une dépendance se fait en utilisant le mot clé requires suivi du nom du module qui est une dépendance suivie du caractères point-virgule.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.monapp {
requires fr.jmdoudoux.dej.monapp.utils;
}
|
La référence fournie après le mot clé requires ne peut être qu'un nom de module.
Dans l'exemple ci-dessus :
Le descripteur de module peut avoir aucune, une ou plusieurs directives requires. La directive requires est donc facultative : si un module n'a pas de directive requires alors il n'a pas de dépendances vers d'autres modules et il est donc un module indépendant.
Par défaut, le module java.base est automatiquement en dépendance. Donc si la déclaration d'un module ne définit pas explicitement une dépendance vers le module java.base, alors le module a une dépendance implicite vers le module java.base. Le module java.base n'a aucune dépendance.
La lisibilité n'est pas transitive par défaut. La lisibilité implicite implique que tout module qui lit le module lit également implicitement le module transitif. La lisibilité implicite a été introduite pour permettre qu'un module qui utilise les types d'un autre module dans sa propre API publique soit automatiquement utilisable sans que le module appelant doive déclarer explicitement la dépendance vers le module concerné.
Le mot-clé requires peut être suivi du mot clé transitive. Cela fait que tout module qui requiert le module courant a une dépendance implicite déclarée sur le module spécifié par la directive requires transitive.
Cela implique que si le module A requiert le module B, et que le module B requiert le module C de manière transitive, alors le module A requiert aussi implicitement le module C
Pour illustrer le concept, trois classes sont utilisées chacune encapsulées dans un module qui expose leur package.
Le moduleC contient la classe publique Info dont le package fr.jmdoudoux.dej.model est exposé.
Exemple ( code Java 9 ) : |
package fr.jmdoudoux.dej.model;
public class Info {
public String getVersion() {
return "2.0";
}
}
|
Exemple ( code Java 9 ) : |
module moduleC {
exports fr.jmdoudoux.dej.model;
}
|
Le moduleB contient la classe publique Information dont le package fr.jmdoudoux.dej.services est exposé. Comme une méthode renvoie une instance de type Info, il a une dépendance vers moduleC.
Exemple ( code Java 9 ) : |
package fr.jmdoudoux.dej.services;
import fr.jmdoudoux.dej.model.Info;
public class Information {
public Info get() {
return new Info();
}
}
|
Exemple ( code Java 9 ) : |
module moduleB {
exports fr.jmdoudoux.dej.services;
requires moduleC;
}
|
Le moduleA contient la classe publique Main qui utilise les classes Info et Information. Il a donc une dépendance explicite vers les modules moduleB et moduleC.
Exemple ( code Java 9 ) : |
package fr.jmdoudoux.dej;
import fr.jmdoudoux.dej.model.Info;
import fr.jmdoudoux.dej.services.Information;
public class Main {
public static void main(String[] args) {
Information information = new Information();
Info info = information.get();
System.out.println(info.getVersion());
}
}
|
Exemple ( code Java 9 ) : |
module moduleA {
requires moduleB;
requires moduleC;
}
|
Les dépendances telles que définies dans les descripteurs de module sont les suivantes :
Le moduleA définit explicitement une dépendance vers les modules moduleB et moduleC.
Il est possible d'utiliser la directive requires transitive pour permettre à tous les autres modules qui auront le module comme dépendance d'avoir une dépendance implicite vers la dépendance transitive.
Si un module exporte un package contenant un type dont la signature utilise un package d'un second module, la déclaration du premier module peut utiliser une dépendance transitive sur le second. Dans ce cas, cela garantira que les modules qui dépendent du premier module seront automatiquement capables de lire le second module et, par conséquent, d'accéder à tous les types des packages exportés par ce module.
L'instruction requires peut être suivi de l'instruction transitive qui permet de déclarer une dépendance implicite vers un autre module : tous les modules qui auront une dépendance vers le module auront accès aux packages exposés par le module requis de manière transitive. C'est par exemple utile si un type définit dans une dépendance transitive est utilisé dans une classe d'un package exposé par le module.
La syntaxe repose sur l'utilisation de la directive required transitive :
requires transitive <nom_module>;
Une dépendance implicite d'un module est déclarée avec la directive requires transitive. Tout module qui requiert (avec requires) un module qui contient une directive requires transitive requiert aussi implicitement le module déclaré dans la directive requires transitive. La directive requires transitive introduit une lisibilité implicite.
Dans le descripteur de moduleB, la dépendance vers le moduleC est déclarée transitive.
Exemple ( code Java 9 ) : |
module moduleB {
exports fr.jmdoudoux.dej.services;
requires transitive moduleC;
}
|
La directive requires transitive spécifie une dépendance sur un autre module et garantit que les autres modules qui lisent le module lisent également cette dépendance.
Le descripteur du moduleA ne déclare donc plus que la dépendance vers le moduleB.
Exemple ( code Java 9 ) : |
module moduleA {
requires moduleB;
}
|
Les dépendances telles définies dans les descripteurs de module sont les suivantes :
Idéalement la définition de dépendances transitives ne devrait être utilisée que si l'API publique du module en dépend. Par exemple, si un module contient une méthode qui est publiquement exposée dont la signature ou la valeur de retour utilise une classe de la dépendance transitive.
La lisibilité implicite induite par les dépendances transitives permet de mettre en oeuvre quelques patterns qui peuvent être utiles. Ils reposent sur le fait qu'avec elle, un client peut consommer les API de divers modules sans en dépendre explicitement, s'il dépend d'un module qui requiert de manière transitive les API utilisées.
La lisibilité implicite peut être utilisée pour mettre en oeuvre différents patterns :
L'agrégation de modules (aggregator module)
Les modules agrégateurs ont une responsabilité spécifique : regrouper les fonctionnalités de modules connexes en une seule unité.
Ce pattern est utilisé dans les modules du JDK lui-même :
Résultat : |
C:\java>java --describe-module java.se
java.se@9.0.1
requires java.sql.rowset transitive
requires java.sql transitive
requires java.datatransfer transitive
requires java.security.jgss transitive
requires java.logging transitive
requires java.desktop transitive
requires java.base mandated
requires java.xml.crypto transitive
requires java.management.rmi transitive
requires java.rmi transitive
requires java.security.sasl transitive
requires java.naming transitive
requires java.management transitive
requires java.instrument transitive
requires java.compiler transitive
requires java.xml transitive
requires java.scripting transitive
requires java.prefs transitive
|
L'utilisation de modules agrégateurs met les modules qui les ont en dépendances dans la situation où ils utilisent en interne des API de modules dont ils ne dépendent pas explicitement. Cela contredit la recommandation sur l'utilisation des dépendances transitives dans le cas où les types sont utilisés dans la signature ou la valeur de retour. |
Le fractionnement des modules (Splitting modules)
Un module peut être décomposé en modules plus spécialisés sans implications de compatibilité s'il devient un agrégateur pour les nouveaux modules.
Dans l'exemple ci-dessous, moduleA est découpé en trois modules. Pour ne pas impacter les modules qui ont moduleA en dépendance, les dépendances vers les modules moduleA1, moduleA2 et moduleA3 sont déclarées transitives.
Résultat : |
module moduleA {
requires transitive moduleA1;
requires transitive moduleA2;
requires transitive moduleA3;
}
|
La fusion de modules
Par exemple si moduleA, moduleB et moduleC sont fusionnés dans moduleD :
Résultat : |
module moduleA {
requires transitive moduleD;
}
|
Résultat : |
module moduleB {
requires transitive moduleD;
}
|
Résultat : |
module moduleC {
requires transitive moduleD;
}
|
Attention dans ce cas, les dépendances sont plus larges
Le renommage d'un module
La lisibilité implicite peut être utilisée pour renommer ou fournir un alias à un module.
Par exemple, si le moduleA est renommé en moduleB
Résultat : |
module moduleA {
requires transitive moduleB;
}
|
Si le moduleA nécessite le moduleB alors le système de modules :
Il en va exactement de même si moduleA dépend de moduleB de manière transitive : la moduleB doit être présent, peut être lue et accessible. En fait, pour moduleA et moduleB, le mot-clé transitif ne change rien. Par contre, pour les modules qui dépendent de moduleA, il obtienne une dépendance implicite vers moduleB.
Les dépendances transitives sont recommandées lorsqu'un module dont l'API publique accepte ou renvoie le type d'un autre module.
La gestion des dépendances par le système de module est par défaut très strict : toutes les dépendances doivent être déclarées explicitement et elles doivent être accessibles à la compilation et à l'exécution avec une vérification dans ces deux contextes à leur démarrage lors d'une étape de résolution.
La directive requires indique qu'un module est requis à la compilation et à l'exécution. Par conséquent à la résolution du module (traitement du descripteur de module et résolutions des dépendances), lorsque le système de modules rencontre une telle directive, il recherche dans l'ensemble des modules observables et émet une erreur s'il ne trouve pas le module. Une erreur est donc émise par le compilateur et la JVM si ce module n'est pas trouvé dans les modules du JDK ou dans le module-path lors de la résolution des modules.
C'est intéressant pour des dépendances qui sont requises mais il est parfois nécessaire d'avoir des dépendances optionnelles : parfois, certaines dépendances n'ont pas toujours besoin d'être présentes à l'exécution.
Pour rendre une dépendance optionnelle vers un module, le système de modules propose de faire suivre la directive requires par le modificateur static.
La directive requires static définit une dépendance vers un module de manière optionnelle : le module devra être trouvé par le compilateur mais il n'y aura pas d'obligation à ce que le module soit trouvé par la JVM lors de la résolution des modules. Ainsi une dépendance optionnelle est obligatoire dans la phase statique, lors de la compilation, mais elle est facultative dans la phase dynamique, lors de l'exécution.
Si un module A déclare dans son descripteur de module requires static B, alors le système de modules va se comporter différemment :
Ceci ne concerne que la déclaration de la dépendance. Indépendamment de la facilité à déclarer un module optionnel, l'utilisation dans le code de fonctionnalités de ce module est moins triviale. Les modules optionnels étant ignorés à la résolution lors de l'exécution, le code doit prendre en compte que les types exposés d'un module optionnel peuvent ne pas être présents à l'exécution.
Si une dépendance optionnelle est présente, le système de modules configure sa lisibilité et il est donc possible d'utiliser ses types exposés. Si elle est absente, alors une exception de type NoClassDefFoundError est levée lors de l'utilisation d'un de ses types exposés.
D'une manière générale, lorsque le code exécuté fait référence à un type, la JVM vérifie s'il est déjà chargé. Si ce n'est pas le cas, elle utilise un ClassLoader pour le faire et si cela échoue, alors une exception de type NoClassDefFoundError est levée.
Le code utilisant des types inclus dans un module optionnel doit donc prendre en compte de manière défensive la levée d'une exeption de type NoClassDefFoundError lors de la création d'une instance via l'opérateur new ou ClassNotFoundException lors du chargement via l'API Reflection (méthodes forName(), loadClass() et findSystemClass() de la classe Class).
L'API Reflection est fréquemment mise en oeuvre notamment par des frameworks open source : elle est largement utilisée par exemple par Spring, Hibernate, ...
Avec les modules, l'utilisation de l'API Reflection est restreinte. Sans les modules, il est possible d'utiliser cette API sur n'importe quel élément contenu dans le classpath incluant ceux de Java Core et ce quelle que soit la visibilité définie dans le code.
Dans les modules, par défaut, l'utilisation de l'API Reflection n'est possible que sur des éléments public de packages exportés. Pour les autres éléments, il faut obligatoirement autoriser l'accès aux packages sur lesquels on souhaite pouvoir faire des accès via l'API Reflection.
Il n'est plus possible d'utiliser l'API Reflection dans d'autres modules sans certaines contraintes :
Les accès via l'API Reflection sur des types ou des membres dans le module lui-même sont possibles sans contraintes quelques soit le niveau de visibilité.
L'ouverture d'un package permet de réaliser des accès via l'API Reflexion sur tous les types et sur tous leurs membres du package que celui-ci soit exporté ou non.
Il existe deux types de modules :
Le type de module détermine les accès aux types du module et aux membres de ces types pour le code situé en dehors du module. Un module standard (sans le modificateur open) permet l'accès à la compilation et à l'exécution uniquement aux types publics des packages exportés. Un module standard permet un accès par introspection :
Lors de la compilation, il n'est pas possible d'utiliser des types d'un package ouvert : seuls les accès via l'API Reflection à l'exécution sont permis même sur des types et des membres non publics.
Pour permettre l'introspection sur les types ou les membres non public d'un package par un autre module, il faut ouvrir le package qui les contient. Cela se fait avec l'instruction opens suivi du nom du package concernés.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.persistance {
exports fr.jmdoudoux.dej.entite;
opens fr.jmdoudoux.dej.entite;
}
|
La directive opens précise le nom d'un package du module à ouvrir : les modules qui auront le module en dépendance auront accès via l'API Reflection à tous les types et tous les membres du package.
Un package ouvert peut être qualifié, comme avec les exportations, uniquement vers certains modules.
Exemple ( code Java 9 ) : |
module fr.jmdoudoux.dej.persistance {
exports fr.jmdoudoux.dej.entite;
opens fr.jmdoudoux.dej.entite to org.hibernate.orm.core;
}
|
Il est aussi possible d'ouvrir tout un module en utilisant l'instruction open avant l'instruction module. Un module ouvert ouvre simplement tous ses packages.
Exemple ( code Java 9 ) : |
open module fr.jmdoudoux.dej.monapp {
}
|
Dans ce cas, le module devient un module ouvert : ce module permet l'utilisation de l'API Reflection sur tous les types et tous les membres de tous ces packages. Il n'est alors plus nécessaire d'ouvrir aucun package puisque dans ce cas, ils sont tous ouverts.
Un module ouvert (avec le modificateur open) permet l'accès à la compilation aux types public des packages exportés, mais l'autorise également, à l'exécution, aux types de tous ses packages via l'API Reflexion.
Les accès via l'API Reflection sur les types ou les membres dans un module ouvert sont possibles sans contraintes quelques soit le niveau de visibilité.
Plusieurs vérifications sont faites par le compilateur et peuvent émettre une erreur si elles échouent.
Le compilateur émet une erreur si plus d'une directive opens est utilisée sur le même package.
Résultat : |
C:\java\TestModules\src>javac module-info.java
module-info.java:3: error: duplicate or conflicting opens: fr.jmdoudoux.dej.java
opens fr.jmdoudoux.dej.java;
^
1 error
|
Le compilateur émet une erreur si une directive opens est utilisée sur un module déclaré avec la directive open.
Résultat : |
C:\java\TestModules\src>javac module-info.java
module-info.java:2: error: 'opens' only allowed in strong modules
opens fr.jmdoudoux.dej.java;
^
1 error
|
Le compilateur émet une erreur si un même module est précisé plusieurs fois à la suite d'une directive to.
Résultat : |
C:\java\TestModules\src>javac module-info.java
module-info.java:2: error: duplicate or conflicting opens to module: fr.jmdoudoux.dej.util
opens fr.jmdoudoux.dej.java to fr.jmdoudoux.dej.util, fr.jmdoudoux.dej.util;
^
1 error
|
Les ressources (fichiers qui ne sont pas des .class) sont aussi encapsulées par défaut dans un module. Ainsi pour permettre leur accès de l'extérieure du module, il est nécessaire d'ouvrir le ou les packages qui les contiennent ou d'ouvrir le module.
Pour accéder à une ressource dans le module lui-même, il est préférable d'utiliser les méthodes getResourceAsStream() des classes Class ou Module plutôt que celle de la classe ClassLoader.
Les méthodes de la classe ClassLoader ne peuvent accéder qu'à des ressources dont les packages qui les contiennent sont ouverts.
Les modules introduisent un nouveau niveau de visibilité : il n'est pas possible d'utiliser les types public à la compilation ou à l'exécution si leur package n'est pas exporté.
Le code contenu dans le module peut accéder aux types et à leurs membres en respectant les règle de visibilité de tous les packages du module, à la compilation et à l'exécution.
Lorsque de l'on souhaite utiliser un module présent dans le modulepath, la JVM va appliquer plusieurs règles pour permettre à une classe d'un package A d'un module A de pouvoir utiliser une classe d'un package B d'un module B :
Ces trois règles doivent être respectées pour permettre une utilisation de la classe par le module A.
Accès à la compilation |
Accès par introspection |
|
Par défaut |
Non |
Non |
Export |
Accès aux éléments public du package pour les modules qui l'auront en dépendances |
Accès aux éléments public du package pour les modules qui l'auront en dépendances |
Export qualifié |
Accès aux éléments public du package uniquement pour les modules précises qui l'auront en dépendances |
Accès aux éléments public du package uniquement pour les modules précisés qui l'auront en dépendances |
Open |
Non |
Accès à tous les éléments du package quelle que soit la visibilité |
Open qualifié |
Non |
Accès à tous les éléments du package quelle que soit la visibilité pour les modules précisés |
Opens |
Non |
Accès à tous les éléments de tous les packages quelle que soit la visibilité |
Le descripteur de module est un élément important d'un module : il est donc important de garantir la qualité de son contenu comme pour tout autre fichier qui compose le code source. Comme il définit comment le module va interagir avec les autres modules, il peut être amener à évoluer.
Il n'y a pas d'ordre imposé dans l'utilisation des directives mais il est intéressant d'en respecter un afin d'en faciliter la lecture et la maintenance comme dans les classes. Par exemple, le JDK utilise l'ordre suivant :
Les opinions sur la documentation du code, comme la Javadoc ou les commentaires en ligne, varient énormément, mais quelle que soit la position sur les commentaires, il faut les utiliser dans les descripteurs de modules. Il est toujours intéressant de documenter les raisons d'une décision spécifique. Dans une déclaration de module, cela pourrait ajouter des commentaires dans différentes situations, par exemple :
Les descripteurs de module sont du code source et en tant que tel ils doivent être pris en compte lors des revues de codes. Durant ces revues, il est intéressant de vérifier différentes choses, notamment :
|