AEM Guide

Servicios OSGi en AEM: @Component, @Reference y su ciclo de vida

Cómo declara AEM un servicio OSGi con @Component, inyecta dependencias con @Reference (cardinality, policy, bind/unbind), y por qué el error más común ocurre en fugas de ResourceResolver dentro de @Activate.

osgideclarative-servicescomponentreferencelifecycle

Casi todo lo que hace AEM en el lado de servidor —un servlet, un Sling Model, un scheduler, un listener de eventos, un endpoint de administración— termina apoyándose en un servicio OSGi. @Component y @Reference son las dos anotaciones que declaran ese servicio y sus dependencias, y Felix Service Component Runtime (SCR) —el runtime de Declarative Services que usa AEM— es quien decide, en base a esas anotaciones, cuándo tu clase se instancia, cuándo se activa y cuándo se destruye. Entender ese ciclo de vida no es un ejercicio académico de OSGi: es lo que explica por qué un @Activate a veces no se ejecuta al arrancar el bundle, por qué una referencia @Reference puede quedarse sin satisfacer indefinidamente, y por qué el patrón más habitual de fuga de recursos en servicios AEM ocurre precisamente dentro del método de activación.

El patrón interfaz + implementación que espera AEM

El patrón que recomienda Adobe para un servicio OSGi en AEM es separar el contrato en dos archivos: una interfaz que define la API pública, anotada con @ProviderType para marcarla como un tipo publicado pensado para ser consumido desde otros bundles, y una clase de implementación anotada con @Component, que es la que realmente se registra como servicio:

@Component(
    service = { Activities.class }
)
public class ActivitiesImpl implements Activities {
    // implementación
}

El parámetro service de @Component le dice a Felix SCR qué interfaz (o interfaces) debe publicar en el registro de servicios OSGi; si lo omites, por defecto se publican todas las interfaces que la clase implementa directamente.

Este patrón interfaz + implementación tiene una trampa específica de AEM que no aparece hasta que despliegas: el paquete Java donde vive la interfaz necesita un package-info.java con una anotación @Version para que bnd lo exporte. Sin ese archivo, el paquete simplemente no se exporta y ningún otro bundle puede resolver esa interfaz, lo que se manifiesta como un error de “Cannot be resolved” en la consola de bundles de Felix, no como un fallo de compilación. Este es el mismo mecanismo de Export-Package/Import-Package que gobierna la visibilidad entre bundles en AEM en general; si además tu proyecto separa un bundle “API” (con la interfaz) de un bundle “core” (con la implementación) —algo habitual cuando otros equipos deben consumir el servicio—, necesitas que el bundle que defina la interfaz la exporte y que el que la use la importe, exactamente igual que con cualquier otra dependencia de clase.

Cuándo se activa realmente un componente

Declarative Services no instancia tu clase directamente cuando lees el código: construye el componente a partir de metadatos (las anotaciones se compilan a un XML de componente dentro del bundle) y sigue una secuencia de estados que Felix SCR gestiona por ti. Los tres métodos de ciclo de vida que vas a escribir en un servicio AEM son:

@Activate
protected void activate() {
    this.activities = new String[] {
        "Running", "Cycling", "Skateboarding"
    };
    log.info("Activated ActivitiesImpl with activities [ {} ]",
             String.join(", ", this.activities));
}

@Deactivate
protected void deactivate() {
    log.info("ActivitiesImpl has been deactivated!");
}

La especificación de Declarative Services (que es lo que Felix SCR implementa dentro de AEM) precisa cuándo ocurre exactamente esa activación: SCR activa una configuración de componente cuando el componente está habilitado y la configuración está satisfecha y se necesita una instancia. La desactivación ocurre en el sentido inverso: cuando el componente pasa a estar deshabilitado, la configuración deja de estar satisfecha, o la instancia deja de ser necesaria. Si tu @Activate no lanza una excepción, tienes la garantía de que @Deactivate se invocará más adelante para esa misma instancia.

Ese “se necesita una instancia” es la parte que sorprende a más desarrolladores AEM. El atributo immediate de @Component es false por defecto para cualquier componente que provea un servicio, lo que significa activación diferida: tu clase no se instancia ni se activa al arrancar el bundle, sino la primera vez que otro componente reclama el servicio a través del registro OSGi (por ejemplo, cuando alguien obtiene una referencia a él vía @Reference). Si esperas que el log de @Activate aparezca en cuanto el bundle pasa a Active en /system/console/bundles y no lo ves, casi siempre es esto: nadie ha pedido todavía el servicio. Puedes forzar activación inmediata con @Component(immediate = true), algo típico en servlets, schedulers o componentes que necesitan ejecutar lógica de arranque aunque nadie los consuma como servicio.

