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.
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!");
}
@Activatese invoca cuando el componente pasa a estar activo.@Modifiedse invoca cuando cambia la configuración OSGi del componente, sin necesidad de desactivarlo y reactivarlo por completo.@Deactivatese invoca cuando el componente se desactiva.
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:
cardinality:MANDATORY(1..1, el valor por defecto),OPTIONAL(0..1),MULTIPLE(0..n) oAT_LEAST_ONE(1..n). Si el campo es unCollection/List, la cardinalidadMULTIPLEse infiere automáticamente del tipo; si es unjava.util.Optional, se infiereOPTIONAL.policy:STATIC(por defecto) oDYNAMIC. Una referencia estática debe estar resuelta antes de activar el componente, y si el servicio del que depende desaparece mientras está en uso, SCR fuerza la desactivación y reactivación del componente para volver a resolverla. Una referencia dinámica permite que SCR cambie el conjunto de servicios enlazados sin desactivar la instancia.DYNAMICse infiere automáticamente si el campo esvolatile— una regla fácil de pasar por alto: declarar un campovolatilesin querer política dinámica puede sorprenderte.policyOption:RELUCTANT(por defecto) oGREEDY, que controla si SCR prefiere quedarse con el servicio ya enlazado o cambiar al de mayor ranking cuando aparece uno mejor.target: un filtro LDAP opcional que restringe qué instancias del servicio satisfacen la referencia, equivalente a filtrar por propiedades de servicio.bind/unbind/updated: los nombres de los métodos que SCR invoca al enlazar, desenlazar o recibir una actualización de propiedades del servicio referenciado. Un método de bind puede declarar, entre otras combinaciones, un parámetro del tipo de la interfaz referenciada seguido opcionalmente de unjava.util.Mapcon las propiedades de servicio — el patrón más común en código AEM real.
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
- Un
@Activateque “nunca se ejecuta” tras el despliegue. Casi siempre es elimmediate = falsepor defecto: el componente provee un servicio y nadie lo ha reclamado todavía. Si necesitas lógica de arranque garantizada, usaimmediate = true. - Un componente que se desactiva y reactiva sin motivo aparente. Si
usa referencias
STATIC(el valor por defecto) hacia servicios que aparecen y desaparecen con frecuencia —por ejemplo, servicios registrados por otro bundle que se reinicia—, cada ciclo de ese servicio fuerza un ciclo completo de desactivación/reactivación de tu componente. Si el caso de uso lo tolera, una referenciaDYNAMICevita ese coste. - Fugas de
ResourceResolverque solo aparecen bajo carga. El patrón más común es unResourceResolverobtenido en@Activateo en un método de negocio que se guarda como campo de instancia y se reutiliza entre invocaciones en lugar de cerrarse tras cada uso — la sesión subyacente queda viva hasta que el recolector de basura la detecta, lo que en una instancia con tráfico real puede acumular cientos de sesiones filtradas. LoginExceptionintermitente justo después de desplegar. Suele deberse a que el componente se activó antes de que el mapeo de usuario de servicio existiera; referenciarServiceUserMappedresuelve esta carrera de forma declarativa en lugar de con reintentos manuales.