Guide de la modularité Java 9

Guide de la modularité Java 9

1. Vue d'ensemble

Java 9 introduit un nouveau niveau d'abstraction par rapport aux packages, anciennement connu sous le nom de Java Platform Module System (JPMS), ou "Modules".

Dans ce didacticiel, nous allons passer en revue le nouveau système et discuter de ses différents aspects.

Nous allons également créer un projet simple pour démontrer tous les concepts que nous allons apprendre dans ce guide.

2. Qu'est-ce qu'un module?

Tout d'abord, nous devons comprendre ce qu'est un module avant de pouvoir comprendre comment les utiliser.

Un module est un groupe de packages et de ressources étroitement liés avec un nouveau fichier de descripteur de module.

En d’autres termes, il s’agit d’une abstraction «package of Java Packages» qui nous permet de rendre notre code encore plus réutilisable.

2.1. Paquets

Les packages à l'intérieur d'un module sont identiques aux packages Java que nous utilisons depuis la création de Java.

Lorsque nous créons un module,we organize the code internally in packages just like we previously did with any other project.

Outre l'organisation de notre code, les packages sont utilisés pour déterminer quel code est accessible au public en dehors du module. Nous passerons plus de temps à en parler plus tard dans l'article.

2.2. Ressources

Chaque module est responsable de ses ressources, telles que les fichiers multimédias ou de configuration.

Auparavant, nous plaçons toutes les ressources au niveau racine de notre projet et gérons manuellement les ressources appartenant à différentes parties de l'application.

Avec les modules, nous pouvons livrer les images et les fichiers XML requis avec le module qui en a besoin, facilitant ainsi la gestion de nos projets.

2.3. Descripteur de module

Lorsque nous créons un module, nous incluons un fichier descripteur qui définit plusieurs aspects de notre nouveau module:

  • Name - le nom de notre module

  • Dependencies - une liste des autres modules dont ce module dépend

  • Public Packages - une liste de tous les paquets que nous voulons accessibles depuis l'extérieur du module

  • Services Offered - nous pouvons fournir des implémentations de service qui peuvent être consommées par d'autres modules

  • Services Consumed - permet au module actuel d'être un consommateur d'un service

  • Reflection Permissions - autorise explicitement d'autres classes à utiliser la réflexion pour accéder aux membres privés d'un package

Les règles de nommage des modules sont similaires à la façon dont nous nommons les packages (les points sont autorisés, les tirets ne le sont pas). Il est très courant d’utiliser des noms de style de type projet (my.module) ou DNS inversé (com.example.mymodule). Nous utiliserons le style projet dans ce guide.

Nous devons lister tous les packages que nous voulons rendre publics car par défaut, tous les packages sont des modules privés.

La même chose est vraie pour la réflexion. Par défaut, nous ne pouvons pas utiliser la réflexion sur les classes que nous importons depuis un autre module.

Plus loin dans l'article, nous examinerons des exemples d'utilisation du fichier descripteur de module.

2.4. Types de modules

Le nouveau système de modules comprend quatre types de modules:

  • System Modules – Ce sont les modules répertoriés lorsque nous exécutons la commandelist-modules ci-dessus. Ils incluent les modules Java SE et JDK.

  • Application Modules - Ces modules sont ce que nous voulons généralement construire lorsque nous décidons d'utiliser des modules. Ils sont nommés et définis dans le fichiermodule-info.class compilé inclus dans le JAR assemblé.

  • Automatic Modules - Nous pouvons inclure des modules non officiels en ajoutant des fichiers JAR existants au chemin du module. Le nom du module sera dérivé du nom du fichier JAR. Les modules automatiques auront un accès en lecture complet à tous les autres modules chargés par le chemin.

  • Unnamed Module - Lorsqu'une classe ou un JAR est chargé sur le chemin de classe, mais pas sur le chemin du module, il est automatiquement ajouté au module sans nom. C'est un module fourre-tout pour maintenir la compatibilité descendante avec le code Java précédemment écrit.

2.5. Distribution

Les modules peuvent être distribués de deux manières: en tant que fichier JAR ou en tant que projet compilé «éclaté». Ceci, bien sûr, est identique à tout autre projet Java et ne devrait donc pas surprendre.

Nous pouvons créer des projets multi-modules composés d'une "application principale" et de plusieurs modules de bibliothèque.

Nous devons cependant faire attention car nous ne pouvons avoir qu'un seul module par fichier JAR.

