Fichiers JAR à plusieurs versions - mauvais ou bon?


Depuis un traducteur: nous travaillons activement à la traduction de la plate - forme sur des rails Java 11 et réfléchissons à la façon de développer efficacement des bibliothèques Java (telles que YARG ), en tenant compte des fonctionnalités de Java 8/11 afin que vous n'ayez pas à créer des branches et des versions distinctes. Une solution possible est un JAR à plusieurs versions, mais tout n'est pas fluide.


Java 9 comprend une nouvelle option d'exécution Java appelée JAR à plusieurs versions. C'est peut-être l'une des innovations les plus controversées de la plateforme. TL; DR: nous trouvons que c'est une solution tordue à un problème grave . Dans cet article, nous expliquerons pourquoi nous le pensons et vous expliquerons également comment créer un tel JAR si vous le souhaitez vraiment.


Les JARs multi-versions , ou MR JARs, est une nouvelle fonctionnalité de la plate-forme Java, introduite dans JDK 9. Nous allons décrire ici en détail les risques importants associés à l'utilisation de cette technologie, et comment créer des JARs multi-versions en utilisant Gradle, si vous voulez toujours.


En fait, un JAR à plusieurs versions est une archive Java qui comprend plusieurs variantes de la même classe pour travailler avec différentes versions du runtime. Par exemple, si vous travaillez dans JDK 8, l'environnement Java utilisera la version de classe pour JDK 8 et si dans Java 9, la version pour Java 9 est utilisée. De même, si la version est créée pour une future version de Java 10, le runtime utilise cette version au lieu de la version pour Java 9 ou la version par défaut (Java 8).


Sous le chat, nous comprenons l'appareil du nouveau format JAR et voyons si c'est tout.


Quand utiliser des fichiers JAR à plusieurs versions


  • Runtime optimisé. Il s'agit d'une solution au problème auquel de nombreux développeurs sont confrontés: lors du développement d'une application, on ne sait pas dans quel environnement elle sera exécutée. Cependant, pour certaines versions du runtime, vous pouvez incorporer des versions génériques de la même classe. Supposons que vous souhaitiez afficher le numéro de version de Java dans lequel l'application s'exécute. Pour Java 9, vous pouvez utiliser la méthode Runtime.getVersion. Cependant, il s'agit d'une nouvelle méthode disponible uniquement dans Java 9+. Si vous avez besoin d'autres runtimes, par exemple Java 8, vous devez analyser la propriété java.version. En conséquence, vous aurez 2 implémentations différentes d'une fonction.
  • API en conflit: la résolution des conflits entre les API est également un problème courant. Par exemple, vous devez prendre en charge deux runtimes, mais dans l'un d'eux, l'API est obsolète. Il existe 2 solutions courantes à ce problème:


    1. Le premier est la réflexion. Par exemple, vous pouvez spécifier l'interface VersionProvider, puis 2 classes spécifiques Java8VersionProvider et Java9VersionProvider, et charger la classe correspondante dans le runtime (c'est drôle que pour choisir entre deux classes, vous devez analyser le numéro de version!). L'une des options de cette solution consiste à créer une classe unique avec différentes méthodes appelées à l'aide de la réflexion.
    2. Une solution plus avancée consiste à utiliser des poignées de méthode lorsque cela est possible. Très probablement, la réflexion vous paraîtra inhibitrice et inconfortable, et, en général, telle qu'elle est.


Alternatives connues aux JAR à plusieurs versions


La deuxième façon, plus simple et plus compréhensible, est de créer 2 archives différentes pour des runtimes différents. En théorie, vous créez deux implémentations de la même classe dans l'EDI, et les compiler, les tester et les emballer correctement dans 2 artefacts différents est la tâche du système de génération. Il s'agit d'une approche qui a été utilisée à Guava ou Spock depuis de nombreuses années. Mais il est également requis pour des langues comme Scala. Et tout cela parce qu'il y a tellement d'options pour le compilateur et le runtime que la compatibilité binaire devient presque inaccessible.


Mais il existe de nombreuses autres raisons d'utiliser des archives JAR distinctes:


  • JAR est juste un moyen d'emballage.

c'est un artefact d'assemblage qui inclut des classes, mais ce n'est pas tout: les ressources, en règle générale, sont également incluses dans l'archive. L'emballage, comme la gestion des ressources, a un prix. L'équipe Gradle vise à améliorer la qualité de construction et à réduire le temps d'attente pour le développeur pour compiler les résultats, les tests et le processus de construction en général. Si l'archive apparaît trop tôt dans le processus, un point de synchronisation inutile est créé. Par exemple, pour compiler des classes dépendantes de l'API, la seule chose nécessaire est les fichiers .class. Ni les archives du pot ni les ressources du pot ne sont nécessaires. De même, seuls les fichiers et les ressources Grade sont nécessaires pour exécuter les tests Gradle. Pour les tests, il n'est pas nécessaire de créer un pot. Il ne sera nécessaire que par un utilisateur externe (c'est-à-dire lors de la publication). Mais si la création d'un artefact devient obligatoire, certaines tâches ne peuvent pas être exécutées en parallèle et l'ensemble du processus d'assemblage est inhibé. Si pour les petits projets ce n'est pas critique, pour les grands projets d'entreprise c'est le principal facteur de ralentissement.


  • il est beaucoup plus important que, étant un artefact, une archive jar ne puisse pas transporter d'informations sur les dépendances.

Il est peu probable que les dépendances de chaque classe dans Java 9 et Java 8 soient identiques. Oui, dans notre exemple simple, ce sera le cas, mais pour les grands projets, ce n'est pas vrai: généralement, l'utilisateur importe le backport de la bibliothèque pour la fonctionnalité Java 9 et l'utilise pour implémenter la version de la classe Java 8. Cependant, si vous emballez les deux versions dans une archive, dans un artefact il y aura des éléments avec différents arbres de dépendances. Cela signifie que si vous travaillez avec Java 9, vous avez des dépendances qui ne seront jamais nécessaires. De plus, il pollue le chemin de classe, créant des conflits probables pour les utilisateurs de bibliothèque.


Et enfin, dans un projet, vous pouvez créer des fichiers JAR à différentes fins:


  • pour l'API
  • pour java 8
  • pour java 9
  • avec liaison native
  • etc.

Une utilisation incorrecte du classificateur de dépendances entraîne des conflits liés au partage du même mécanisme. Habituellement, les sources ou javadocs sont installés en tant que classificateurs, mais en réalité, ils n'ont pas de dépendances.


  • Nous ne voulons pas générer d'incohérences; le processus de génération ne doit pas dépendre de la façon dont vous obtenez les classes. En d'autres termes, l'utilisation de fichiers JAR à plusieurs versions a un effet secondaire: appeler depuis l'archive JAR et appeler depuis le répertoire de classe sont maintenant des choses complètement différentes. Ils ont une énorme différence en sémantique!
  • Selon l'outil que vous utilisez pour créer le JAR, vous pouvez vous retrouver avec des archives JAR incompatibles! Le seul outil qui garantit que lorsque vous empaquetez deux options de classe dans une archive, elles auront une seule API ouverte - l'utilitaire jar lui-même. Ce qui, non sans raison, n'implique pas nécessairement des outils d'assemblage ou même des utilisateurs. JAR est essentiellement une «enveloppe» ressemblant à une archive ZIP. Ainsi, selon la façon dont vous le collectez, vous obtiendrez différents comportements, ou peut-être collecterez-vous un artefact incorrect (et vous ne le remarquerez pas).

Des moyens plus efficaces pour gérer les archives JAR individuelles


La raison principale pour laquelle les développeurs n'utilisent pas d'archives distinctes est qu'elles sont peu pratiques à collecter et à utiliser. Les outils de construction sont à blâmer, qui, avant Gradle, ne s'en sortaient pas du tout. En particulier, ceux qui ont utilisé cette méthode dans Maven ne pouvaient compter que sur la fonction de classificateur faible pour publier des artefacts supplémentaires. Cependant, le classificateur n'aide pas dans cette situation difficile. Ils sont utilisés à diverses fins, de la publication de codes sources, de la documentation, des javadocs, à l'implémentation d'options de bibliothèque (guava-jdk5, guava-jdk7, ...) ou à divers cas d'utilisation (api, fat jar, ...). En pratique, il n'y a aucun moyen de montrer que l'arbre de dépendances du classifieur est différent de l'arbre de dépendances du projet principal. En d'autres termes, le format POM est fondamentalement cassé car Il représente la façon dont le composant est assemblé et les artefacts qu'il fournit. Supposons que vous ayez besoin d'implémenter 2 archives jar différentes: classique et fat jar, qui inclut toutes les dépendances. Maven décide que 2 artefacts ont des arbres de dépendance identiques, même si c'est manifestement faux! Dans ce cas, cela est plus qu'évident, mais la situation est la même qu'avec les JAR à plusieurs versions!


La solution est de gérer correctement les options. Gradle peut le faire en gérant les dépendances en fonction des options. Cette fonctionnalité est actuellement disponible pour le développement sur Android, mais nous travaillons également sur sa version pour Java et les applications natives!


La gestion des dépendances basée sur les variantes est basée sur le fait que les modules et les artefacts sont des choses complètement différentes. Le même code peut parfaitement fonctionner dans différents temps d'exécution, en tenant compte de différentes exigences. Pour ceux qui travaillent avec la compilation native, cela est évident depuis longtemps: nous compilons pour i386 et amd64 et ne pouvons en aucun cas interférer avec les dépendances de la bibliothèque i386 avec arm64 ! Dans le contexte de Java, cela signifie que pour Java 8, vous devez créer une version de l'archive JAR «java 8», où le format de classe correspondra à Java 8. Cet artefact contiendra des métadonnées avec des informations sur les dépendances à utiliser. Pour Java 8 ou 9, les dépendances correspondant à la version seront sélectionnées. C'est aussi simple que cela (en fait, la raison n'est pas que le runtime n'est qu'un champ d'options, vous pouvez en combiner plusieurs).


Bien sûr, personne ne l'avait fait auparavant en raison d'une complexité excessive: Maven, apparemment, ne permettrait jamais qu'une opération aussi compliquée soit effectuée. Mais avec Gradle c'est possible. L'équipe Gradle travaille actuellement sur un nouveau format de métadonnées qui indique aux utilisateurs l'option à utiliser. Autrement dit, un outil de construction doit gérer la compilation, les tests, le conditionnement et le traitement de ces modules. Par exemple, le projet devrait fonctionner dans les runtimes Java 8 et Java 9. Idéalement, vous devez implémenter 2 versions de la bibliothèque. Cela signifie qu'il existe 2 compilateurs différents (pour éviter d'utiliser l'API Java 9 lorsque vous travaillez en Java 8), 2 répertoires de classe et, finalement, 2 archives JAR différentes. Et aussi, très probablement, il sera nécessaire de tester 2 runtimes. Ou vous implémentez 2 archives, mais décidez de tester le comportement de la version Java 8 dans le runtime Java 9 (car cela peut arriver au démarrage!).


Ce schéma n'a pas encore été mis en œuvre, mais l'équipe Gradle a fait des progrès significatifs dans cette direction.


Comment créer un JAR à plusieurs versions à l'aide de Gradle


Mais si cette fonction n'est pas encore prête, que dois-je faire? Détendez-vous, les artefacts corrects sont créés de la même manière. Avant que la fonction ci-dessus n'apparaisse dans l'écosystème Java, il y a deux options:


  • bonne vieille méthode en utilisant la réflexion ou différentes archives JAR;
  • utilisez des fichiers JAR à plusieurs versions (notez que cela peut être une mauvaise solution, même s'il existe de bons cas d'utilisation).

Quoi que vous choisissiez, des archives différentes ou des fichiers JAR à plusieurs versions, le schéma sera le même. Les fichiers JAR à plusieurs versions sont essentiellement le mauvais emballage: ils devraient être une option, mais pas l'objectif. Techniquement, la disposition source est la même pour les fichiers JAR individuels et externes. Ce référentiel décrit comment créer un JAR à plusieurs versions à l'aide de Gradle. L'essence du processus est brièvement décrite ci-dessous.


Tout d'abord, vous devez toujours vous souvenir d'une mauvaise habitude des développeurs: ils exécutent Gradle (ou Maven) en utilisant la même version de Java sur laquelle il est prévu de lancer des artefacts. De plus, parfois une version ultérieure est utilisée pour lancer Gradle, et la compilation se produit avec un niveau d'API antérieur. Mais il n'y a aucune raison particulière de le faire. A Gradle, la compilation de Ross est possible. Il vous permet de décrire la position du JDK, ainsi que d'exécuter la compilation en tant que processus distinct, pour compiler le composant à l'aide de ce JDK. La meilleure façon de configurer divers JDK est de configurer le chemin d'accès au JDK via des variables d'environnement, comme cela est fait dans ce fichier . Il vous suffit ensuite de configurer Gradle pour utiliser le bon JDK, en fonction de la compatibilité avec la plate-forme source / cible . Il convient de noter qu'à partir de JDK 9, les versions précédentes de JDK ne sont pas nécessaires pour la compilation croisée. Cela fait une nouvelle fonctionnalité, -release. Gradle utilise cette fonction et configurera le compilateur selon les besoins.


Un autre point clé est l' ensemble des sources de désignation. L'ensemble source est un ensemble de fichiers source qui doivent être compilés ensemble. Un JAR est obtenu en compilant un ou plusieurs ensembles source. Pour chaque ensemble, Gradle crée automatiquement une tâche de compilation personnalisée appropriée. Cela signifie que s'il existe des sources pour Java 8 et Java 9, ces sources seront dans des ensembles différents. C'est exactement ainsi que cela fonctionne dans l'ensemble source pour Java 9 , dans lequel il y aura une version de notre classe. Cela fonctionne vraiment, et vous n'avez pas besoin de créer un projet séparé, comme dans Maven. Mais surtout, cette méthode vous permet d'affiner la compilation de l'ensemble.


L'une des difficultés d'avoir différentes versions d'une classe est que le code de classe est rarement indépendant du reste du code (il a des dépendances avec des classes qui ne sont pas dans l'ensemble principal). Par exemple, son API peut utiliser des classes qui n'ont pas besoin de sources spéciales pour prendre en charge Java 9. En même temps, je ne voudrais pas recompiler toutes ces classes communes et emballer leurs versions pour Java 9. Ce sont des classes communes, elles doivent donc exister séparément de classes pour un JDK spécifique. Nous l'avons configuré ici : ajoutez une dépendance entre l'ensemble source pour Java 9 et l'ensemble principal, de sorte que lors de la compilation de la version pour Java 9, toutes les classes communes restent dans le chemin de classe de compilation.


