Salut !
En Java, je suis en train d’écrire une bibliothèque conçue pour être intégrée via maven-shade
. Concrètement, j’écris un SDK Sentry (basé sur le SDK sentry-log4j2
) pour une plateforme ayant certaines spécificités (Spigot, API Bukkit) et utilisant log4j2.
Concrètement, c’est une plateforme à laquelle on s’intègre via des plugins qui ont leur propre class loader. Je n’ai pas de contrôle sur l’installation elle-même (donc je ne peux utiliser la méthode normale de sentry-log4j2
en ajoutant un Appender
dans la configuration de log4j2 — il faut que je le fasse autrement, en contournant un peu, d’où le fait d’écrire un SDK réutilisable pour ne s’embêter qu’une fois).
Une autre implication de cette architecture est que je ne peux pas ajouter des dépendances comme ça au runtime. Il faut que je shade toutes les dépendances qui ne sont pas de base disponibles dans mon JAR qui sera chargé.
Avec ces contraintes en tête, voici ce que j’ai tenté. J’ai donc deux projets : sentry-bukkit
(mon SDK Sentry basé sur sentry-log4j2
), et un projet Bukkit l’utilisant. sentry-bukkit
contient quelques classes servant en gros à configurer le SDK sentry-log4j2
correctement, et derrière les API classiques du SDK Java de Sentry peuvent être utilisées (c’est également comme cela que fonctionnent les versions spécialisées de l’API Java de Sentry, d’ailleurs : elles configurent juste le SDK Java et l’intègrent à l’environnement).
Le POM de sentry-bukkit
ressemble à cela, en retirant les parties inutiles.
pom.xml
de sentry-bukkit
<!-- retiré : ids, version, repos & properties -->
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>io.sentry</groupId>
<artifactId>sentry-log4j2</artifactId>
<version>4.3.0</version>
<scope>compile</scope>
</dependency>
<!-- Il y a d'autres dépendances (bukkit, jetbrains annotation) mais ici osef -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet>
<includes>
<!-- Je suis obligé de shader sentry car pas dispo au runtime -->
<include>io.sentry:sentry</include>
<include>io.sentry:sentry-log4j2</include>
<!-- ceux là devraient l'être mais étrangement ça compile pas sans -->
<!-- peut-être à cause du classloader louche utilisé ? -->
<include>org.apache.logging.log4j:log4j-core</include>
<include>org.apache.logging.log4j:log4j-api</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>io.sentry</pattern>
<shadedPattern>fr.zcraft.sentrybukkit.sentry</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.logging.log4j</pattern>
<shadedPattern>fr.zcraft.sentrybukkit.log4j</shadedPattern>
</relocation>
</relocations>
<minimizeJar>true</minimizeJar>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Du côté des projets utilisant le SDK, je dois aussi shader le SDK pour la même raison : tout doit être dans le JAR du plugin, je ne peux pas toucher au runtime autrement. Ce POM est simplifié : en réalité je shade aussi une autre bibliothèque qui elle fonctionne très bien.
pom.xml
d’un projet utilisant sentry-bukkit
<dependencies>
<!-- ... d'autres deps ... -->
<dependency>
<groupId>fr.zcraft</groupId>
<artifactId>sentry-bukkit</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>true</minimizeJar>
<artifactSet>
<includes>
<include>fr.zcraft:sentry-bukkit</include>
</includes>
</artifactSet>
<relocations>
<relocation>
<pattern>fr.zcraft.sentrybukkit</pattern>
<shadedPattern>le.projet.quelconque.libs.sentrybukkit</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
J’utilise tout ça de cette façon, en gros.
SentryBukkit.init(this, "https://mon-dsn@o475316.ingest.sentry.io/123456");
// ...
try {
throw new Exception("This is a toast. Yummy!");
} catch (Exception e) {
Sentry.captureException(e);
}
// Ça fonctionnera aussi avec les exceptions non-attrapées
// en s'intégrant au système de logs, mais peu importe ici.
Puis je compile les deux successivement avec mvn clean install
. La première ligne surlignée fonctionne correctement, comme on peut s’y attendre. Par contre, la seconde échoue avec une belle erreur m’informant que la classe n’est pas trouvée.
java.lang.NoClassDefFoundError: io/sentry/Sentry
at fr.zcraft.Ping.Ping.onEnable(Ping.java:35) ~[?:?]
at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:263) ~[patched_1.16.3.jar:git-Paper-253]
at org.bukkit.plugin.java.JavaPluginLoader.enablePlugin(JavaPluginLoader.java:380) ~[patched_1.16.3.jar:git-Paper-253]
at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:483) ~[patched_1.16.3.jar:git-Paper-253]
at org.bukkit.craftbukkit.v1_16_R2.CraftServer.enablePlugin(CraftServer.java:501) ~[patched_1.16.3.jar:git-Paper-253]
at org.bukkit.craftbukkit.v1_16_R2.CraftServer.enablePlugins(CraftServer.java:415) ~[patched_1.16.3.jar:git-Paper-253]
at net.minecraft.server.v1_16_R2.MinecraftServer.loadWorld(MinecraftServer.java:468) ~[patched_1.16.3.jar:git-Paper-253]
at net.minecraft.server.v1_16_R2.DedicatedServer.init(DedicatedServer.java:237) ~[patched_1.16.3.jar:git-Paper-253]
at net.minecraft.server.v1_16_R2.MinecraftServer.w(MinecraftServer.java:939) ~[patched_1.16.3.jar:git-Paper-253]
at net.minecraft.server.v1_16_R2.MinecraftServer.lambda$a$0(MinecraftServer.java:177) ~[patched_1.16.3.jar:git-Paper-253]
at java.lang.Thread.run(Thread.java:832) [?:?]
Caused by: java.lang.ClassNotFoundException: io.sentry.Sentry
at java.net.URLClassLoader.findClass(URLClassLoader.java:435) ~[?:?]
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:171) ~[patched_1.16.3.jar:git-Paper-253]
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:100) ~[patched_1.16.3.jar:git-Paper-253]
at java.lang.ClassLoader.loadClass(ClassLoader.java:589) ~[?:?]
at java.lang.ClassLoader.loadClass(ClassLoader.java:522) ~[?:?]
... 11 more
Pourtant, en ouvrant le JAR du projet dépendant de sentry-bukkit
, on trouve bien le .class
qui va bien.
Je soupçonne donc que le souci soit lié au fait qu’on a ici deux relocalisations par Maven et que le second shade
ne re-transforme pas correctement le premier. La classe est bien là, mais Java ne sait pas la retrouver car il n’a plus l’info de la première relocalisation lors du second shading.
Si je dépends directement de sentry-log4j2
dans le projet utilisant le SDK (en le shadant aussi), ça fonctionne correctement. Mais ça implique de devoir dépendre des deux : c’est plus lourd à l’utilisation. Si possible, j’aimerais pouvoir ne dépendre que de mon SDK sentry-bukkit
sans avoir à ajouter une ou deux autres dépendances à également shader.
D’où ma question : est-ce possible de résoudre ce problème, ou faudra-t-il forcément demander aux utilsateurs de dépendre des deux bibliothèques ? Normalement, ça ne devrait pas poser de souci, mais le shading et la relocation cassent les pieds.
Ou alors peut-être est-ce possible de s’en passer, mais il me semble que c’est mieux pour éviter d’éventuels conflits avec d’autres plugins incluant le même SDK shadé… sachant que la classe doit être un singleton indépendant par plugin l’utilisant. Et qu’un tel cas d’usage multiple se produira forcément : lorsque ce sera au point, je compte bien l’utiliser pour tous mes projets du genre, histoire de tout centraliser sur Sentry (c’est tellement pratique).
À noter que si la solution est « utilise plutôt Gradle de la façon suivante », ça me va aussi, j’suis pas sectaire. Et s’il n’y en a pas… well, la documentation servira à ça mais s’il y a une solution plus jolie, je préfère .
Merci d’avoir lu ce bien trop long post, et merci d’avance pour toute idée !