Introduction aux modules

images/bluebelt.png

L’organisation en classes et en packages permet de bien organiser les "cercles de responsabilité" d’un code (qui est responsable de quoi) mais ne suffit pas pour mettre en place une modularité sophistiquée.

A l’intérieur d’un projet on peut répartir le travail et les responsabilités de chaque équipe mais lorsque l’ensemble de packages est mis à la disposition d’une organisation "extérieure" tous les aspects public de ces packages se trouvent exposés.

On devrait pouvoir limiter les points d’entrée réservés à ces codes extérieurs. Par exemple: avoir des accès "public" réservés uniquement à d’autres packages de la même organisation (sans être "publics" pour tout le monde!)

Il peut y avoir plusieurs raisons de faire ça: la plus immédiate est de préserver les possibilités d'évolution des codes vis-à-vis de codes "extérieurs". Normalement un code public ne devrait pas pouvoir changer sa signature une fois qu’il est mis à la disposition de code "clients "extérieurs; toute modification impacterait ces codes … mais si ces codes "clients" n’ont justement pas accès à des services qui sont considérés comme "interne" à la logithèque alors on a complété un niveau supplémentaire de modularité (et on s’est réservé le droit de faire évoluer les A.P.I.s internes au projet).

Souvent (mais pas nécessairement) la mise en place de blocs de logithèques va correspondre à une frontière entre organisations. Un fournisseur de codes va offrir différents modules et une autre organisation va utiliser certains de ces super-composants en se limitant à quelques points d’entrée. (Il y a en fait d’autres cas de figure - comme par exemple le fait que les logithèques java ont été dispersées en différents modules pour éviter d’avoir à charger à l’exécution l’ensemble de ces codes -. Ici, nous essayerons d’introduire le plus simplement possible cette notion de module au sens java).

[Avertissement]

L’organisation modulaire apporte certes des avantages mais aussi des inconvénients en terme de complexité! Donc son adoption (du moins au niveau de java 9) devrait être soigneusement planifiée: il faut connaître les aspects de la technologie mais ne l’adopter que quand le projet est mûr pour en tirer des avantages (et peut-être attendre des versions de java ultérieures avec des outils qui simplifieraient les opérations de création d’applications autonomes).

L’organisation et les répertoires en pratique

Supposons que nous écrivions un code de nom canonique com.maboite.monapp.ihm.Fenetre

Nous allons créer un module de nom com.maboite.monapp (ce n’est pas illogique: dans le cadre de l’organisation com.maboite on aura un ensemble de codes regroupés dans le cadre de monapp. Maintenant ce n’est pas non plus strictement obligatoire si ce module ne constitue pas un point d’entrée pour d’autres codes - si ce module reste "interne" à l’organisation - ).

Les répertoires sources seront alors organisés de la manière suivante à partir du répertoire de référence des sources (que nous appellerons projet).

projet
  |_ com.maboite.monapp -> module-info.java
      |_ com
           |_ maboite
                |_ monapp
                     |_ ihm ->  Fenetre.java

Fenetre.java sera un code graphique (ihm= Interactions Homme/machine!). Ce code va faire appel à des classes du package standard java.awt.

Ce package ne se trouve pas dans le module standard java.base qui est automatiquement accessible mais dans le module standard java.desktop

Le répertoire com.maboite.monapp on va trouver un fichier source spécial de nom module-info.java et qui contient la description du module.

module com.maboite.monapp {
   requires java.desktop;
}

Ici:

  • On déclare le nom du module (il doit être le même que le répertoire qui l’abrite)
  • On demande l’accès au module java.desktop

