Migration java 9: Les challenges les plus importants

Je suis sûr que vous avez entendu dire que la migration vers Java 9 n’est pas une démarche évidente, peut-être même inutile et qu’une migration n’a aucun sens pour de grands projets à l’heure actuelle. Après avoir migrer une base de code ancienne et assez importante, je peux vous dire que ce n’est pas si difficile. C’est plus tricky que de passer à Java 8, certe, mais c’est un temps dépensé à bon escient. Plus que toute autre chose, la migration a révélé quelques  problèmes qui devaient être réparés indépendamment de la migration elle-même. J’ai pu recueillir quelques détails surprenants sur java 9. Mais j’ai condensé les sept problèmes les plus importants dans ce guide de migration Java 9. Cet article peut être considéré aussi bien comme un simple poste qu’une ressource qui pourrait vous servir dans le futur, lorsque vous aurez un problème concret. Notez également que si vous avez besoin de connaître un peu le système de module (voici un guide pratique).

Il ne s’agit pas de la modularisation de votre application: il ne s’agit que de là compiler et de l’exécuter sur Java 9.

  • Aperçu:

Il s’agit d’une liste des sept points les plus susceptibles de vous poser des problèmes pendant une migration vers Java 9:

  1. Accès illégal aux API internes
  2. Dépendances sur les modules Java EE
  3. Packages fractionnés
  4. Le casting de l’URL class loader
  5. Nouvelle gestion des ressources java
  6. Le Boot class path
  7.  Le format des nouvelles versions de Java
  8. Résumé

Chaque section explique le problème, les symptômes les plus communs qui vous aident à l’identifier et un ensemble de corrections possibles (généralement des options de ligne de commande pour java ou javac). Un poste futur va lier les corrections individuelles à une stratégie de migration plus large et faire des recommandations basées sur mes expériences.

    • Accès illégal aux API internes

L’un des principaux points de vente du système de module est une encapsulation solide. Java 9 s’assure que les classes non publiques ainsi que les classes provenant de paquets non exportés soient inaccessibles de l’extérieur du module. Cela s’applique bien sûr aux modules de plate-forme livrés avec le JDK, où seuls les paquetages java.* et javax.* sont entièrement pris en charge. La plupart des paquets com.sun.* et sun.*, sont internes et donc inaccessibles par défaut. Bien que le compilateur Java 9 se comporte exactement comme si vous l’aviez prévu et empêche l’accès illégal, ceci n’est plus le cas durant la phase d’exécution. Pour offrir un minimum de retro compatibilité, il facilite la migration et améliore les chances d’applications créées sur Java 8 pour s’exécuter sur Java 9 en accordant l’accès aux classes internes. Si la réflexion est utilisée pour l’accès, un avertissement est émis.

      • Symptômes

        Au cours de la compilation sous java 9, vous ferez probablement face à ce genre d’erreurs:

Les warnings émis à cause de la réflexion en java ressemblent à ceci:

      • Corrections 

La solution la plus évidente et durable pour les dépendances sur les API internes est de les éliminer. Remplacez-les par des API maintenues et vous aurez remboursé une dette technique à haut risque. Si cela ne peut être fait pour une raison quelconque, la meilleure chose à suivre est d’identifier les dépendances et d’informer le système de module dont vous avez besoin. À cette fin, vous pouvez utiliser deux options en ligne de commande:

      1.           L’option –add-exports $module / $package = $readingmodule peut être utilisée pour exporter $package d’un $module vers $readmodule. Le code de $readmodule peut donc accéder à tous les types publics dans $package mais d’autres modules ne le peuvent pas. Lorsque vous configurez $readingmodule à « ALL-UNNAMED », tous les modules du graphe  et code du classePath peuvent accéder à ce paquet. Lors d’une migration vers Java 9, vous utiliserez toujours ce placeholder. L’option est disponible pour les commandes java et javac.
      2.          Cela couvre l’accès aux membres publics de types publics, mais la réflexion peut faire plus que cela: grâce à l’utilisation généreuse de setAccessible (true), elle permet une interaction avec des classes, des champs, des constructeurs et des méthodes non publiques (parfois appelés réflexion profonde), qui dans les paquets exportés sont toujours encapsulés. L’option java –add-opens utilise la même syntaxe que –add-exports et ouvre le package à une réflexion profonde, ce qui signifie que tous ses types et leurs membres sont accessibles indépendamment de leurs modificateurs de visibilité.

