Créer un module Java 9 (projet JIGSAW)

Publié le 1 septembre 2020 par Thierry LEROY

Préambule

Dans cet article, nous allons créer deux modules, les compiler et les faire interagir.

Le premier module contiendra une interface “ISayHello” avec une seule méthode : “sayHello” et une implémentation de cette interface : “SayHelloEnglish”. Le deuxième module va contenir une autre implémentation de l’interface “ISayHello” : “SayHelloFrench”.

Les sources utilisées dans cet article sont disponibles sur GitHub : creer-un-module-java-9.git. Vous pouvez en récupérer une archive ici : creer-un-module-java-9.zip.

Prérequis

Le JDK Java 9 (ou une version ultérieure) est installé sur votre ordinateur. Si ça n’est pas le cas, vous pouvez lire l’article “Installer Java”. Aucun IDE n’est nécessaire, nous n’utiliserons que les commandes : “javac”, “java”, “jar” et “javadoc”.

Création du premier module

Les noms de module respectent les mêmes conventions que les noms de package, le module s’appellera “com.thierryleroy.modulejava.helloworld”. Nous allons créer un répertoire du même nom que le module, ça n’est pas obligatoire, mais ça simplifiera les commandes “javac” et “javadoc”. Nous allons y créer trois fichiers :

com.thierryleroy.modulejava.helloworld/com/thierryleroy/modulejava/helloworld/ISayHello.java
com.thierryleroy.modulejava.helloworld/com/thierryleroy/modulejava/helloworld/priv/SayHelloEnglish.java
com.thierryleroy.modulejava.helloworld/module-info.java                 

“ISayHello.java” :

Une interface qui contient une seule méthode “sayHello()”, qui doit dire bonjour :

package com.thierryleroy.modulejava.helloworld; public interface ISayHello { void sayHello(); }

“SayHelloEnglish.java” :

Une implémentation de l’interface “ISayHello” en anglais. Elle se trouve dans un sous-package “priv”, nous verrons la raison un peu plus bas.

package com.thierryleroy.modulejava.helloworld.priv; import com.thierryleroy.modulejava.helloworld.ISayHello; public class SayHelloEnglish implements ISayHello { @Override public void sayHello() { System.out.println("Hello!"); } }

“module-info.java” :

Le fichier sert à définir le module. Une version minimaliste pour notre module pourrait être :

module com.thierryleroy.modulejava.helloworld { }

Mais notre module doit partager son interface “ISayHello” avec les autres modules. Pour la rendre accessible, nous devons exporter son package grâce au mot clef “exports” :

module com.thierryleroy.modulejava.helloworld { exports com.thierryleroy.modulejava.helloworld; }

Maintenant, toutes les interfaces et classes publiques du package “com.thierryleroy.modulejava.helloworld” sont accessibles aux autres modules. Mais pas celles de ses sous-packages, l’export n’est pas récursif. La classe “SayHelloEnglish” reste inaccessible de l’extérieur, c’est la raison du sous-package “priv”.

Compilation du module

Les options “–module-source-path” et “–module”, permettent de compiler le module en entier, sans préciser la liste des fichiers “.java”. Pour fonctionner, il faut que le répertoire des sources du module porte le même nom que le module. Si l’on se met dans le répertoire racine des sources des modules, il faut lancer “javac” ainsi :

javac -d classes \ --module-source-path ./ \ --module-version 0.1.0 \ --module com.thierryleroy.modulejava.helloworld
    -d : l'emplacement cible des fichiers ".class" générés. Par défaut, ils sont créés au même niveau que les  fichiers ".java".
    --module-version : la version du module
    --module-source-path : la racine des sources des modules
    --module : le nom du module à compiler          

Vérification

L’option “–list-modules” de la commande “java” permet de lister les modules disponibles :

java --module-path classes/ --list-modules
    --module-path : la racine des classes des modules

Vous devriez obtenir un affichage similaire à celui-ci :

… jdk.xml.dom@11.0.7 jdk.zipfs@11.0.7 com.thierryleroy.modulejava.helloworld@0.1.0 classes/

