Por qué tu clase Java no ve otra clase: classloading de bundles OSGi en AEM
Por qué una clase que compila sin problemas en Maven puede lanzar ClassNotFoundException en AEM, cómo Import-Package/Export-Package controlan la visibilidad entre bundles, y cómo depurarlo con la consola web de Felix.
En una aplicación Java normal, una vez que un jar está en el classpath,
cualquier clase de ese jar puede ver cualquier otra clase del classpath.
AEM no funciona así. AEM corre sobre Apache Felix, un framework OSGi, y la
capa de módulos de OSGi le da a cada bundle desplegado (un jar OSGi) su
propio classloader. Dos bundles que conviven uno al lado del otro en
/system/console/bundles, aunque se hayan construido en el mismo reactor
de Maven, no pueden ver las clases del otro a menos que los metadatos del
bundle lo digan explícitamente. Esta es la causa más común de “compiló,
se desplegó, y luego lanzó ClassNotFoundException” en AEM.
Por qué el classpath de Maven no es el classpath en tiempo de ejecución
Cuando ejecutas mvn package en un proyecto AEM, Maven resuelve todo el
árbol de dependencias transitivas y lo coloca en un único classpath para
compilar y (si los tienes) ejecutar los tests unitarios. Ese classpath no
tiene ningún concepto de bundles, imports o exports: es plano.
El runtime de AEM es un mundo distinto. Felix instala cada bundle con su
propio classloader y solo conecta bundles entre sí a través de los
paquetes que explícitamente exportan e importan, tal como se
declara en el manifest del bundle (META-INF/MANIFEST.MF). Una clase de
bundle-a es invisible para bundle-b a menos que:
bundle-aexporte el paquete donde vive esa clase (Export-Package), ybundle-bimporte ese mismo paquete (Import-Package), o incluya la clase directamente dentro de su propio jar.
Esto es cierto incluso si ambos bundles vienen del mismo módulo Maven, del mismo build de reactor, o de la misma librería “compartida” de tu equipo: la visibilidad de Maven y la visibilidad de OSGi son dos mecanismos independientes que se parecen en la superficie, pero no tienen nada que ver entre sí.
Import-Package y Export-Package: el verdadero classpath en AEM
Todo bundle OSGi lleva cabeceras de manifest que describen su contrato de dependencias:
Export-Package: com.mysite.core.util;version="1.0.0"
Import-Package: org.apache.commons.lang3;version="[3.0,4)",*
Export-Package dice “otros bundles pueden usar estos paquetes míos”.
Import-Package dice “necesito estos paquetes, suministrados por
cualquier bundle que los exporte, dentro de este rango de versiones”.
Cuando Felix arranca un bundle, intenta conectar (wire) cada entrada de
Import-Package con un Export-Package compatible de otro bundle activo.
Si no encuentra ninguno, el bundle se queda en estado Installed en lugar
de pasar a Active — o, para imports opcionales o dinámicos, se activa
sin problema y solo falla en el momento exacto en que el código toca el
paquete que falta.
Los proyectos AEM actuales (el aem-project-archetype dejó atrás
maven-bundle-plugin hace tiempo) generan estas cabeceras con el
bnd-maven-plugin, construido sobre la misma herramienta bnd que usa
el propio Apache Sling. A diferencia del antiguo Maven Bundle Plugin, el
bnd-maven-plugin no exporta todos los paquetes por defecto: tienes que
declararlo explícitamente, normalmente anotando un paquete con
@org.osgi.annotation.bundle.Export en package-info.java:
// core/src/main/java/com/mysite/core/util/package-info.java
@org.osgi.annotation.bundle.Export
package com.mysite.core.util;
o con una instrucción bnd explícita Export-Package en el pom.xml del
módulo:
<plugin>
<groupId>biz.aQute.bnd</groupId>
<artifactId>bnd-maven-plugin</artifactId>
</plugin>
Import-Package normalmente se deja al análisis de bytecode del propio
bnd: escanea las clases compiladas para ver qué paquetes referencia
realmente tu código y genera la lista de imports automáticamente,
resolviendo los rangos de versión a partir de las dependencias declaradas
en el POM. Esto es cómodo, pero es exactamente la razón por la que un
export que falta en otra parte del proyecto se manifiesta como una
sorpresa en runtime y no como un error de build: bnd solo puede importar
lo que algún bundle esté dispuesto a exportar.
Por qué compila perfectamente y aun así falla en runtime
A Maven solo le importa si una clase es alcanzable en el classpath de compilación. No tiene ni idea de si esa clase va a ser visible cruzando una frontera de bundle una vez desplegada. Por eso esta secuencia es tan habitual:
- Añades un método de utilidad a un módulo
corecompartido. - Lo llamas desde un servlet, un Sling Model o un scheduled job del mismo reactor de Maven.
mvn installtiene éxito. Los tests unitarios, que corren sobre el classpath plano de la JVM sin OSGi de por medio, pasan.- Despliegas en AEM. El bundle se instala e incluso se activa, porque la referencia al paquete puede resolverse de forma perezosa.
- La ruta de código que llama al método de utilidad se ejecuta por
primera vez —en una petición, un paso de workflow, o un scheduled
job— y AEM lanza
NoClassDefFoundErroroClassNotFoundExceptionpara una clase que, según toda señal de Maven, “existe”.
La brecha está en el cableado Export-Package/Import-Package, no en la
compilación. Los tests unitarios no lo detectan porque nunca corren
dentro de Felix; corren sobre un classpath plano donde las reglas de
visibilidad de OSGi sencillamente no aplican.
Depurarlo con la consola web de Felix
La consola web de Felix es la herramienta principal para diagnosticar esto
en AEM, tanto en local sobre author (http://localhost:4502/system/console)
como en un entorno de desarrollo de Cloud Service.
/system/console/bundles lista todos los bundles instalados con su
estado. Un bundle atascado en Installed (en lugar de Active)
normalmente significa que Felix no pudo satisfacer alguna de sus entradas
Import-Package. Entrando en un bundle concreto se ve su manifest
completo, incluyendo las secciones Imported Packages y Exported
Packages: los imports que se resolvieron muestran qué bundle los está
suministrando; los que no se resolvieron aparecen como insatisfechos, lo
que te dice de inmediato que ningún bundle activo del framework exporta
actualmente ese paquete con una versión compatible.
/system/console/depfinder recibe un nombre de clase o paquete
totalmente cualificado y te dice qué bundle (o qué dependencia Maven, si
no está presente en absoluto) lo proporciona. Es la forma más rápida de
responder “¿está disponible esta clase en algún sitio de esta instancia
de AEM, y bajo qué versión de paquete?” antes de perseguir un
NoClassDefFoundError por tu propio código.
Entre las dos: usa bundles para confirmar el cableado de imports/exports
de tu propio bundle, y depfinder para averiguar de dónde debería venir
realmente una clase que falta.
Dónde aparece esto en proyectos AEM reales
- Un módulo de utilidades “common” o “core” compartido. Un desarrollador añade una clase a un módulo compartido, olvida exportar el paquete donde vive (o el comportamiento por defecto de bnd de no exportar la excluye silenciosamente), y cualquier bundle consumidor que no esté en el mismo build de reactor falla en runtime aunque el propio build del reactor compile sin problemas.
- Una dependencia transitiva que no es en sí misma un bundle OSGi.
Muchas librerías Java normales nunca se construyeron pensando en OSGi y
no tienen su propia cabecera
Export-Package. Si tu bundle necesita una de sus clases, o bien la incluyes (instrucciones tipo-includeresource/Embed-Dependencydebnd) o la proporcionas como un bundle instalado aparte: tenerla solo como<dependency>de Maven no es suficiente, y este es exactamente el caso para el que existe/system/console/depfinder. - Tests unitarios que dan una falsa sensación de seguridad. Como los tests corren fuera de Felix, un servicio puede tener el 100% de sus tests en verde y aun así fallar en cuanto se despliega, si la clase de la que depende no está realmente cableada a nivel de OSGi. Trata los tests unitarios en verde como prueba de que la lógica es correcta, no como prueba de que el bundle va a resolver.