Lorsque nous configurons notre fichier de construction, nous devons nous assurer de regrouper chaque module de notre projet dans un fichier jar distinct.

3. Modules par défaut

Lorsque nous installons Java 9, nous pouvons constater que le JDK a maintenant une nouvelle structure.

Ils ont pris tous les paquets originaux et les ont déplacés dans le nouveau système de modules.

Nous pouvons voir ce que sont ces modules en tapant dans la ligne de commande:

java --list-modules

Ces modules sont divisés en quatre grands groupes:java, javafx, jdk, andOracle.

Les modulesjava sont les classes d'implémentation de la spécification de langage SE principale.

Les modulesjavafx sont les bibliothèques FX UI.

Tout ce dont le JDK lui-même a besoin est conservé dans les modulesjdk.

Et enfin,anything that is Oracle-specific is in the oracle modules.

4. Déclarations de module

Pour configurer un module, nous devons mettre un fichier spécial à la racine de nos paquets nommémodule-info.java.

Ce fichier est appelé descripteur de module et contient toutes les données nécessaires à la construction et à l'utilisation de notre nouveau module.

Nous construisons le module avec une déclaration dont le corps est vide ou composé de directives de module:

module myModuleName {
    // all directives are optional
}

Nous commençons la déclaration du module avec le mot-clémodule, et nous suivons cela avec le nom du module.

Le module fonctionnera avec cette déclaration, mais nous aurons généralement besoin de plus d'informations.

C'est là qu'interviennent les directives du module.

4.1. A besoin

Notre première directive estrequires. Cette directive de module nous permet de déclarer des dépendances de module:

module my.module {
    requires module.name;
}

Maintenant,my.module aboth a runtime and a compile-time dependency surmodule.name.

Et tous les types publics exportés depuis une dépendance sont accessibles par notre module lorsque nous utilisons cette directive.

4.2. Nécessite statique

Parfois, nous écrivons du code faisant référence à un autre module, mais que les utilisateurs de notre bibliothèque ne voudront jamais utiliser.

Par exemple, nous pourrions écrire une fonction utilitaire qui imprime joliment notre état interne lorsqu'un autre module de journalisation est présent. Cependant, tous les utilisateurs de notre bibliothèque ne souhaiteront pas cette fonctionnalité et ne souhaitent pas inclure de bibliothèque de journalisation supplémentaire.

Dans ces cas, nous souhaitons utiliser une dépendance optionnelle. En utilisant la directiverequires static, nous créons une dépendance à la compilation uniquement:

module my.module {
    requires static module.name;
}

4.3. Nécessite Transitive

Nous travaillons généralement avec des bibliothèques pour nous faciliter la vie.

Mais, nous devons nous assurer que tout module qui apporte notre code apportera également ces dépendances «transitives» supplémentaires, sinon elles ne fonctionneront pas.

Heureusement, nous pouvons utiliser la directiverequires transitive pour forcer tous les consommateurs en aval à lire également nos dépendances requises:

module my.module {
    requires transitive module.name;
}

Désormais, lorsqu'un développeurrequires my.module, il n'aura pas non plus à direrequires module.name pour que notre module fonctionne toujours.

4.4. Exportations

By default, a module doesn’t expose any of its API to other modules. Cestrong encapsulation a été l'un des principaux facteurs de motivation pour créer le système de modules en premier lieu.

Notre code est nettement plus sécurisé, mais nous devons maintenant ouvrir explicitement notre API au monde entier si nous voulons qu’elle soit utilisable.

Nous utilisons la directiveexports pour exposer tous les membres publics du package nommé:

module my.module {
    exports com.my.package.name;
}

Maintenant, quand quelqu'un faitrequires my.module, il aura accès aux types publics de notre paquetcom.my.package.name, mais pas à tout autre paquet.

4.5. Exportations… vers

Nous pouvons utiliserexports…to pour ouvrir nos classes publiques au monde.

Mais que faire si nous ne voulons pas que le monde entier accède à notre API?

Nous pouvons restreindre les modules qui ont accès à nos API en utilisant la directiveexports…to.

Semblable à la directiveexports, nous déclarons un package comme exporté. Mais, nous listons également les modules que nous autorisons à importer ce paquet en tant querequires. Voyons à quoi cela ressemble:

module my.module {
    export com.my.package.name to com.specific.package;
}

4.6. Uses

Unservice est une implémentation d'une interface spécifique ou d'une classe abstraite qui peut êtreconsumed par d'autres classes.

