Les modules

Depuis sa version 9, le langage Java supporte la notion de module. Un module est un ensemble de classes et de packages qui forment un tout complet. Il peut se présenter sous la forme d’un répertoire ou d’un fichier JAR. Un projet utilise généralement plusieurs modules.

Il ne faut pas confondre un module et un package. Un package représente simplement un espace de nom pour ranger des types Java (classes, interfaces, énumérations, annotations ou sous-packages). Les modules ont été introduits pour améliorer la sécurité et pour permettre une meilleure modularité de la plate-forme Java.

Des modules pour améliorer la sécurité

Java supporte la notion de portée pour les attributs, les méthodes et les types. Ainsi, il est possible de déclarer des classes de portée package pour qu’elles ne soient pas accessibles par du code extérieur au package de déclaration. Cependant, il ne s’agit pas d’un mécanisme de sécurité mais plus d’une manière de cacher de l’information qui n’est pas considérée comme pertinente pour le reste de l’application. Avec l’API de réflexion, il est tout à fait possible d’accéder aux types normalement inaccessibles à la compilation.

Java utilise la notion de chemin de classes (classpath) pour déterminer la localisation des packages et classes à charger dans la machine virtuelle. Ce mécanisme souffre de plusieurs problèmes de sécurité, notamment pour les serveurs qui peuvent potentiellement chargés du code malveillant embarqué dans les applications qu’ils hébergent.

Pour ces raisons, le développeur d’un module doit préciser :

  • la liste des modules requis par son module

  • éventuellement la liste des packages que le module exporte. C’est-à-dire les packages qui seront accessibles par les autres modules qui peuvent requérir ce module.

Des modules pour plus de modularité

Au fur et à mesure des années, l’API standard Java n’a fait que s’enrichir, rendant l’environnement Java de plus en plus volumineux. Cela peut être un handicap pour les applications ciblant un environnement à faible ressource ou pour faciliter la distribution d’une application. Beaucoup de fonctionnalités disponibles dans l’API standard ne sont pas ou peu utilisées par les applications (support CORBA, Web Services, accès aux bases de données…).

Avec les modules, il est possible de préciser les modules de l’API standard qui sont réellement utiles à l’application. Ainsi, il est possible de créer des applications plus légères.

L’introduction des modules a demandé un important travail au niveau de l’API standard. Cette dernière a été découpée en plus de 50 modules. Le module principal se nomme java.base et il contient les principaux packages de l’API standard comme java.lang et java.util.

Le descripteur de module (module-info.java)

Un module est défini par un fichier descripteur. Le fichier s’appelle obligatoirement module-info.java. Il est placé à la racine des sources du module. Ce fichier sera donc compilé pour produire le fichier module-info.class qui doit être distribué avec les autres fichiers du module.

Un descripteur de module indique le nom du module :

module dev.gayerie {
}

Comme pour les packages, le nom d’un module peut contenir plusieurs identifiants séparés par .. On utilise généralement la même convention de nommage que pour les packages en utilisant un nom de domaine inversé. Il n’est pas obligatoire que le nom du module ait un rapport avec le nom des packages qu’il contient.

Un descripteur de module permet de préciser :

  • les modules dont dépend le module

  • les packages exposés par le module

  • les services provenant d’autres modules et utilisés par le module

  • les services fournis par le module.

Dépendance à d’autres modules

L’instruction requires permet de nommer un module dont dépend le module.

module dev.gayerie {
    requires java.sql;
}

Dans l’exemple ci-dessus, on déclare que le module dev.gayerie a besoin du module java.sql pour fonctionner correctement. Cela signifie que le module dev.gayerie utilise des types Java publics ou protégés exposés par le module java.sql. Si par exemple, on souhaite créer un module qui utilise la classe java.sql.Connection, il faut obligatoirement indiquer que ce module dépend de java.sql pour que la classe java.sql.Connection soit disponible à la compilation et à l’exécution.

L’instruction requires peut s’accompagner des mots-clés static et transitive.

static

Signifie que la dépendance n’est nécessaire qu’à la compilation et qu’elle est optionnelle à l’exécution. Si le module n’est pas disponible à l’exécution, le programme n’échouera pas.

transitive

Implique que si un autre module est dépendant de notre module, il sera également dépendant du module requis. On parle de dépendance transitive.

