IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

34. Les modules

 

chapitre    3 4

 

Niveau : niveau 2 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 :

  • il doit avoir un nom unique dans l'espace global défini par la JVM. Comme pour les noms de packages, le nom utilise généralement par convention le nom de domaine inversé mais ce n'est pas une obligation
  • il doit avoir un unique descripteur de module sous la forme d'un fichier source module-info.java compilé en module-info.class. Il doit être situé à la racine de la structure de répertoires du code source du module
  • un module peut dépendre d'autres modules. Ces dépendances sont définies dans le descripteur de module

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 :

  • module observable : module fournit par la plateforme d'exécution ou dans le module path
  • module résolu : module observable qui a été ajouté au graphe des modules pendant la résolution des modules

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 :

  • le nom du module
  • les packages exportés : une liste de packages dont les éléments publics seront accessibles à l'extérieur du module par d'autres modules qui en dépendent
  • les dépendances du module : les modules requis par le module donc les autres modules dont le module à besoin
  • les packages ouverts : une liste de packages sur lesquels l'introspection pourra être utilisée
  • les services proposés : une ou plusieurs implémentations des services que le module fournit et qui pourront être utilisé par d'autres modules
  • les services fournis ou consommés par le module

Un module possède obligatoirement un nom, soit implicite ou explicite.

Par défaut, un module est hermétique :

  • aucune classe n'est accessible de l'extérieur du module, même celles qui sont public
  • il n'est pas possible d'utiliser l'introspection sur les classes du module de l'extérieur du module
  • aucune ressource n'est accessible de l'extérieur du module

Ce chapitre contient plusieurs sections :

 

34.1. Le contenu d'un module

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 :

  • une classe regroupe des champs et des méthodes
  • un package regroupe des types (classes, interfaces, enums, records) et éventuellement des ressources
  • un module regroupe des packages

Un module est donc un artefact qui contient :

  • un ou plusieurs packages contenant des types compilés en fichier .class
  • éventuellement des ressources
  • des métadonnées dans un descripteur de module (module-info.class)

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.

 

34.2. Le code source d'un module

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

 

34.3. Le descripteur de module

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 :

  • quel est le nom du module ?
  • quelles sont les dépendances requises ? Par défaut, il y a toujours une dépendance implicite vers le module java.base
  • quels sont les packages exportés ? L'accès à une classe public ne pourra se faire en dehors du module que si son package est exporté. Par défaut, aucun package n'est exporté

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 :

  • le nom du module
  • les modules dont il dépend : les modules requis par ce module
  • les packages qu'il expose pour permettre l'utilisation de leur classes plublic par d'autres modules
  • les packages sur lesquels l'introspection est autorisé dans des classes d'autres modules
  • les services qu'il expose et/ou qu'il consomme

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>]*
}

 

34.3.1. Le nommage d'un module

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 :

  • des noms suffisamment longs pour être descriptif
  • suffisamment court pour être mémorisé
  • et unique pour éviter les conflits

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 :

  • utiliser un nom de domaine inverse. Cette solution est historiquement utilisée notamment pour le nommage des packages qui peuvent aussi requérir une unicité
  • utiliser un nom de module qui commence par le nom du projet concerné

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

 

34.3.2. Le descripteur de module : le fichier module-info.java

Le descripteur de module contient des méta-données concernant le module. Un module peut dans son descripteur :

  • déclarer ses dépendances : cela permet de définir les types utilisables dans le code du module en plus des types définis dans le module lui-même
  • exporter les packages pour lesquels le module va permettre l'accès aux classes publiques au module qui vont en dépendre
  • ouvrir les packages dans les lesquels les autres modules pourront faire de l'introspection sur des éléments non public
  • déclarer les services fournis ou consommés par le module

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 :

  • globalement unique dans le module-path
  • stable

Obligatoire

Syntaxe :

module nom_du_module {
  // ...
  }

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 :

  • le module de base nommé java.base qui est implicitement requis car il contient des types requis par tout code Java
  • les dépendances déclarées transitives dans la description d'une dépendance

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 nom_du_module;

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 :

  • être public
  • son package doit être exporté en utilisant le mot clé exports
  • le module qui en a besoin doit déclarer le module comme une dépendance en utilisant le mot clé requires

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;

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 :

  • dont le nom est fr.jmdoudoux.dej.util
  • qui possède une dépendance explicite vers le module com.google.guava et implicite vers java.base
  • qui expose les classes du package fr.jmdoudoux.dej.util. Les autres packages contenus dans le module ne seront accessibles que par le module lui-même

 

34.3.3. L'export des packages

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.

 

