Comment migrer une application vers le système de modules Java?

De nombreuses applications n’ont pas été conçues pour utiliser le Système de Modules de la Plateforme Java, soit parce qu’elles ont été écrites avant sa création, soit parce qu’elles ont choisi de ne pas l’utiliser. Idéalement, elles ont au moins été conçues avec des projets plutôt que comme une grande masse désorganisée. Cette section vous donne un aperçu des stratégies pour migrer une application existante vers l’utilisation des modules. Nous couvrons l’ordonnancement des modules, la migration ascendante, la migration descendante, et comment diviser un projet existant.

Scénario du Monde Réel

Migration de Vos Applications au Travail

Dans le monde réel, les applications ont des bibliothèques qui n’ont pas été mises à jour depuis 10 ans ou plus, des graphes de dépendances complexes, et toutes sortes de surprises.

Notez que vous pouvez utiliser toutes les fonctionnalités de Java 17 sans convertir votre application en modules (à l’exception des fonctionnalités de ce chapitre sur les modules, bien sûr !). Assurez-vous d’avoir une raison pour la migration et ne pensez pas que c’est obligatoire.

Si vous vous retrouvez dans cette situation, envisagez de lire “The Java Module System” par Nicolai Parlog (Manning Publications, 2019).

Déterminer l’Ordre

Avant de pouvoir migrer notre application pour utiliser des modules, nous devons savoir comment les packages et les bibliothèques de l’application existante sont structurés. Supposons que nous avons une application simple avec trois fichiers JAR, comme montré dans la Figure 12.14. Les dépendances entre projets forment un graphe. Les deux représentations dans la Figure 12.14 sont équivalentes. Les flèches montrent les dépendances en pointant du projet qui nécessitera la dépendance vers celui qui la rend disponible. Dans le langage des modules, la flèche ira de requires à exports.

Le côté droit du diagramme facilite l’identification du haut et du bas auxquels la migration ascendante et descendante font référence. Les projets qui n’ont pas de dépendances sont en bas. Les projets qui ont des dépendances sont en haut.

Dans cet exemple, il n’y a qu’un seul ordre de haut en bas qui respecte toutes les dépendances. La Figure 12.15 montre que l’ordre n’est pas toujours unique. Puisque deux des projets n’ont pas de flèche entre eux, les deux ordres sont autorisés lors de la décision de l’ordre de migration.

Explorer une Stratégie de Migration Ascendante

L’approche la plus simple pour la migration est une migration ascendante. Cette approche fonctionne mieux lorsque vous avez le pouvoir de convertir tous les fichiers JAR qui ne sont pas déjà des modules. Pour une migration ascendante, vous suivez ces étapes:

  1. Choisissez le projet de niveau le plus bas qui n’a pas encore été migré. (Rappelez-vous comment nous les avons ordonnés par dépendances dans la section précédente?)
  2. Ajoutez un fichier module-info.java à ce projet. Assurez-vous d’ajouter des exports pour exposer tout package utilisé par des fichiers JAR de niveau supérieur. Ajoutez également une directive requires pour tous les modules dont ce module dépend.
  3. Déplacez ce module nommé nouvellement migré du classpath vers le module path.
  4. Assurez-vous que tous les projets qui n’ont pas encore été migrés restent comme modules sans nom sur le classpath.
  5. Répétez avec le projet de niveau suivant le plus bas jusqu’à ce que vous ayez terminé.

Vous pouvez voir cette procédure appliquée pour migrer trois projets dans la Figure 12.16. Remarquez que chaque projet est converti en module à son tour.

Avec une migration ascendante, vous mettez les projets de niveau inférieur en bon état. Cela facilite la migration des projets de niveau supérieur à la fin. Cela encourage également à faire attention à ce qui est exposé.

Pendant la migration, vous avez un mélange de modules nommés et de modules sans nom. Les modules nommés sont ceux de niveau inférieur qui ont été migrés. Ils sont sur le module path et ne sont pas autorisés à accéder aux modules sans nom.

Les modules sans nom sont sur le classpath. Ils peuvent accéder aux fichiers JAR sur le classpath et le module path.

Explorer une Stratégie de Migration Descendante

Une stratégie de migration descendante est plus utile lorsque vous n’avez pas le contrôle de chaque fichier JAR utilisé par votre application. Par exemple, supposons qu’une autre équipe possède un projet. Ils sont simplement trop occupés pour migrer. Vous ne voudriez pas que cette situation retarde toute votre migration.

