AEM Guide

Pourquoi votre classe Java ne voit pas une autre : le classloading des bundles OSGi dans AEM

Pourquoi une classe qui compile sans problème avec Maven peut quand même lever une ClassNotFoundException dans AEM, comment Import-Package/Export-Package contrôlent la visibilité entre bundles, et comment déboguer cela avec la console web Felix.

osgifelixclassloadingmavendebugging

Dans une application Java classique, dès qu’un jar est sur le classpath, n’importe quelle classe de ce jar peut voir n’importe quelle autre classe du classpath. AEM ne fonctionne pas ainsi. AEM tourne sur Apache Felix, un framework OSGi, et la couche de modules d’OSGi donne à chaque bundle déployé (un jar OSGi) son propre classloader. Deux bundles qui cohabitent dans /system/console/bundles, même construits dans le même reactor Maven, ne peuvent pas voir les classes l’un de l’autre à moins que les métadonnées du bundle ne le déclarent explicitement. C’est la cause la plus fréquente du fameux “ça compile, ça se déploie, puis ça lève une ClassNotFoundException” dans AEM.

Pourquoi le classpath Maven n’est pas le classpath d’exécution

Quand vous lancez mvn package sur un projet AEM, Maven résout tout l’arbre des dépendances transitives et le place sur un seul classpath pour la compilation et (si vous en avez) l’exécution des tests unitaires. Ce classpath n’a aucune notion de bundles, d’imports ou d’exports : il est plat.

Le runtime d’AEM est un monde différent. Felix installe chaque bundle avec son propre classloader et ne relie les bundles entre eux qu’à travers les paquets qu’ils exportent et importent explicitement, tels que déclarés dans le manifest du bundle (META-INF/MANIFEST.MF). Une classe du bundle-a est invisible pour bundle-b sauf si :

C’est vrai même si les deux bundles proviennent du même module Maven, du même build de reactor, ou de la même bibliothèque “partagée” de votre équipe : la visibilité Maven et la visibilité OSGi sont deux mécanismes indépendants qui se ressemblent en surface, mais n’ont rien à voir entre eux.

Import-Package et Export-Package : le vrai classpath dans AEM

Chaque bundle OSGi porte des en-têtes de manifest qui décrivent son contrat de dépendances :

Export-Package: com.mysite.core.util;version="1.0.0"
Import-Package: org.apache.commons.lang3;version="[3.0,4)",*

Export-Package signifie “les autres bundles peuvent utiliser ces paquets qui m’appartiennent.” Import-Package signifie “j’ai besoin de ces paquets, fournis par n’importe quel bundle qui les exporte, dans cette plage de versions.” Quand Felix démarre un bundle, il essaie de câbler (wire) chaque entrée Import-Package avec un Export-Package compatible provenant d’un autre bundle actif. S’il n’en trouve aucun, le bundle reste à l’état Installed au lieu de passer à Active — ou, pour les imports optionnels ou dynamiques, il s’active sans problème et échoue seulement au moment précis où le code touche le paquet manquant.

Les projets AEM actuels (l’aem-project-archetype a abandonné le maven-bundle-plugin il y a un moment) génèrent ces en-têtes avec le bnd-maven-plugin, construit sur le même outil bnd qu’utilise Apache Sling lui-même. Contrairement à l’ancien Maven Bundle Plugin, le bnd-maven-plugin n’exporte pas tous les paquets par défaut : il faut le déclarer explicitement, généralement en annotant un paquet avec @org.osgi.annotation.bundle.Export dans package-info.java :

// core/src/main/java/com/mysite/core/util/package-info.java
@org.osgi.annotation.bundle.Export
package com.mysite.core.util;

ou avec une instruction bnd explicite Export-Package dans le pom.xml du module :

<plugin>
  <groupId>biz.aQute.bnd</groupId>
  <artifactId>bnd-maven-plugin</artifactId>
</plugin>

Import-Package est généralement laissé à l’analyse de bytecode de bnd lui-même : il scanne les classes compilées pour repérer tous les paquets que votre code référence réellement et génère la liste d’imports automatiquement, en résolvant les plages de versions à partir des dépendances déclarées dans le POM. C’est pratique, mais c’est exactement pour cela qu’un export manquant ailleurs dans votre projet se manifeste comme une surprise à l’exécution plutôt que comme une erreur de build : bnd ne peut importer que ce qu’un bundle est prêt à exporter.

Pourquoi ça compile parfaitement et casse quand même à l’exécution

Maven se soucie uniquement de savoir si une classe est accessible sur le classpath de compilation. Il n’a aucune idée de si cette classe sera visible au-delà d’une frontière de bundle une fois déployée. D’où cette séquence très courante :

  1. Vous ajoutez une méthode utilitaire à un module core partagé.
  2. Vous l’appelez depuis un servlet, un Sling Model, ou un scheduled job du même reactor Maven.
  3. mvn install réussit. Les tests unitaires, qui tournent sur le classpath JVM classique sans OSGi, passent.
  4. Vous déployez sur AEM. Le bundle s’installe et s’active même, parce que la référence au paquet peut être résolue paresseusement.
  5. Le chemin de code qui appelle la méthode utilitaire s’exécute pour la première fois — dans une requête, une étape de workflow, ou un scheduled job — et AEM lève une NoClassDefFoundError ou une ClassNotFoundException pour une classe qui, selon tous les signaux Maven, “existe”.

L’écart se situe dans le câblage Export-Package/Import-Package, pas dans la compilation. Les tests unitaires ne le détectent pas car ils ne tournent jamais à l’intérieur de Felix ; ils tournent sur un classpath plat où les règles de visibilité OSGi ne s’appliquent tout simplement pas.

Déboguer cela avec la console web Felix

La console web Felix est l’outil principal pour diagnostiquer ce problème dans AEM, que ce soit en local sur author (http://localhost:4502/system/console) ou sur un environnement de développement Cloud Service.

/system/console/bundles liste tous les bundles installés avec leur état. Un bundle bloqué à Installed (au lieu de Active) signifie généralement que Felix n’a pas pu satisfaire l’une de ses entrées Import-Package. En cliquant sur un bundle précis, vous voyez son manifest complet, y compris les sections Imported Packages et Exported Packages : les imports résolus indiquent quel bundle les fournit ; ceux qui ne sont pas résolus apparaissent comme insatisfaits, ce qui vous indique immédiatement qu’aucun bundle actif du framework n’exporte actuellement ce paquet dans une version compatible.

/system/console/depfinder prend un nom de classe ou de paquet pleinement qualifié et vous indique quel bundle (ou quelle dépendance Maven, si elle n’est présente nulle part) le fournit. C’est le moyen le plus rapide de répondre à “cette classe est-elle disponible quelque part dans cette instance AEM, et sous quelle version de paquet ?” avant de partir traquer une NoClassDefFoundError dans votre propre code.

Entre les deux : utilisez bundles pour confirmer le câblage import/export de votre propre bundle, et depfinder pour trouver d’où une classe manquante devrait réellement provenir.

Où cela se manifeste dans de vrais projets AEM