34.3.4. La déclaration des dépendances

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 :

  • fiabilité de la configuration (reliable configuration) :
  • lisibilité (readibility)
  • accessibilité (accessiblity)

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 fiabilité de la configuration est assurée par la déclaration de la dépendance vers le module B dans le descripteur de module A et par la vérification de la présence du module A et B dans le module path
  • la lisibilité est assurée par la directive requires B qui permet au module A de lire le module B
  • l'accessibilité est assurée par la directive requires B qui permet au module A d'accéder aux classes public des packages exportés du module B

La dépendance d'un module vers un autre peut prendre deux formes :

  • le concept de lisibilité (Readability) : le module dépendant dépend d'un autre mais cette relation est invisible pour les autres modules. Lorsqu'un module dépend directement d'un autre, le code du premier module pourra utiliser les types public des packages exportés du second module. On dit que le premier module lit (read) le second ou, de manière équivalente, que le second module est lisible par le premier. C'est le cas le plus courant qui induit une déclaration de la dépendance de manière explicite
  • le concept de lisibilité implicite (Implied Readability) : le code qui veut appeler le module dépendant peut être amené à utiliser des types de son module dépendant. Mais il ne peut pas le faire s'il ne lit pas également le second module. Par conséquent, pour que le module dépendant soit utilisable, les modules clients devraient tous dépendre explicitement de ce second module. Identifier et résoudre manuellement ces dépendances dîtes transitives serait une tâche fastidieuse et source d'erreurs. La déclaration de modules permet qu'un module puisse accorder la lisibilité à d'autres modules, dont il dépend, à tout module qui en dépend. Le cas d'usage correspond à un module qui dépend d'un autre et expose les types du module dépendant dans sa propre API publique.

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 :

  • requires : déclare une lisibilité vers un module. La directive requires spécifie le nom d'un module vers lequel le module a une dépendance. Exemple : le module A dépend d'un autre module B
  • requires transitive : déclare une lisibilité implicite, la dépendance vers un troisième module est propagée, permettant au premier de lire le troisième sans en dépendre explicitement. Exemple : le module A dépend de B, le module B dépend de manière transitive au module C, alors le module A dépend aussi implicitement de C

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.

 

34.3.4.1. Les dépendances explicites

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 module déclare une dépendance à la compilation et à l'exécution vers le module nommé
    fr.jmdoudoux.dej.monapp.utils
  • le module a ainsi accès aux classes public des packages exportés par le module nommé
    fr.jmdoudoux.dej.monapp.utils

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.

 

34.3.4.2. Les dépendances transitives

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.

 

34.3.4.3. Les patterns utilisables avec la lisibilité implicite

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 :

  • agrégation de modules (aggregator module)
  • fractionnement des modules (splitting modules)
  • fusion de modules
  • renommage d'un module

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
attention 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;
}

 

34.3.4.4. Récapitulatif de la lisibilité

Si le moduleA nécessite le moduleB alors le système de modules :

  • impose la présence de moduleB dans le modulepath : configuration fiable (reliable configuration)
  • permet à moduleA de lire moduleB : lisibilité (readibility)
  • permet au code de moduleA d'accéder aux classes publiques des packages exportés de moduleB : accessibilité (accessiblity)

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.

 

34.3.5. Les dépendances optionnelles

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 :

  • à la compilation : module B doit être présent dans le module path sinon une erreur est retournée par le compilateur
  • à l'exécution : module B n'est pas l'obligation d'être présent. S'il est présent, module A pourra lire le module B

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).

 

34.3.6. L'utilisation de l'API Reflection et l'accès aux ressources

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 :

  • Sur des types ou des membres publics : le package du type concerné doit être exporté
  • Sur des types ou des membres non publics : le package doit être ouvert (ou le module lui-même peut être ouvert)

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 :

  • module standard (normal module) : par défaut, l'accès par introspection aux éléments qu'il contient n'est pas possible
  • module ouvert (open module) : permet un accès par introspection à tous les packages qu'il contient

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 :

  • aux éléments publics des packages exportés
  • à tous les éléments des packages ouverts

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.

 

34.4. Les règles d'accès

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 :

  • la classe dans le package B est public
  • le module B contenant le package B l'exporte dans sa déclaration
  • Le module A déclare une dépendance vers le 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é

 

34.5. La qualité des descripteurs de module

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 :

  • requires (avec static et transitive)
  • exports
  • exports to
  • opens
  • opens to
  • uses
  • provides

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 :

  • une exportation qualifiée pour expliquer pourquoi elle n'est pas une API publique, mais est partiellement accessible
  • un paquet ouvert expliquant les frameworks qui doivent y avoir accès
  • une dépendance facultative pour expliquer les raisons de l'absence du module
  • ...

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 :

  • vérifier l'export d'un package et s'assurer que les API exposées se limitent bien uniquement à celles requises
  • vérifier l'utilité des exports qualifiés
  • vérifier les dépendances
  • vérifier que le code prend en compte l'absence des modules optionnels
  • ...

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.