L'étape suivante est simple : vous devez expliquer à Gradle que l'ensemble principal de sources se compilera avec le niveau d'API Java 8 et l'ensemble pour Java 9 avec le niveau Java 9.


Tout ce qui précède vous aidera à utiliser les deux approches mentionnées précédemment: implémenter des archives JAR distinctes ou des JAR à plusieurs versions. Puisque la publication porte sur ce sujet, regardons un exemple de comment obtenir Gradle pour construire un JAR à plusieurs versions:


jar { into('META-INF/versions/9') { from sourceSets.java9.output } manifest.attributes( 'Multi-Release': 'true' ) } 

Ce bloc décrit: le conditionnement des classes pour Java 9 dans le répertoire META-INF / versions / 9 , qui est utilisé pour les JAR MR, et la définition de l'étiquette à plusieurs versions dans le manifeste.


Et voilà, votre premier MR JAR est prêt!


Mais malheureusement, le travail n'est pas terminé. Si vous avez travaillé avec Gradle, vous savez que lorsque vous utilisez le plug-in d'application, vous pouvez exécuter l'application directement via la tâche d' exécution . Cependant, étant donné que Gradle essaie généralement de réduire la quantité de travail, la tâche d' exécution doit utiliser à la fois les répertoires de classe et les répertoires des ressources traitées. Pour les fichiers JAR à plusieurs versions, c'est un problème car les fichiers JAR sont nécessaires immédiatement! Par conséquent, au lieu d'utiliser le plug-in, vous devrez créer votre propre tâche , et c'est un argument contre l'utilisation de JAR à plusieurs versions.