Vous avez évidemment besoin de –add-exports pour apaiser le compilateur, mais grouper les commandes addexporter-exportations et –add-opens pour la phase d’exécution a aussi des avantages :

      1.  Le comportement permissif de la phase d’exécution changera dans les versions futures de Java, donc vous devez faire ce travail à un moment donné de toute façon
      2.  –add-opens fait disparaître les avertissements d’accès réfléchi illégal
      3. Comme je vous  le montrerai dans une minute, vous pouvez vous assurer qu’aucune nouvelle dépendance ne survient en faisant en sorte que la phase d’exécution applique une forte encapsulation
      • Aller plus loin

Compiler en Java 9 aide à identifier les dépendances sur les API internes dans la base de code projet. Mais les bibliothèques et les frameworks utilisés par votre projet sont tout aussi susceptibles de créer des problèmes. JDeps est l’outil idéal pour trouver des dépendances de compilation sur les API internes JDK dans votre projet et vos dépendances. Si vous ne le connaissez pas, j’écrirai un tutoriel qui vous permettra de commencer. Voici comment l’utiliser pour la tâche à accomplir:

Ici, libs est un dossier contenant toutes vos dépendances et $project, les  JAR de votre projet. L’analyse de la sortie est au-delà de la portée de cet article, mais ce n’est pas si difficile – vous allez vous retrouver. Trouver un accès réflexion est un peu plus difficile. Le comportement par défaut de l’exécution est de vous avertir une fois pour le premier accès illégal à un paquet, ce qui est insuffisant. Heureusement, il existe l’option –illegal-access = $value, où $value peut être:

      • permit: l’accès à toutes les API internes JDK est autorisé à n’importe quel code source situé dans le classpath. Pour un accès par réflexion, un seul avertissement est émis pour le premier accès à chaque paquet. (Par défaut dans Java 9)
      • warn: se comporte comme l’option permit, mais un avertissement est émis pour chaque accès par reflexion.
      • debug: se comporte comme l’option warn, mais une stack trace est incluse dans chaque avertissement.
      • deny: l’option pour ceux qui croient en un encapsulage fort:    Tous les accès illégaux sont interdits par défaut.

L’option deny est particulièrement très utile pour identifier tous les accès par réflexion. Il est également une excellente valeur par défaut à définir une fois que vous avez collecté toutes les dépendance via les options –add-exports et –add-opens . De cette façon, aucune nouvelle dépendance ne peut apparaître sans que vous ne le remarquiez.

  • Dépendances sur les modules Java EE 

Il existe beaucoup de code source dans Java SE qui est en réalité lié à Java EE. On retrouve ces six modules:

  1. java.activation avec le package javax.activation
  2. java.corba avec les packages javax.activity, javax.rmi, javax.rmi.CORBA et org.omg. *    
  3. java.transaction avec le package javax.transaction
  4. java.xml.bind avec tous les packages javax.xml.bind. *    
  5. java.xml.ws avec les packages javax.jws, javax.jws.soap, javax.xml.soap et tous les packages javax.xml.ws.*   
  6.  java.xml.ws.annotation avec le package javax.annotation

Pour diverses raisons de compatibilité (certain d’entre eux étant des paquets fractionnés, que nous examinerons ensuite), le source code dans le classpath ne voit pas ces modules par défaut, ce qui entraîne des erreurs de compilation ou d’exécution.

Symptômes

Ci-dessous est une erreur de compilation pour les classes utilisant JAXBException du module java.xml.bind:

Si vous essayez de lancer l’exécution, vous obtiendrez une NoClassDefFoundError:

Corrections:

Une fois que vous avez modulé votre code source, vous pouvez définir une dépendance régulière dans la déclaration du module. Jusque-là, la commande –add-modules $module convient parfaitement, ce qui garantit que le $module est disponible et peut être ajouté aux programmes java et javac. Si vous ajoutez java.se.ee, vous aurez accès à tous les modules Java EE.

  • Package fractionnés

Celui-ci est un peu « tricky »… En fait pour faire respecter la cohérence, un module n’est pas autorisé à lire le même package à partir de deux modules différents. Cependant, l’implémentation réelle est plus stricte et aucun module ne peut contenir le même package(exporté ou non). Le système de module fonctionne sous cette hypothèse et chaque fois qu’il faut charger une classe, il recherche quel module contient ce package en question et recherche la classe à l’intérieur (ce qui devrait stimuler la performance du chargement de classe, vu que la recherche sera faite uniquement à un endroit).