@Reference: cómo AEM resuelve las dependencias de un servicio

@Reference sobre un campo (o un método) le pide a SCR que inyecte otro servicio OSGi. El comportamiento por defecto ya cubre el caso más común —una dependencia obligatoria, de una sola instancia, resuelta antes de activar el componente— pero cada uno de sus atributos cambia ese comportamiento de forma bastante concreta:

Una referencia con cardinalidad MULTIPLE y política DYNAMIC es el caso en el que más falta hacen bind/unbind explícitos, porque el conjunto de servicios enlazados puede cambiar mientras el componente sigue activo. Este ejemplo, adaptado de los servicios de ejemplo de Adobe Consulting Services, muestra el patrón completo:

@Component(
    reference = {
        @Reference(
            name = "sampleService",
            service = SampleService.class,
            policy = ReferencePolicy.DYNAMIC,
            policyOption = ReferencePolicyOption.GREEDY,
            cardinality = ReferenceCardinality.MULTIPLE
        )
    }
)
public class SampleMultiReferenceServiceImpl implements SampleMultiReferenceService {

    private volatile Map<String, SampleService> sampleServices =
        new ConcurrentHashMap<String, SampleService>();

    @Override
    public final List<String> helloWorlds() {
        final List<String> results = new ArrayList<String>();
        for (final Map.Entry<String, SampleService> entry : sampleServices.entrySet()) {
            results.add(entry.getValue().helloWorld());
        }
        return results;
    }

    protected final void bindSampleService(final SampleService service,
                                            final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(SampleService.PROP_NAME), null);
        if (type != null) {
            this.sampleServices.put(type, service);
        }
    }

    protected final void unbindSampleService(final SampleService service,
                                              final Map<Object, Object> props) {
        final String type = PropertiesUtil.toString(props.get(SampleService.PROP_NAME), null);
        if (type != null) {
            this.sampleServices.remove(type, service);
        }
    }
}

Nota el ConcurrentHashMap: como bind/unbind se pueden invocar en cualquier momento del ciclo de vida del componente activo, y potencialmente desde un hilo distinto al que está usando el servicio, la estructura que guarda las referencias enlazadas tiene que ser thread-safe por tu cuenta. SCR no te da eso gratis; solo garantiza que se invocará bind/unbind una vez por cada servicio enlazado o desenlazado.

Una advertencia sobre el orden de los bind. Es habitual leer que, cuando un componente tiene varias referencias MULTIPLE, SCR invoca los métodos de bind “en orden alfabético”. Esa afirmación aparece en documentación general de Declarative Services, pero no está confirmada por el texto de la especificación OSGi Compendium ni por ninguna fuente específica de Felix SCR (la implementación que realmente corre dentro de AEM). No escribas lógica que dependa de un orden concreto de invocación entre bind de referencias distintas: si tu servicio necesita procesar las dependencias en un orden determinado, ordénalas tú explícitamente después de la inyección en lugar de confiar en el orden de llamada.

@Reference en un @Component no es @OSGiService en un Sling Model

Un error de principiante habitual es intentar usar @Reference dentro de una clase anotada con @Model de Sling Models, esperando que se comporte igual que en un @Component. No es así: Sling Models tiene su propio mecanismo de inyección de servicios OSGi, la anotación @OSGiService (injector osgi-services), que es independiente de @Reference y funciona con cualquier tipo adaptable —request, resource, lo que sea que tu modelo adapte—:

@Model(adaptables = SlingJakartaHttpServletRequest.class)
public class MyModel {
    @OSGiService
    private PrintWriter out;
}

@OSGiService admite un filter opcional con sintaxis LDAP, equivalente al target de @Reference, para inyectar solo los servicios que cumplen cierta condición:

@OSGiService(filter = "paths=/bin/something")
private List<Servlet> servlets;