Enfin et surtout, nous avons mentionné que nous aurions besoin de tester 2 versions de la classe. Pour cela, vous ne pouvez utiliser que des VM dans un processus distinct, car il n'y a pas d'équivalent du marqueur -release pour le runtime Java. L'idée est qu'un seul test doit être écrit, mais il sera exécuté deux fois: en Java 8 et Java 9. C'est le seul moyen de s'assurer que les classes spécifiques à l'exécution fonctionnent correctement. Par défaut, Gradle crée une tâche de test et utilise les répertoires de classe de la même manière au lieu du JAR. Par conséquent, nous allons faire deux choses: créer une tâche de test pour Java 9 et configurer les deux tâches afin qu'elles utilisent le JAR et les runtimes Java spécifiés. L'implémentation ressemblera à ceci:


 test { dependsOn jar def jdkHome = System.getenv("JAVA_8") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println "$name runs test using JDK 8" } } task testJava9(type: Test) { dependsOn jar def jdkHome = System.getenv("JAVA_9") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println classpath.asPath println "$name runs test using JDK 9" } } check.dependsOn(testJava9) 

Maintenant, lorsque la tâche démarre, vérifiez que Gradle compilera chaque ensemble de sources à l'aide du JDK souhaité, créera un JAR à plusieurs versions, puis exécutera les tests à l'aide de ce JAR sur les deux JDK. Les futures versions de Gradle vous aideront à le faire de manière plus déclarative.