Pour préserver cette hypothèse, le système de module vérifie que deux modules nommés ne divisent pas un package, s’il en trouve un. Au cours de la migration, vous n’êtes pas tout à fait dans cette situation. Votre code source provient du classpath, ce qui le place dans le module dit non nommé. Pour maximiser la compatibilité, il n’est pas contrôlé et aucune vérification relative au module n’est appliquée. Maintenant, dans le cas de packages fractionnés, cela signifie qu’une division entre un module nommé (par exemple dans le JDK) et le module non nommé n’est pas identifiée(découverte). Ce qui peut sembler très favorable à première vu, mais c’est tout le contraire si vous prenez en compte le comportement de chargement des classes: si un package est divisé entre un module et le classpath, le chargement des classes de ce package seront toujours effectué à partir de ce module. Cela signifie que les classes dans la partie du classpath du package seront effectivement invisibles.

Symptômes

Le symptôme ici est qu’une classe du classpath ne peut pas être chargée même si elle est effectivement là, ce qui entraîne des erreurs de compilation comme ceci:

Ou, au moment de l’exécution, à NoClassDefFoundErrors comme ci-dessus. Un exemple où cela peut se produire est avec les différentes implémentations JSR-305. Un projet utilisant, par exemple, les annotations javax.annotation.Generated (from java.xml.ws.annotation) et java.annotation.Nonnull (à partir de com.google.code.findbugs: jsr305) aura du mal à compiler. Soit il lui manquera les annotations Java EE ou alors, lorsque le module sera ajouté comme décrit ci-dessus, il aura un problème de package fractionné et ne verra pas le module JSR 305.

Corrections 

Le processus de migration sera différent, selon l’artefact qui divise le package JDK. Dans certains cas, il pourrait s’agir de plus que certaines classes qui incluse de façon aléatoire dans un package du JDK, mais plutôt un remplacement pour un module JDK complet, par exemple parce qu’il remplace un standard approuvé. Dans ce cas, vous aurez besoin de l’option –upgrade-module-path $dir – les modules trouvés dans $dir sont utilisés pour remplacer les modules évolutifs pendant la phase d’exécution. Si en effet, vous avez juste quelques classes qui divisent un package, la solution à long terme consiste à supprimer la division. Dans le cas où cela n’est pas possible à court terme, vous pouvez réparer le module nommé avec le contenu du classpath. L’option –patch-module $module = $artefact fusionne toutes les classes de $artefact en $module, en mettant toutes les parties du package fractionné dans le même module, en supprimant ainsi la division. Cependant, il existe quelques éléments à surveiller. Tout d’abord, le module corrigé doit effectivement entrer dans le graph du module, pour lequel il peut être nécessaire d’utiliser la commande –add-modules. Ensuite, il doit avoir accès à toutes les dépendances qu’il doit exécuter avec succès. Étant donné que les modules nommés ne peuvent pas accéder au code source du classpath, cela pourrait être nécessaire de créer des modules automatiques, ce qui dépasse la portée de ce post.

Aller plus loin

Trouver un package fractionné par tentative d’exécution, avec une erreur à la clé est assez énervant. Heureusement, JDeps les rapporte, donc si vous analysez votre projet et ses dépendances, les premières lignes de sortie signalent des packages fractionnés. Vous pouvez utiliser la même commande que ci-dessus:

  • Le casting de l’URL class loader

La stratégie de chargement de classe que je viens de décrire est implémenté avec un nouveau type et, dans Java 9, le chargeur de classe d’application est de ce type. Cela signifie que ce n’est plus un URLClassLoader qui sera utilisé. Donc l’instruction occasionnelle (URLClassLoader) getClass (). GetClassLoader () ne sera plus exécutée. Il s’agit d’un autre exemple typique où Java 9 est rétrocompatible au sens strict (parce que c’est un URLCassLoader qui n’a jamais été spécifié) mais qui peut néanmoins entraîner des problèmes de migration.

Symptômes

Celui-ci est très évident. Vous obtiendrez une ClassCastException en se plaignant que le nouvel AppClassLoader n’est pas URLClassLoader:

 

Corrections 

Le chargeur de classe a probablement été lancé pour accéder aux méthodes spécifiques à URLClassLoader. Si c’est le cas, vos chances de faire une migration avec seulement de petits changements sont minces. Les seuls super types supportés (et donc accessibles) du nouveau AppClassLoader sont SecureClassLoader et ClassLoader et seules quelques méthodes ont été ajoutées à java 9. Prenez quand même le temps de regarder ce qu’ils proposent, ils pourraient faire ce que vous recherchez.

Nouvelle gestion des ressources java