Cuando el punto de inyección es un List/Collection parametrizado, Sling Models obtiene el array de servicios que coinciden y lo envuelve en una List inmutable — el equivalente funcional a una referencia MULTIPLE en un @Component, pero resuelto por el framework de Sling Models en lugar de por Felix SCR directamente. Si tu clase es un @Component, usa @Reference; si es un @Model, usa @OSGiService. No son intercambiables entre sí.

El error más común: fugas de recursos dentro de @Activate

La mayoría de servicios AEM necesitan acceder al repositorio bajo su propia identidad, no bajo la de una petición HTTP. El patrón estándar es inyectar un ResourceResolverFactory y pedirle un ResourceResolver de servicio:

@Reference
ResourceResolverFactory rrf;

try (ResourceResolver resourceResolver = rrf.getResourceResolver(authInfo)) {
    // trabajar con resourceResolver
} catch (LoginException e) {
    // manejar el fallo de login
}

Desde AEM 6.2, ResourceResolver implementa AutoCloseable, así que el try-with-resources anterior es la forma recomendada de garantizar el cierre. Si tu código todavía necesita el patrón manual (por ejemplo por compatibilidad con una versión anterior), la alternativa equivalente es:

ResourceResolver resourceResolver = null;
try {
    resourceResolver = rrf.getResourceResolver(authInfo);
    // hacer el trabajo
} finally {
    if (resourceResolver != null) {
        resourceResolver.close();
    }
}

Cualquier ResourceResolver (o Session de JCR) que hayas obtenido directamente —dentro de @Activate, de un método de negocio, de un bind— es tuyo: tienes que cerrarlo siempre, en un finally o equivalente. Esto es distinto de los resolvers que vienen ya gestionados por el framework, como el que devuelve slingRequest.getResourceResolver() en una petición, que no debes cerrar tú. Este artículo sobre fugas de ResourceResolver en AEM entra en más detalle sobre este patrón fuera del contexto de servicios OSGi; dentro de un @Activate la regla es la misma, solo que el resolver suele durar más tiempo (mientras el componente esté activo) si lo guardas como campo en lugar de pedirlo y cerrarlo en cada operación.

Para diagnosticar una fuga ya en producción, la consola JMX de AEM (/system/console/jmx) expone objetos SessionStatistics; un número alto de instancias vivas —Adobe cita más de 500 como señal de alerta observada en entornos AEM 6.4/6.5— apunta a sesiones que se abrieron y nunca se cerraron. Para localizar el código responsable, se inspeccionan las instancias con IDs más recientes (las últimas sesiones creadas que siguen abiertas) y se lee su stack trace, buscando paquetes de la aplicación propia en lugar de código core de AEM.

Un segundo detalle específico de AEM que evita fallos de login en @Activate: cuando un servicio necesita resolver explícitamente su propia identidad —en lugar del authInfo genérico del ejemplo anterior—, Sling expone ResourceResolverFactory.getServiceResourceResolver(Map<String, Object> authenticationInfo), que resuelve un ResourceResolver a partir del mapeo de usuario de servicio (ServiceUserMapper de Sling) configurado para el bundle. Ese mapeo puede no estar disponible todavía cuando tu componente se activa por primera vez, por ejemplo justo después de un despliegue, y en ese caso getServiceResourceResolver lanza LoginException. El patrón habitual para evitarlo es añadir una @Reference al servicio marcador ServiceUserMapped correspondiente a tu mapeo, de forma que SCR no active tu componente hasta que la configuración de mapeo exista.

Anotaciones Felix SCR heredadas: no des la interoperabilidad por hecha

Si trabajas en un proyecto AEM con historia, es probable que te encuentres con las anotaciones antiguas de Felix SCR (@scr.component, @Property al estilo previo a Declarative Services estándar) conviviendo con @Component/@Reference. Hay blogs técnicos sobre AEM 6.4 que afirman que la migración es sencilla y que las anotaciones SCR antiguas siguen funcionando junto a las nuevas. Ninguna fuente oficial actual de Experience League confirma esa compatibilidad para AEM as a Cloud Service, que además exige un runtime Java y un stack OSGi más modernos que los de AEM 6.4. Si mantienes código con anotaciones Felix SCR heredadas, no asumas que la interoperabilidad está garantizada en tu versión concreta: verifícalo en tu propio entorno o, mejor, migra por completo a @Component/@Reference en lugar de mezclar ambos estilos en el mismo bundle.

Dónde aparece esto en un proyecto AEM real

Recursos