En fait la directive requires implique que :

  • La présence effective des codes du module demandé sera garantie au démarrage (reliable configuration)
  • Il est garanti que le ClassLoader du module demandeur pourra "lire" les codes exportés par ce module (readability). (mais il peut aussi lire des codes qui ne sont pas explicitement importables)
  • Les codes du module demandeur pourront "accéder explicitement" aux types exportés par le module cible (accessibility : toutefois le terme est un peu trompeur puisque si un package est hors de portée on peut quand même accéder à des objets dont le type n’est pas en portée. Comme nous le répéterons sans cesse, le mot "accès" doit se comprendre en termes de partage des responsabilités: on peut accéder à des codes sans pouvoir "connaître" explicitement leur type effectif … à condition que notre ClassLoader permette d’accéder à son code. (point que nous allons détailler un peu plus loin).

Le module-info de java.desktop est assez compliqué mais en voici un extrait:

module java.desktop {
    // il a lui même besoin d'un autre module
    requires java.prefs;
    // donne accès au module datatransfer qu'il utilise
    requires transitive java.datatransfer;
    // ... autres lignes
    // ici les déclaration d'exportation
    // ces packages deviennent visibles de l'extérieur
    exports java.applet;
    exports java.awt;
    // .... etc. etc...

Le code de Fenetre.java au sein de notre module pourra alors avoir accès aux A.P.I des packages visibles dans le module desktop (plus, éventuellement, à des classes venant de datatransfer qui lui auront été passés par des codes de desktop!)

[Note]

L’utilisation de requires transitive est la suivante dans l’exemple:

  • Tout module qui fait requires java.desktop fait implicitement requires java.datatransfer
  • Le module qui déclare la transitivité le fait uniquement parce que son API publique produit des données d’un type présent dans le module à re-exporter. Ce qui veut dire ici que tout code qui utilise java.desktop peut avoir à manipuler explicitement des données visibles dans le module java.datatransfer. (voir l’exercice associé aux modules dans le chapitre suivant pour bien saisir la portée du terme "explicitement").

Maintenant les compilations!

Il est de bonne guerre de ne pas mélanger les fichiers source et les fichiers binaires. On peut , par exemple, créer un répertoire quelque part pour abriter les binaires (bien noter que dans les cas des IDE ce répertoire est créé pour vous avec un nom propre à l’outil - par ex. out - ).

On va donc créer un répertoire modbin et un sous-répertoire pour le module:

modbin
  |_ com.maboite.monapp

Maintenant une version (simplifiée) de la commande de compilation:

javac -d répertoire_modbin_com.maboite.monapp module-info.java

javac -d répertoire_modbin_com.maboite.monapp Fenetre.java

Si tout se passe bien cela doit générer un arborescence de fichiers comme celle-ci:

modbin
  |_ com.maboite.monapp -> module-info.class
      |_ com
           |_ maboite
                |_ monapp
                     |_ ihm ->  Fenetre.class

Faisons un premier essai d’utilisation du code de Fenetre depuis JShell:

  • En se positionnant dans le répertoire modbin (pour simplifier l’exposé):

    jshell --module-path .

    Exécution:

    jshell> new com.maboite.monapp.ihm.Fenetre("Hello") ;
    |  Error:
    |  package com.maboite.monapp.ihm is not visible
    |    (package com.maboite.monapp.ihm is declared in module com.maboite.monapp, which is not in the module graph)
    |  new com.maboite.monapp.ihm.Fenetre("Hello") ;
    |      ^--------------------^

Ici rien que de très normal du point de vue des principes des modules java: nous sommes "à l’extérieur" du module com.maboite.monapp et le package ihm n’est pas "exporté".

Essayons d’une autre manière de façon à permettre à Jshell de tester notre code (toujours dans le répertoire bin):

jshell --class-path com.maboite.monapp
new com.maboite.monapp.ihm.Fenetre("Hello") ;
$1 ==> com.maboite.monapp.ihm.Fenetre@23fe1d71

Ici l’appel a fonctionné!

Pour lancer java nous avons besoin d’une classe contenant un main. Si nous créons une classe Main (avec la méthode public static void main(string[] args)…) dans un package com.maboite.monapp.mains alors nous pourrons invoquer java (ici depuis le répertoire bin)

java -p . -m com.maboite.monapp/com.maboite.monapp.mains.Main

Ici:

  • l’option -p (ou --module-path) indique l’emplacement des modules
  • l’option -m permet d’indiquer le module hôte de la classe principale. Ici nous avons spécifié explicitement quelle était cette classe principale mais nous verrons ultérieurement qu’il y a un autre moyen de faire connaître cette information à l’exécution.

Comme pour jshell on aurait pu simplifier une invocation de test en se rendant dans le répertoire binaire com.maboite.monapp et en lançant:

java com.maboite.monapp.mains.Main

Si tout ceci vous semble un peu compliqué retenez les points suivants:

  • En fait les modules ne sont pas vraiment faits pour être livrés sous forme de répertoires contenant des fichiers. Le déploiement normal se fait avec des archives jar que nous étudierons plus loin. Chaque archive contient un module: à peu de choses près tout ce qui se trouve dans le répertoire des binaires auquel on a donné le nom d’un module. Il contiendra donc module-info.class, les arborescences binaires de packages et d’autre éléments complémentaires.
  • Au début de l’apprentissage on peut se passer des modules. On aura alors implicitement un "module par défaut". Cette organisation simplifiée est expliquée ci-après (de plus c’est celle des codes java antérieurs à la version 9!).
  • Vous avez eu peut-être l’impression qu’il y avait moyen de contourner la sécurité d’accès aux codes (par exemple lorsqu’on lançait jshell avec directement l’option class-path). En fait le terme "sécurité" doit être replacé dans un contexte analogue à que ce que l’on fait en marquant des membres private: ce que l’on gère c’est des cercles de responsabilité vis à vis des codes. On définit des frontières qu’il n’est pas recommandé de franchir! … Ce ne sont pas des barrages avec des droits d’accès, ce sont des points qui marquent des limites de responsabilité: celui qui viole ces limites le fait en pleine connaissance et à ses risques et périls (mais par exemple un test par le développeur lui même ne devrait pas prêter à conséquence). Pour bien comprendre cette notion de responsabilité associée aux modules voir les exercices sur les modules qui accompagnent la leçon suivante.
[Attention]Attention

En général les outils interactifs d’aide à la programmation utilisent une structure analogue à celle décrite ici. Prendre toutefois garde au fait que les binaires que vous voulez livrer ne sont pas nécessairement ceux que vous utilisez pour votre développement au jour le jour (et qui sont compilés avec toutes les options de debug).

Il peut s’avérer nécessaire de différencier la génération des codes en cours de développement de la génération des codes à livrer. (voir à ce propos les options de jlink utilitaire abordé un peu plus loin)

Les chemins d’accès aux classes

Le compilateur java ne fait pas que vérifier si la syntaxe du code est correcte. Il vérifie aussi si l’utilisation des autres codes est conforme aux A.P.Is.

Pour cela le compilateur doit savoir trouver les binaires correspondants (on peut également compiler plusieurs sources interdépendantes en même temps).

Il faut donc indiquer au compilateur où trouver les binaires.

Ceci se fait par l’option --module-path suivi d’une liste de répertoires dans lesquels se trouvent les codes de modules recherchés (donc le répertoire modbin dans notre exemple. S’il y avait plusieurs répertoires de ce genre on les met dans un liste de noms selon la syntaxe de path propre à votre plateforme: rep1:rep2:rep3 sur les systèmes de type UNIX; rep1;rep2;rep3 sur les sytèmes de type WIN.

ATTENTION: pour un code dans un module qui fait appel à des codes d’autres modules il faut ben s’assurer que ces autres modules sont bien présents dans la clause requires du fichier module-info local!

Par exemple:

javac --module-path repertoire_modbin -d repertoire_modbin/com.maboite.uneautreapp module-info.java

(ici si un requires de com.maboite.uneautreapp spécifie un module on doit le trouver dans bin)

javac --module-path repertoire_modbin -d repertoire_modbin/com.maboite.uneautreapp AutreClasse.java

Il existe de nombreuses autres façons d’utiliser les options de compilation mais nous n’allons pas les détailler dans ce document.

Organisation simplifiée , module par défaut

images/whitebelt.png

Il est possible de ne pas utiliser de modules et donc il est prévu de gérer ce type de code. En fait c’est surtout fait pour des codes antérieurs à la version 9.

De tels codes sont censés appartenir à un module sans nom (unnamed module). Mais il existe aussi des codes non-modulaires qui seront reconnus comme appartenant à un "module automatique" (sur lesquels on peut faire explicitement des requires par ex.) nous verrons cet aspect avec la leçon sur les jars.

Ces codes correspondent à une organisation des sources comme:

projet
  |_ com
      |_ acme
           |_ ventes
                |_ ShowBiz.java

et pour les binaires déployés dans le système de fichier:

bin
  |_ com
      |_ acme
           |_ ventes
                |_ ShowBiz.class

Pour la J.V.M. chargée d’exécuter ce code le CLASSPATH devra comprendre ce répertoire bin. Le ClassLoader qui va rechercher la classe com.acme.ventes.ShowBiz va explorer de manière appropriée la hiérarchie des répertoires et trouver le fichier binaire ShowBiz.class.

Note importante: l’appel à javac devra aussi avoir le répertoire classes dans son CLASSPATH (cela permettra de résoudre les références à d’autres codes comme com.acme.produits.trucs.CycloRameur).

javac --class-path repertoire_bin -d repertoire_bin ShowBiz.java

[Note]

Point de terminologie: on dit que le répertoire bin est le codesource de com.acme.ventes.ShowBiz.

[Note]

L’option --class-path peut référencer plusieurs répertoires :

-cp unixdir1:unixdir2:unixdir3
-cp windir1;windir2;windir3

(En fait la liste peut aussi contenir des archives java -archives jar-)

images/orangebelt.png

Que faire si on mélange des codes avec modules et des codes sans modules?:

  • Bien que ça ne soit pas nécessaire on pourrait différencier les répertoires binaires des code modulaires des autres. (précédemment nous avons marqué modbin pour les binaires modulaires et bin pour les codes non modulaires nous verrons que par contre cette séparation des répertoires sera utile quand on déploiera des archives jar).
  • Depuis un code de modules on peut utiliser l’option --class-path pour trouver les codes sans modules. De plus les packages des codes du module sans nom sont automatiquement acceptables (pas de requires nécessaire).

    javac --module-path repertoire_modbin --class-path repertoire_bin -d repertoire_modbin_module MaClasse.java

    Le module-path est pour les modules; le class-path est pour les packages dans le module par défaut.

  • Utiliser des codes de module depuis un module sans nom (par ex. pour des tests) est plus problématique.

    javac --module-path repertoire_modbin --add-modules ALL-MODULE-PATH -d repertoire_bintest ClassTest.java

Rappel: les déploiements des librairies de modules et de packages se font en utilisant des archives JAR que nous verrons ultérieurement.

--Exercices sur les modules--

Pour des raisons techniques les exercices sur les modules seront reportés à la leçon suivante sur "composition, association, héritage". Les points abordés dans cette leçon permettront de mieux comprendre le fonctionnement d’ensemble des relations entre codes.