Le module “com.thierryleroy.modulejava.helloworld” en version “0.1.0” (@0.1.0) est bien présent dans le “module-path”

Création du second module

Même principe que pour le premier module, le module s’appellera “com.thierryleroy.modulejava.helloworld.french”. Nous allons créer deux fichiers :

com.thierryleroy.modulejava.helloworld.french/module-info.java
com.thierryleroy.modulejava.helloworld.french/com/thierryleroy/modulejava/helloworld/french/priv/SayHelloFrench.java

“module-info.java”

Le second module va implémenter l’interface “ISayHello”, nous devons préciser que le module nécessite le premier module, grâce au mot clef “requires” :

module com.thierryleroy.modulejava.helloworld.french { requires com.thierryleroy.modulejava.helloworld; }

“SayHelloFrench.java”

Une implémentation de l’interface “ISayHello” en français qui affichera “Bonjour !”. Elle aussi se trouve dans un sous-package “priv”. Ici, la présence de ce sous-package n’est pas vraiment nécessaire, car le module n’exporte aucun package, mais c’est plus cohérent de le garder.

Compilation du module

Quasiment similaire au premier module :

javac -d classes \ --module-path classes \ --module-source-path sources/ \ --module-version 0.1.0 \ --module com.thierryleroy.modulejava.helloworld.french

Test

Le test va consister à parcourir les implémentations de “ISayHello” disponibles via le “ServiceLoader” et appeler la méthode “sayHello” pour chacune d’entre elles. Pour cela, nous allons créer un fichier “Main.java” dans “helloworld/src/com/thierryleroy/modulejava/helloworld/”.

“Main.java”

package com.thierryleroy.modulejava.helloworld; import java.util.ServiceLoader; public class Main { public static void main(String[] args) { final ServiceLoader serviceLoader = ServiceLoader.load(ISayHello.class); for (final ISayHello sayHello : serviceLoader) { sayHello.sayHello(); } } }

Le “ServiceLoader” avec Java 9

Avant Java 9, pour fournir une implémentation au “ServiceLoader”, il fallait créer un fichier “com.thierryleroy.modulejava.helloworld.ISayHello” dans “META-INF/services/”

Avec Java 9, il faut le déclarer dans le fichier «module-info.java» par le mot clef : “provides”. Et pour pouvoir utiliser le «ServiceLoader», il faut également le préciser dans «module-info.java» avec le mot clef : «uses».

Le premier module utilise le service «ISayHello» et fournit l’implémentation «SayHelloEnglish» :

import com.thierryleroy.modulejava.helloworld.ISayHello; import com.thierryleroy.modulejava.helloworld.priv.SayHelloEnglish; module com.thierryleroy.modulejava.helloworld { exports com.thierryleroy.modulejava.helloworld; uses ISayHello; provides ISayHello with SayHelloEnglish; }

Le second module n’utilise pas de Service, mais fournit l’implémentation «SayHelloFrench» :

import com.thierryleroy.modulejava.helloworld.french.priv.SayHelloFrench; import com.thierryleroy.modulejava.helloworld.ISayHello; module com.thierryleroy.modulejava.helloword.french { requires com.thierryleroy.modulejava.helloworld; provides ISayHello with SayHelloFrench; }

Compilation et exécution

Nous devons recompiler les deux modules.

Voilà, nous pouvons enfin lancer le test 🙂 !

java --module-path "classes" \ --module com.thierryleroy.modulejava.helloworld/com.thierryleroy.modulejava.helloworld.Main
    --module-path : pour indiquer où chercher les classes des modules
    --module : la classe contenant la méthode "main" à exécuter

Vous devriez obtenir l’affichage suivant :

Hello! Bonjour !

Création de la JavaDoc

javadoc -d javadoc -private \ --module-source-path sources/ \ --module com.thierryleroy.modulejava.helloworld,com.thierryleroy.modulejava.helloworld.french

Vous pouvez voir le résultat ici : javadoc.