Perché la tua classe Java non vede un'altra classe: il classloading dei bundle OSGi in AEM
Perché una classe che compila senza problemi con Maven può comunque lanciare una ClassNotFoundException in AEM, come Import-Package/Export-Package controllano la visibilità tra bundle, e come fare debug con la console web di Felix.
In una normale applicazione Java, una volta che un jar è sul classpath,
qualsiasi classe al suo interno può vedere qualsiasi altra classe presente
sul classpath. AEM non funziona così. AEM gira su Apache Felix, un
framework OSGi, e il livello di moduli di OSGi assegna a ogni bundle
distribuito (un jar OSGi) il proprio classloader. Due bundle che
convivono fianco a fianco in /system/console/bundles, anche se costruiti
nello stesso reactor Maven, non possono vedere le classi l’uno dell’altro
a meno che i metadati del bundle non lo dichiarino esplicitamente. Questa
è la causa più comune del classico “compila, si distribuisce, e poi lancia
ClassNotFoundException” in AEM.
Perché il classpath di Maven non è il classpath a runtime
Quando esegui mvn package su un progetto AEM, Maven risolve l’intero
albero delle dipendenze transitive e lo colloca su un unico classpath per
la compilazione e (se li hai) l’esecuzione dei test unitari. Quel
classpath non ha alcun concetto di bundle, import o export: è piatto.
Il runtime di AEM è un mondo diverso. Felix installa ogni bundle con il
proprio classloader e collega i bundle tra loro solo attraverso i pacchetti
che esportano e importano esplicitamente, così come dichiarato nel
manifest del bundle (META-INF/MANIFEST.MF). Una classe di bundle-a è
invisibile per bundle-b a meno che:
bundle-aesporti il pacchetto in cui vive quella classe (Export-Package), ebundle-bimporti lo stesso pacchetto (Import-Package), oppure includa direttamente la classe nel proprio jar.
Questo vale anche se entrambi i bundle provengono dallo stesso modulo Maven, dallo stesso build di reactor, o dalla stessa libreria “condivisa” del tuo team: la visibilità di Maven e la visibilità di OSGi sono due meccanismi indipendenti che si somigliano in superficie, ma non hanno niente a che vedere l’uno con l’altro.
Import-Package e Export-Package: il vero classpath in AEM
Ogni bundle OSGi porta con sé header del manifest che descrivono il suo contratto di dipendenze:
Export-Package: com.mysite.core.util;version="1.0.0"
Import-Package: org.apache.commons.lang3;version="[3.0,4)",*
Export-Package dice “altri bundle possono usare questi miei pacchetti.”
Import-Package dice “ho bisogno di questi pacchetti, forniti da
qualunque bundle li esporti, entro questo intervallo di versioni.” Quando
Felix avvia un bundle, cerca di collegare (wire) ogni voce
Import-Package con un Export-Package compatibile proveniente da un
altro bundle attivo. Se non ne trova uno, il bundle rimane nello stato
Installed invece di passare ad Active — oppure, per gli import
opzionali o dinamici, si attiva senza problemi e fallisce solo nel momento
esatto in cui il codice tocca il pacchetto mancante.
I progetti AEM attuali (l’aem-project-archetype è passato da tempo dal
maven-bundle-plugin) generano questi header con il bnd-maven-plugin,
costruito sullo stesso strumento bnd che usa Apache Sling stesso. A
differenza del vecchio Maven Bundle Plugin, il bnd-maven-plugin non
esporta tutti i pacchetti per impostazione predefinita: bisogna dichiararlo
esplicitamente, tipicamente annotando un pacchetto con
@org.osgi.annotation.bundle.Export in package-info.java:
// core/src/main/java/com/mysite/core/util/package-info.java
@org.osgi.annotation.bundle.Export
package com.mysite.core.util;
oppure con un’istruzione bnd esplicita Export-Package nel pom.xml del
modulo:
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
Import-Package viene solitamente lasciato all’analisi del bytecode fatta
da bnd stesso: scansiona le classi compilate per individuare tutti i
pacchetti effettivamente referenziati dal tuo codice e genera
automaticamente la lista degli import, risolvendo gli intervalli di
versione a partire dalle dipendenze dichiarate nel POM. Questo è comodo,
ma è esattamente il motivo per cui un export mancante altrove nel progetto
si manifesta come una sorpresa a runtime piuttosto che come un errore di
build: bnd può importare solo ciò che qualche bundle è disposto a
esportare.
Perché compila perfettamente e si rompe comunque a runtime
A Maven interessa solo se una classe è raggiungibile sul classpath di compilazione. Non ha idea se quella classe sarà visibile oltre il confine di un bundle una volta distribuita. Da qui questa sequenza molto comune:
- Aggiungi un metodo di utilità a un modulo
corecondiviso. - Lo chiami da un servlet, un Sling Model, o uno scheduled job dello stesso reactor Maven.
mvn installva a buon fine. I test unitari, che girano sul classpath piatto della JVM senza OSGi, passano.- Distribuisci su AEM. Il bundle si installa e persino si attiva, perché il riferimento al pacchetto può essere risolto in modo lazy.
- Il percorso di codice che chiama il metodo di utilità viene eseguito
per la prima volta — in una richiesta, in uno step di workflow, o in
uno scheduled job — e AEM lancia
NoClassDefFoundErroroClassNotFoundExceptionper una classe che, secondo ogni segnale di Maven, “esiste”.
Il divario sta nel collegamento Export-Package/Import-Package, non
nella compilazione. I test unitari non lo intercettano perché non girano
mai dentro Felix; girano su un classpath piatto dove le regole di
visibilità di OSGi semplicemente non si applicano.
Fare debug con la console web di Felix
La console web di Felix è lo strumento principale per diagnosticare questo
problema in AEM, sia in locale su author
(http://localhost:4502/system/console) sia in un ambiente di sviluppo
Cloud Service.
/system/console/bundles elenca tutti i bundle installati con il loro
stato. Un bundle bloccato su Installed (invece che Active) di solito
significa che Felix non è riuscito a soddisfare una delle sue voci
Import-Package. Entrando in un bundle specifico si vede il suo manifest
completo, incluse le sezioni Imported Packages ed Exported
Packages: gli import risolti mostrano quale bundle li sta fornendo;
quelli non risolti compaiono come insoddisfatti, il che ti dice subito che
nessun bundle attivo nel framework esporta attualmente quel pacchetto in
una versione compatibile.
/system/console/depfinder prende un nome di classe o pacchetto
completamente qualificato e ti dice quale bundle (o quale dipendenza
Maven, se non è presente da nessuna parte) lo fornisce. È il modo più
rapido per rispondere a “questa classe è disponibile da qualche parte in
questa istanza AEM, e con quale versione di pacchetto?” prima di andare a
inseguire un NoClassDefFoundError nel tuo stesso codice.
Tra i due: usa bundles per confermare il collegamento import/export del
tuo bundle, e depfinder per scoprire da dove dovrebbe effettivamente
arrivare una classe mancante.
Dove emerge questo problema in progetti AEM reali
- Un modulo di utilità “common” o “core” condiviso. Uno sviluppatore aggiunge una classe a un modulo condiviso, dimentica di esportare il pacchetto in cui vive (oppure il comportamento predefinito di bnd di non esportare la esclude silenziosamente), e ogni bundle consumatore che non è nello stesso build di reactor fallisce a runtime, anche se il build del reactor stesso compila senza problemi.
- Una dipendenza transitiva che non è essa stessa un bundle OSGi.
Molte librerie Java comuni non sono mai state costruite pensando a
OSGi e non hanno un proprio header
Export-Package. Se il tuo bundle ha bisogno di una delle loro classi, devi incorporarla (istruzioni tipo-includeresource/Embed-Dependencydibnd) oppure fornirla come bundle installato separatamente: averla solo come<dependency>Maven non basta, ed è esattamente il caso d’uso per cui esiste/system/console/depfinder. - Test unitari che danno una falsa sicurezza. Poiché i test girano al di fuori di Felix, un servizio può avere il 100% dei test verdi e comunque fallire non appena viene distribuito, se la classe da cui dipende non è realmente collegata a livello OSGi. Considera i test unitari verdi come prova che la logica è corretta, non come prova che il bundle si risolverà.