Pour une migration descendante, vous suivez ces étapes:

  1. Placez tous les projets sur le module path.
  2. Choisissez le projet de niveau le plus élevé qui n’a pas encore été migré.
  3. Ajoutez un fichier module-info.java à ce projet pour convertir le module automatique en module nommé. Encore une fois, n’oubliez pas d’ajouter des directives exports ou requires. Vous pouvez utiliser le nom du module automatique d’autres modules lors de l’écriture de la directive requires puisque la plupart des projets sur le module path n’ont pas encore de noms.
  4. Répétez avec le projet de niveau suivant le plus élevé jusqu’à ce que vous ayez terminé.

Vous pouvez voir cette procédure appliquée pour migrer trois projets dans la Figure 12.17.

Avec une migration descendante, vous concédez que toutes les dépendances de niveau inférieur ne sont pas prêtes, mais que vous voulez faire de l’application elle-même un module.

Pendant la migration, vous avez un mélange de modules nommés et de modules automatiques. Les modules nommés sont ceux de niveau supérieur qui ont été migrés. Ils sont sur le module path et ont accès aux modules automatiques. Les modules automatiques sont également sur le module path.

CatégorieAscendanteDescendante
Projet qui dépend de tous les autresModule sans nom sur le classpathModule nommé sur le module path
Projet qui n’a pas de dépendancesModule nommé sur le module pathModule automatique sur le module path

Diviser un Grand Projet en Modules

Supposons que vous commenciez avec une application qui possède un certain nombre de packages. La première étape consiste à les diviser en groupes logiques et à dessiner les dépendances entre eux. La Figure 12.18 montre la décomposition d’un système imaginaire. Remarquez qu’il y a sept packages des deux côtés gauche et droit. Il y a moins de modules car certains packages partagent un module.

Il y a un problème avec cette décomposition. Le voyez-vous? Le Système de Modules de la Plateforme Java ne permet pas les dépendances cycliques. Une dépendance cyclique, ou dépendance circulaire, est lorsque deux choses dépendent directement ou indirectement l’une de l’autre. Si le module zoo.tickets.delivery requiert le module zoo.tickets.discount, zoo.tickets.discount n’est pas autorisé à requérir le module zoo.tickets.delivery.

Maintenant que nous savons que la décomposition de la Figure 12.18 ne fonctionnera pas, que pouvons-nous faire? Une technique courante consiste à introduire un autre module. Ce module contient le code que les deux autres modules partagent. La Figure 12.19 montre les nouveaux modules sans aucune dépendance cyclique. Remarquez le nouveau module zoo.tickets.etech. Nous avons créé de nouveaux packages à mettre dans ce module. Cela permet aux développeurs d’y mettre le code commun et de briser la dépendance. Plus de dépendances cycliques!

Échec de Compilation avec une Dépendance Cyclique

Il est extrêmement important de comprendre que Java ne vous permettra pas de compiler des modules qui ont des dépendances circulaires. Dans cette section, nous examinons un exemple menant à cette erreur de compilation.

Considérez le module zoo.butterfly décrit ici:

// Butterfly.java
package zoo.butterfly;
public class Butterfly {
    private Caterpillar caterpillar;
}

// module-info.java
module zoo.butterfly {
    exports zoo.butterfly;
    requires zoo.caterpillar;
}

Nous ne pouvons pas encore compiler cela car nous devons d’abord construire zoo.caterpillar. Après tout, notre papillon le requiert. Maintenant, nous regardons zoo.caterpillar:

// Caterpillar.java
package zoo.caterpillar;
public class Caterpillar {
    Butterfly emergeCocoon() {
        // logique omise
    }
}

// module-info.java
module zoo.caterpillar {
    exports zoo.caterpillar;
    requires zoo.butterfly;
}

Nous ne pouvons pas encore compiler cela car nous devons d’abord construire zoo.butterfly. Oh non! Nous avons maintenant une impasse. Aucun des modules ne peut être compilé. C’est notre problème de dépendance circulaire à l’œuvre.

Vous pourriez vous demander ce qui se passe si trois modules sont impliqués. Supposons que le module balleA requiert le module balleB et que balleB requiert le module balleC. Le module balleC peut-il requérir le module balleA? Non. Cela créerait une dépendance cyclique. N’êtes-vous pas convaincus? Essayez de le dessiner. Vous pouvez suivre votre crayon autour du cercle de balleA à balleB à balleC à balleA à… enfin, vous voyez l’idée. Il y a simplement trop de balles en l’air!

Java vous permettra toujours d’avoir une dépendance cyclique entre packages au sein d’un module. Il impose que vous n’ayez pas de dépendance cyclique entre modules.