Avec la modularité de JDK, les fichiers comme rt.jar, tools.jar et dt.jar ont disparu; Les classes JDK sont maintenant regroupées dans des fichiers jmod (un par module), un format de fichier sans précision. Ce qui permet des optimisations futures sans faire attention à la compatibilité ascendante. De plus, la distinction entre JRE et JDK a disparu également. Tout cela n’est plus spécifié dans la documentation officielle, mais cela ne signifie pas qu’il n’y ait aucun code là-bas en fonction de ces détails. En particulier, les outils tels que les IDE (bien que ceux-ci ont déjà été mis à jour ) auront des problèmes de compatibilité avec ces changements et commenceront à fonctionner de manière imprévisible, à moins qu’ils ne soient mis à jour. À la suite de ces modifications, l’URL que vous obtenez pour les ressources du système, par exemple de ClasLoader :: getSystemResource, sera modifié. Il s’agissait de la forme suivante: jar: file: $ javahome/ lib/rt.jar! $Path, où $ path est quelque chose comme java/lang/String.class. Il ressemble maintenant à jrt:/$module/$path. Bien sûr, toutes les API qui créent ou consomment de telles URL ont été mises à jour, mais les codes sources non-JDK qui utilisent ces URL doivent être mis à jour pour Java 9.

De plus, les méthodes Class :: getResource * et ClassLoader :: getResource * ne peuvent plus lire les ressources internes JDK. Utilisez plutôt Module :: getResourceAsStream pour accéder aux ressources internes du module ou créer un système de fichiers JRT comme suit:

 

Boot Class Path

Je suis un peu dans les eaux obscures ici parce que je n’ai jamais utilisé l’option -Xbootclasspath, qui a surtout supprimée. Apparemment, ses fonctionnalités sont remplacées par de nouvelles options en ligne de commande (paraphraser à partir de JEP 220 ici):

  •  l’option javac – – system peut être utilisée pour spécifier une autre source de modules système
  •  l’option javac – – release peut être utilisée pour spécifier une autre version de la plate-forme
  •  l’option java – -patch-module, mentionnée ci-dessus, peut être utilisée pour injecter du contenu dans des modules dans le graph du module initial.

Le format des nouvelles versions de java

Après plus de 20 ans, Java a finalement et officiellement accepté que le format de ses versions ne soit plus sur la forme 1.x. Hooray! Ainsi, à partir de Java 9, la propriété système java.version et ses frères et sœurs ne commencent plus avec 1.x mais avec x, c’est-à-dire 9 dans Java 9.

Symptômes

Il n’y a pas de symptômes clairs: à peu près tout pourrait se passer si une fonction d’utilité détermine la mauvaise version. Ce n’est pas trop difficile à trouver. Une recherche de texte complet pour les chaînes suivantes devrait conduire à tout le code spécifique à la chaîne de la version: java.version, java.runtime.version, java.vm.version, java.specification.version, java.vm.specification.version.

Corrections

Si vous êtes prêt à effectuer la migration votre projet sur Java 9, vous pouvez éviter toutes les insinuations et analyses des propriétés systèmes et, à la place, utiliser le nouveau type Runtime.Version. Si vous souhaitez rester compatible avec les version précédant Java 9, vous pouvez toujours utiliser la nouvelle API en créant un JAR à version multiple. Si cela est également hors de question, il semble que vous deviez écrire ducode (uch!) et créer une branche en fonction de la version majeure

Résumé

Maintenant, vous savez:

  1. Comment utiliser les API internes (–add-export et –add-opens), et s’assurer que les modules Java EE sont présents (–add-modules)
  2. Comment traiter les packages fractionnés (–patch -module).

Ces 2 points sont les problèmes les plus probables que vous rencontrerez lors d’une migration.

Les moins fréquents et moins faciles à corriger, car aucun accès au code problématique n’est disponible, sont:

  1. Les erreurs liées à l’ URLClassLoader,
  2. Les problèmes dus à la nouvelle gestions des URL  ressources java,
  3. Les -Xbootclasspath qui ont été retirés
  4. Les nouveaux format des versions java.

Savoir comment réparer cela vous donnera de très bonnes chances de surmonter tous vos défis de migration et de faire compiler et exécuter votre application sur Java 9. Sinon, consultez les sections Risques et Hypothèses de JEP 261, qui répertorie quelques autres pièges potentiels. Si vous êtes un peu débordé de tout cela, attendez mes prochains messages, qui donnent des conseils sur la manière de mettre en place ces correctifs individuels dans une stratégie de migration globale, par exemple en intégrant des outils de build et une intégration continue. Vous pouvez vous aider de ce livre. Il m’a été d’un grand secours.

 

 

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.