Conclusion


Pour résumer. Vous avez appris que les fichiers JAR à plusieurs versions sont une tentative de résoudre le vrai problème auquel sont confrontés de nombreux développeurs de bibliothèques. Cependant, cette solution au problème semble être incorrecte. Gestion correcte des dépendances, liaison des artefacts et des options, souci des performances (la possibilité d'exécuter autant de tâches que possible en parallèle) - tout cela fait de MR JAR une solution pour les pauvres. Ce problème peut être résolu correctement à l'aide des options. Et pourtant, alors que la gestion des dépendances à l'aide des options de Gradle est en cours de développement, les fichiers JAR à plusieurs versions sont très pratiques dans les cas simples. Dans ce cas, cet article vous aidera à comprendre comment faire cela et comment la philosophie de Gradle diffère de Maven (ensemble source vs projet).


Enfin, nous ne nions pas qu'il existe des cas dans lesquels les JAR multi-versions ont du sens: par exemple, quand on ne sait pas dans quel environnement l'application sera exécutée (pas une bibliothèque), mais c'est plutôt une exception. Dans cet article, nous avons décrit les principaux problèmes rencontrés par les développeurs de bibliothèques et comment les JAR multi-versions tentent de les résoudre. Modélisation correcte des dépendances car les options améliorent les performances (grâce à un parallélisme à granularité fine) et réduisent les coûts de maintenance (évitant une complexité imprévue) par rapport aux JAR à plusieurs versions. Dans votre situation, des JAR MR peuvent également être nécessaires, donc Gradle a déjà pris soin de cela. Jetez un œil à cet exemple de projet et essayez-le par vous-même.

Source: https://habr.com/ru/post/fr428868/


All Articles