module dev.gayerie {
    requires transitive java.sql;
    requires static java.compiler
}

Dans l’exemple précédent, le module dev.gayerie indique qu’il utilise des types Java des modules java.sql et java.compiler. Les types publics et protégés rendus disponibles par le module java.sql seront aussi disponibles pour tout module qui requière dev.gayerie (car on utilise le mot-clé transitive). Les types publics et protégés du module java.compiler ne sont requis qu’au moment de la compilation et sont optionnels à l’exécution (car on utilise le mot-clé static).

Le module java.base est requis par défaut, il n’est pas nécessaire de le préciser dans la liste des dépendances.

Exposer des packages

L’instruction exports permet de préciser le nom d’un package. Tous les types Java de portée public ou protected seront exposés pour tous les modules qui requièrent ce module. De cette manière, il est possible de contrôler les types qui sont exposés.

module dev.gayerie {
    exports dev.gayerie.data;
    exports dev.gayerie.service;
}

L’instruction opens permet de préciser le nom d’un package. Tous les types Java de portée public ou protected seront exposés à l’exécution et pas à la compilation. Cela permet d’autoriser l’accès pour le code qui utilise l’API de réflexion Java pour accéder dynamiquement à des types lors de l’exécution.

module dev.gayerie {
    exports dev.gayerie.data;
    exports dev.gayerie.service;
    opens dev.gayerie.handler;
}

Note

Il est possible de déclarer l’ensemble du module open. Cela signifie que tous ses packages sont ouverts par défaut en plus de ceux qui sont déclarés comme exportés.

open module dev.gayerie {
    exports dev.gayerie.data;
    exports dev.gayerie.service;
}

Il est possible d’exporter ou d’ouvrir certains packages uniquement pour un module donné. Cela peut être utile pour fournir des classes d’implémentation à un framework contenu dans un autre module. On précise le nom du module grâce au mot-clé to.

module dev.gayerie {
    exports dev.gayerie.data to dev.gayerie.dao;
}

Utiliser un service

Les services désignent des implémentations d’interface ou des spécialisations de classes abstraites fournies par des bibliothèques tierces. On parle de Service Provider Interface, abrégé en SPI. Un service est accessible à travers la classe ServiceLoader. Pour pouvoir utiliser un service issu d’un autre module, il faut déclarer son utilisation dans le descripteur de module grâce à l’instruction uses. Par exemple, le module java.base fournit des spécialisations du service FileSystemProvider. Pour pouvoir les charger, il faut déclaration l’utilisation de ce service dans le module.

module dev.gayerie {
    uses java.nio.file.spi.FileSystemProvider;
}

On peut ensuite charger le service grâce à la classe ServiceLoader :

ServiceLoader<FileSystemProvider> loader = ServiceLoader.load(FileSystemProvider.class);
Optional<FileSystemProvider> fsp = loader.findFirst();

Si l’utilisation du service n’est pas déclarée dans le descripteur de module, le code précédent échoue avec une ServiceConfigurationError.

Fournir un service

Un module peut fournir une implémentation d’un service accessible à travers la classe ServiceLoader. Pour cela, il faut déclarer l’implémentation du service avec l’instruction provides ... with.

import java.nio.file.spi.FileSystemProvider;
import dev.gayerie.service.RemoteFileSystemProvider;

module dev.gayerie {
    provides FileSystemProvider with RemoteFileSystemProvider;
}

Le module anonyme

Pour permettre une rétro-compatibilité avec le code Java antérieur à la version 9, la machine virtuelle Java doit considérer qu’un fichier JAR ou un chemin de classes qui ne fournit pas de descripteur de module module-info.class appartient à un module anonyme. Un module anonyme exporte et ouvre tous les packages qu’il définit.

La notion de module path

Avant l’apparition des modules, les classes Java et les packages pouvaient être localisés dans différents répertoires. L’ensemble des répertoires que la machine virtuelle Java doit inspecter pour trouver une classe constitue le chemin des classes ou classpath.

Avec l’apparition des modules, le classpath est remplacé par le chemin des modules ou module-path. Le chemin des modules correspond à l’ensemble des répertoires et/ou des fichiers JAR qui désignent un module, c’est-à-dire qui fournissent le descripteur de module module-info.class.