AEM Guide

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.

osgifelixclassloadingmavendebugging

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:

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:

  1. Aggiungi un metodo di utilità a un modulo core condiviso.
  2. Lo chiami da un servlet, un Sling Model, o uno scheduled job dello stesso reactor Maven.
  3. mvn install va a buon fine. I test unitari, che girano sul classpath piatto della JVM senza OSGi, passano.
  4. Distribuisci su AEM. Il bundle si installa e persino si attiva, perché il riferimento al pacchetto può essere risolto in modo lazy.
  5. 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 NoClassDefFoundError o ClassNotFoundException per 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