Avec Java9, sont arrivés les modules, et les Multi-Release jars, plus connus sous le nom de MR Jars.
L’idée est la suivante : à partir de Java9, des classes du JDK sont supprimées – ou rendues inaccessibles. Si on veut un jar qui puisse être utilisé avec Java8, Java9, Java10, Java11, Java12, il faut donc avoir des classes ayant le même nom, mais différentes en fonction de la version de Java, qui seront stockées dans le jar sous une arborescence particulière. Cet article explique comment s’y prendre pour créer un tel jar avec Maven.
La structure des classes dans un MR Jar est la suivante :
module-info.class
com
+- oxiane
| +- blog
| +- mrjar
| +- MaClasse.class
+- META-INF
+- versions
+- 10
| +- com
| +- oxiane
| +- blog
| +- mrjar
| +- MaClasse.class
+- 11
+- com
+- oxiane
+- blog
+- mrjar
+- MaClasse.class
On s’aperçoit que MaClasse.class apparaît 3 fois dans le jar, une fois à la racine, compilée pour Java8 ou avant, une fois sous META-INF/versions/10 compilée pour Java10, et une fois sous META-INF/versions/11 compilée pour Java11.
Première chose, organiser les sources. Pour ma part, j’ai trois parties :
- une partie commune à toutes les versions de Java
- une partie valable uniquement pour Java11
- une partie valable uniquement pour Java8
J’ai donc créé l’arborescence suivante :
src
+- main
+- java
| +- module-info.java
| +- com
| +- oxiane
| +- blog
| +- mrjar
| +- StarterBean.java
+- java8
| +- com
| +- oxiane
| +- blog
| +- mrjar
| +- StarterBeanBuilder.java
+- java11
+- com
+- oxiane
+- blog
+- mrjar
+- StarterBeanBuilder.java
Ici, StarterBean est commun quelle que soit la version de Java utilisée (il n’y a que des properties, des setters, des getters), mais StarterBeanBuilder est différent en Java8 et en Java11. En effet, dans la partie Java11, dans les commentaires de sérialisation, je veux faire apparaître le nom du module, qui n’existe pas en Java8.
Maintenant, il faut compiler tout cela, et produire les classes dans la bonne arborescence. J’utilise la version 3.8.1 du maven-compiler-plugin.
Dans le maven-compiler-plugin, il existe un attribut non documenté qui permet de spécifier l’emplacement des sources. Nous allons utiliser cette fonctionnalité pour spécifier les sources à inclure. Nous allons aussi utiliser l’option release de javac, qui permet de spécifier en une fois la version des sources, la version de la cible, et la version du bytecode à utiliser.
Nous définissons plusieurs exécutions :
- l’une pour compiler en Java11, release=11, multiReleaseOutput=true, utilisant src/main/java et src/main/java11
- une seconde pour compiler en Java8, release=8, utilisant src/main/java et src/main/java8
- enfin une dernière, qui désactive le comportement par défaut de ce que fait le plugin.
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>jdk11-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>11</release>
<multiReleaseOutput>true</multiReleaseOutput>
<compileSourceRoots>
<compileSourceRoot>src/main/java</compileSourceRoot>
<compileSourceRoot>src/main/java11</compileSourceRoot>
</compileSourceRoots>
</configuration>
</execution>
<execution>
<id>jdk8-compile</id>
<phase>compile</phase>
<goals><goal>compile</goal></goals>
<configuration>
<release>8</release>
<compileSourceRoots>
<compileSourceRoot>src/main/java</compileSourceRoot>
<compileSourceRoot>src/main/java8</compileSourceRoot>
</compileSourceRoots>
<excludes>
<exclude>module-info.java</exclude>
</excludes>
</configuration>
</execution>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
</executions>
<configuration>
<configuration>
<release>11</release>
</configuration>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
Ceci permet de compiler les classes, et des les positionner aux bons endroits dans target/classes
Ensuite, il reste à construire le jar. Pour que la JVM comprenne qu’un jar est un MR Jar, il faut définir un attribut particulier dans le manifest: Multi-Release. C’est ce que nous faisons ici.
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
</plugin>
Et voilà !
Dans la vraie vie, c’est un peu plus compliqué : il faut aussi définir la façon dont on compile les tests-unitaires, et surtout la façon dont on les exécute. A cause de limitations dans le maven-surefire-plugin, on ne peut pas exécuter des tests unitaires avec une structure de MR Jar. On est donc obligé de définir un profil de test Java8, un profil de test Java11, et de lancer plusieurs fois l’exécution de maven. Et souvent, pour produire le livrable (en Java8 et 11), on est obligé de désactiver les tests unitaires. Cela se fait très bien dans un pipeline Jenkins, mais c’est en dehors du sujet de cet article de blog !
Amusez-vous bien avec les MR Jar, ils sont très utiles lorsque vous développez de nouvelles librairies qui doivent pouvoir être utilisées par les applications anciennes, comme par les nouvelles, développées nativement en Java11+.
Christophe