Nous désignons les services que notre module consomme avec la directiveuses.

Notez quethe class name we use is either the interface or abstract class of the service, not the implementation class:

module my.module {
    uses class.name;
}

Il faut noter ici qu'il existe une différence entre une directiverequires et la directiveuses.

Nous pourrionsrequire un module qui fournit un service que nous voulons consommer, mais ce service implémente une interface à partir d'une de ses dépendances transitives.

Au lieu de forcer notre module à exiger les dépendances transitoires deall au cas où, nous utilisons la directiveuses pour ajouter l'interface requise au chemin du module.

4.7. Fournit… avec

Un module peut également être unservice provider que d'autres modules peuvent consommer.

La première partie de la directive est le mot cléprovides. Voici où nous mettons le nom de l'interface ou de la classe abstraite.

Ensuite, nous avons la directivewith où nous fournissons le nom de la classe d'implémentation qui soitimplementsest l'interface ouextendsest la classe abstraite.

Voici à quoi cela ressemble:

module my.module {
    provides MyInterface with MyInterfaceImpl;
}

4.8. Open

Nous avons mentionné précédemment que l’encapsulation était un facteur de motivation important pour la conception de ce système de modules.

Avant Java 9, il était possible d'utiliser la réflexion pour examiner tous les types et membres d'un package, même lesprivate. Rien n’a été véritablement encapsulé, ce qui peut poser toutes sortes de problèmes aux développeurs des bibliothèques.

Parce que Java 9 appliquestrong encapsulation,we now have to explicitly grant permission for other modules to reflect on our classes.

Si nous voulons continuer à permettre une réflexion complète comme le faisaient les anciennes versions de Java, nous pouvons simplementopen le module entier:

open module my.module {
}

4.9. Ouvre

Si nous devons autoriser la réflexion des types privés, mais que nous ne voulons pas que tout notre code soit exposé,we can use the opens directive to expose specific packages.

Mais rappelez-vous que cela ouvrira le paquet au monde entier, alors assurez-vous que c'est ce que vous voulez:

module my.module {
  opens com.my.package;
}

4.10. Ouvre… à

D'accord, donc la réflexion est parfois excellente, mais nous voulons toujours autant de sécurité que nous pouvons obtenir deencapsulation. We can selectively open our packages to a pre-approved list of modules, in this case, using the opens…to directive:

module my.module {
    opens com.my.package to moduleOne, moduleTwo, etc.;
}

5. Options de ligne de commande

À présent, la prise en charge des modules Java 9 a été ajoutée à Maven et Gradle, vous n'aurez donc pas besoin de faire beaucoup de construction manuelle de vos projets. Cependant, il est toujours utile de savoirhow pour utiliser le système de modules à partir de la ligne de commande.

Nous allons utiliser la ligne de commande pour notre exemple complet ci-dessous pour aider à solidifier le fonctionnement de l'ensemble du système dans notre esprit.

  • module-path– Nous utilisons l'option–module-path pour spécifier le chemin du module. Ceci est une liste d'un ou plusieurs répertoires contenant vos modules.

  • add-reads - Au lieu de nous fier au fichier de déclaration du module, nous pouvons utiliser l'équivalent en ligne de commande de la directiverequires; –add-reads.

  • add-exports–  Remplacement de la ligne de commande pour la directiveexports.

  • add-opens– Remplacez la clauseopen dans le fichier de déclaration du module.

  • add-modules–  Ajoute la liste des modules dans l'ensemble de modules par défaut

  • list-modules– Imprime une liste de tous les modules et leurs chaînes de version

  • patch-module - Ajouter ou remplacer des classes dans un module

  • illegal-access=permit|warn|deny: relâchez l'encapsulation forte en affichant un seul avertissement global, affiche chaque avertissement ou échoue avec des erreurs. La valeur par défaut estpermit.

6. Visibilité

Nous devrions passer un peu de temps à parler de la visibilité de notre code.

