Dépendance shadée d'une dépendance shadée introuvable

Les joies d'un double-usage de maven-shading et d'une toute mignonne java.lang.NoClassDefFoundError

a marqué ce sujet comme résolu.

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
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.

Le `.class` existe dans le JAR final, pas de souci
Le .class existe dans le JAR final, pas de souci

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 :p .

Merci d’avoir lu ce bien trop long post, et merci d’avance pour toute idée !

+0 -0

Je n’ai jamais vraiment joué avec shade donc peut-être que cette réponse ne sera pas pertinente mais je veux être sûr de bien comprendre. Le message d’erreur semble se plaindre que io.sentry.Sentry et ça m’a l’air correct. Tu as bien un fichier Sentry.class dans ton jar, mais son chemin n’est pas io/sentry/Sentry.class donc la JVM considère ne parvient pas à le trouver sur son classpath. Est-ce que maven shade est censé changer tes imports pour utiliser le nouveau chemin?

Sur stackoverflow, quelqu’un propose d’utiliser shadedArtifactAttached mais je ne suis pas certain que ça aide ici.

Le message d’erreur semble se plaindre que io.sentry.Sentry [n’existe pas] et ça m’a l’air correct. Tu as bien un fichier Sentry.class dans ton jar, mais son chemin n’est pas io/sentry/Sentry.class donc la JVM considère ne parvient pas à le trouver sur son classpath. Est-ce que maven shade est censé changer tes imports pour utiliser le nouveau chemin?

Migwel

(J’ai ajouté entre crochets un bout de phrase que je pense manquant, corrige-moi si j’ai mal interprété)

Oui, c’est ce à quoi sert la partie relocations de la configuration de maven-shade.

Ceci :

<relocations>
    <relocation>
        <pattern>io.sentry</pattern>
        <shadedPattern>fr.zcraft.sentrybukkit.sentry</shadedPattern>
    </relocation>
</relocations>

…va renommer tous les imports de io.sentry vers fr.zcraft.sentrybukkit.sentry lors de la construction du JAR (ce qui peut se voir dans certaines exceptions, d’ailleurs).

Le souci semble être que ce premier renommage n’est pas connu de la seconde compilation (qui utilise sentry-bukkit comme dépendance… ce qui fait que sauf à déclarer la dépendance explicitement (ce qui peut se faire mais bon c’est pas super propre), la JVM ne trouve plus ses petits.

Après, peu importe la méthode, tant que mon objectif est rempli, à savoir :

  • pouvoir dépendre d’un JAR avec le moins d’extras possible ;
  • qui apporte une autre dépendance avec lui (sentry-log4j2) ;
  • dépendance qui doit être dans le JAR car impossible de modifier l’environnement d’exécution par un autre biais.

Sur stackoverflow, quelqu’un propose d’utiliser shadedArtifactAttached mais je ne suis pas certain que ça aide ici.

Migwel

Malheureusement ça ne semble pas aider. En réalité c’est limite pire, sans que je ne comprenne trop pourquoi (ce que ça fait n’est pas super clair dans ma tête) : je n’arrive plus à utiliser Sentry depuis sentry-bukkit (qui en dépend donc directement) ^^ .

[12:37:04 ERROR]: [org.bukkit.craftbukkit.v1_16_R2.CraftServer] io/sentry/Sentry$OptionsConfiguration initializing Ping v1.2.1 (Is it up to date?)
java.lang.NoClassDefFoundError: io/sentry/Sentry$OptionsConfiguration
        at fr.zcraft.Ping.libs.sentrybukkit.SentryBukkit.init(SentryBukkit.java:100) ~[?:?]
        at fr.zcraft.Ping.Ping.onLoad(Ping.java:16) ~[?:?]
        at org.bukkit.craftbukkit.v1_16_R2.CraftServer.loadPlugins(CraftServer.java:394) ~[patched_1.16.3.jar:git-Paper-253]
        at net.minecraft.server.v1_16_R2.DedicatedServer.init(DedicatedServer.java:204) ~[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$OptionsConfiguration
        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) ~[?:?]
        ... 7 more

Si jamais, le code source complet du SDK et du projet utilisateur sont dispos, depuis.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte