AEM Guide

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.

osgifelixclassloadingmavendebugging

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:

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:

  1. Añades un método de utilidad a un módulo core compartido.
  2. Lo llamas desde un servlet, un Sling Model o un scheduled job del mismo reactor de Maven.
  3. mvn install tiene éxito. Los tests unitarios, que corren sobre el classpath plano de la JVM sin OSGi de por medio, pasan.
  4. Despliegas en AEM. El bundle se instala e incluso se activa, porque la referencia al paquete puede resolverse de forma perezosa.
  5. 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 NoClassDefFoundError o ClassNotFoundException para 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