A lot of libraries depend on reflection to work their magic (JUnit et Spring viennent à l'esprit).

Par défaut dans Java 9, nous pourronsonly avoir accès aux classes, méthodes et champs publics dans nos packages exportés. Même si nous utilisons la réflexion pour accéder à des membres non publics et que nous appelonssetAccessible(true), , swe ne pourra pas accéder à ces membres.

Nous pouvons utiliser les optionsopen,opens etopens…to pour accorder un accès à l'exécution uniquement pour la réflexion. Remarque,this is runtime-only!

Nous ne pourrons pas compiler avec des types privés, et nous ne devrions jamais en avoir besoin de toute façon.

Si nous devons avoir accès à un module pour la réflexion, et que nous ne sommes pas le propriétaire de ce module (c'est-à-dire que nous ne pouvons pas utiliser la directiveopens…to), alors il est possible d'utiliser la ligne de commande–add-opens option pour permettre à ses propres modules d'accéder par réflexion au module verrouillé au moment de l'exécution.

La seule mise en garde ici est que vous devez avoir accès aux arguments de ligne de commande qui sont utilisés pour exécuter un module pour que cela fonctionne.

7. Mettre tous ensemble

Maintenant que nous savons ce qu'est un module et comment l'utiliser, allons-y et construisons un projet simple pour démontrer tous les concepts que nous venons d'apprendre.

Pour simplifier les choses, nous n'utiliserons ni Maven ni Gradle. Au lieu de cela, nous nous fierons aux outils de ligne de commande pour créer nos modules.

7.1. Mise en place de notre projet

Premièrement, nous devons mettre en place notre structure de projet. Nous allons créer plusieurs répertoires pour organiser nos fichiers.

Commencez par créer le dossier du projet:

mkdir module-project
cd module-project

C’est la base de tout notre projet. Vous devez donc ajouter des fichiers, tels que des fichiers de construction Maven ou Gradle, d’autres répertoires sources et des ressources.

Nous mettons également un répertoire pour contenir tous nos modules spécifiques au projet.

Ensuite, nous créons un répertoire de modules:

mkdir simple-modules

Voici à quoi ressemblera la structure de notre projet:

module-project
|- // src if we use the default package
|- // build files also go at this level
|- simple-modules
  |- hello.modules
    |- com
      |- example
        |- modules
          |- hello
  |- main.app
    |- com
      |- example
        |- modules
          |- main

7.2. Notre premier module

Maintenant que nous avons la structure de base en place, ajoutons notre premier module.

Sous le répertoiresimple-modules , créez un nouveau répertoire appeléhello.modules.

We can name this anything we want but follow package naming rules (c'est-à-dire des points pour séparer les mots, etc.). Nous pouvons même utiliser le nom de notre paquet principal comme nom de module si nous le voulons, mais nous voulons généralement nous en tenir au même nom que celui que nous utiliserions pour créer un fichier JAR de ce module.

Sous notre nouveau module, nous pouvons créer les packages que nous voulons. Dans notre cas, nous allons créer une structure de paquet:

com.example.modules.hello

Ensuite, créez une nouvelle classe appeléeHelloModules.java dans ce package. Nous allons garder le code simple:

package com.example.modules.hello;

public class HelloModules {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }
}

Et enfin, dans le répertoire racine dehello.modules, ajoutez notre descripteur de module; module-info.java:

module hello.modules {
    exports com.example.modules.hello;
}

Pour garder cet exemple simple, tout ce que nous faisons est d'exporter tous les membres publics du spackagecom.example.modules.hello .

7.3. Notre deuxième module

Notre premier module est génial, mais il ne fait rien.

Nous pouvons créer un deuxième module qui l'utilise maintenant.

Dans notre répertoiresimple-modules, créez un autre répertoire de module appelémain.app. Nous allons commencer par le descripteur de module cette fois-ci:

module main.app {
    requires hello.modules;
}

Nous n'avons pas besoin d'exposer quoi que ce soit au monde extérieur. Au lieu de cela, tout ce dont nous avons besoin est de dépendre de notre premier module, nous avons donc accès aux classes publiques qu’il exporte.

Nous pouvons maintenant créer une application qui l'utilise.

Créez une nouvelle structure de package:com.example.modules.main.

Maintenant, créez un nouveau fichier de classe appeléMainApp.java.

package com.example.modules.main;

import com.example.modules.hello.HelloModules;

public class MainApp {
    public static void main(String[] args) {
        HelloModules.doSomething();
    }
}

Et c'est tout le code dont nous avons besoin pour démontrer les modules. Notre prochaine étape consiste à générer et à exécuter ce code à partir de la ligne de commande.

7.4. Construire nos modules

Pour construire notre projet, nous pouvons créer un script bash simple et le placer à la racine de notre projet.

Créez un fichier appelécompile-simple-modules.sh:

#!/usr/bin/env bash
javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

Cette commande comprend deux parties, les commandesjavac etfind.

La commandefind affiche simplement une liste de tous les fichiers.java sous notre répertoire simple-modules. Nous pouvons ensuite alimenter cette liste directement dans le compilateur Java.

La seule chose que nous devons faire différemment des anciennes versions de Java est de fournir un paramètremodule-source-path pour informer le compilateur qu'il construit des modules.

Une fois cette commande exécutée, nous aurons un dossieroutDir avec deux modules compilés à l'intérieur.

7.5. Exécution de notre code

Et nous pouvons enfin exécuter notre code pour vérifier que les modules fonctionnent correctement.

Créez un autre fichier à la racine du projet:run-simple-module-app.sh.

#!/usr/bin/env bash
java --module-path outDir -m main.app/com.example.modules.main.MainApp

Pour exécuter un module, nous devons fournir au moins lesmodule-path et la classe principale. Si tout fonctionne, vous devriez voir:

>$ ./run-simple-module-app.sh
Hello, Modules!

7.6. Ajouter un service

Maintenant que nous avons une compréhension de base de la façon de créer un module, rendons-le un peu plus compliqué.

Nous allons voir comment utiliser les directivesprovides…with etuses.

Commencez par définir un nouveau fichier dans le modulehello.modules nomméHelloInterface.java:

public interface HelloInterface {
    void sayHello();
}

Pour faciliter les choses, nous allons implémenter cette interface avec notre classeHelloModules.java existante:

public class HelloModules implements HelloInterface {
    public static void doSomething() {
        System.out.println("Hello, Modules!");
    }

    public void sayHello() {
        System.out.println("Hello!");
    }
}

C'est tout ce dont nous avons besoin pour créer unservice.

Maintenant, nous devons dire au monde que notre module fournit ce service.

Ajoutez ce qui suit à nosmodule-info.java:

provides com.example.modules.hello.HelloInterface with com.example.modules.hello.HelloModules;

Comme nous pouvons le voir, nous déclarons l'interface et la classe qui l'implémente.

Ensuite, nous devons consommer cesservice. Dans notre modulemain.app, ajoutons ce qui suit à nosmodule-info.java:

uses com.example.modules.hello.HelloInterface;

Enfin, dans notre méthode principale, nous pouvons utiliser ce service comme ceci:

HelloModules module = new HelloModules();
module.sayHello();

Compiler et exécuter:

#> ./run-simple-module-app.sh
Hello, Modules!
Hello!

Nous utilisons ces directives pour être beaucoup plus explicites sur la façon dont notre code doit être utilisé.

Nous pourrions mettre l'implémentation dans un paquet privé tout en exposant l'interface dans un paquet public.

Cela rend notre code beaucoup plus sécurisé avec très peu de frais généraux supplémentaires.

Allez-y et essayez certaines des autres directives pour en savoir plus sur les modules et leur fonctionnement.

8. Ajout de modules au module sans nom

The unnamed module concept is similar to the default package. Par conséquent, il n’est pas considéré comme un module réel, mais peut être considéré comme le module par défaut.

Si une classe n'est pas membre d'un module nommé, elle sera automatiquement considérée comme faisant partie de ce module sans nom.

Parfois, pour garantir des modules spécifiques de plate-forme, de bibliothèque ou de fournisseur de services dans le graphique de module, nous devons ajouter des modules au jeu de racines par défaut. Par exemple, lorsque nous essayons d'exécuter des programmes Java 8 tels quels avec le compilateur Java 9, il peut être nécessaire d'ajouter des modules.

En général,the option to add the named modules to the default set of root modules is *–add-modules <module>*(,<module>)*<module> est un nom de module.

Par exemple, pour donner accès à tous les modulesjava.xml.bind, la syntaxe serait:

--add-modules java.xml.bind

Pour l'utiliser dans Maven, nous pouvons intégrer la même chose auxmaven-compiler-plugin:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.8.0
    
        9
        9
        
            --add-modules
            java.xml.bind
        
    

9. Conclusion

Dans ce guide détaillé, nous avons abordé et abordé les bases du nouveau système Java 9 Module.

Nous avons commencé par parler de ce qu'est un module.

Nous avons ensuite expliqué comment déterminer quels modules sont inclus dans le JDK.

Nous avons également traité en détail le fichier de déclaration du module.

Nous avons complété la théorie en parlant des différents arguments de ligne de commande dont nous aurons besoin pour construire nos modules.

Enfin, nous avons mis en pratique toutes nos connaissances antérieures et avons créé une application simple, construite à partir du système de modules.

Pour voir ce code et plus, assurez-vous decheck it out over on Github.