El patrón de delegación de Sling: extender la lógica Java de los Core Components sin copiar y pegar
Cómo cambiar la lógica del Sling Model de un Core Component en AEM — por ejemplo, cómo el Teaser elige su imagen — sin bifurcar la clase, usando @Via(type = ResourceSuperType.class) de Sling y @Delegate de Lombok.
sling:resourceSuperType te da herencia de scripts HTL gratis: extiendes
core/wcm/components/teaser/v2/teaser, sobrescribes el único bloque que
necesitas y Sling recorre la cadena para el resto (ver
cómo Sling resuelve una petición hasta un componente de AEM
para el mecanismo completo). Pero sling:resourceSuperType solo cambia
qué script renderiza — no toca en absoluto la clase Java que hay
detrás del componente. Si necesitas que el Teaser elija una imagen de
respaldo distinta, o que el componente List ordene los elementos de otra
forma, tienes que tocar el Sling Model, y sling:resourceSuperType por sí
solo no te lleva hasta ahí. Para eso existe el patrón de delegación
documentado por Adobe.
Por qué no puedes simplemente extender el Sling Model del Core Component
El instinto es heredar directamente de la implementación del Core
Component — TeaserImpl, ListImpl, etc. No lo hagas. Esas clases viven
en paquetes internal (por ejemplo
com.adobe.cq.wcm.core.components.internal.models.v2) que no forman parte
de la API pública exportada por los Core Components. Solo se espera que
programes contra las interfaces de
com.adobe.cq.wcm.core.components.models — Teaser, List, Image,
etc. Incluso cuando una clase de implementación resulta ser pública,
extenderla no es un contrato soportado: Adobe puede cambiar constructores,
visibilidad de campos o añadir final en una versión menor de los Core
Components, porque la herencia nunca fue el punto de extensión previsto.
El punto de extensión soportado es la interfaz pública, combinada con la
misma cadena de sling:resourceSuperType que se usa para HTL. Ese es el
patrón de delegación.
El patrón de delegación: el mismo resource, dos modelos
La idea: escribes tu propio Sling Model que implementa la misma interfaz
pública que el Core Component (por ejemplo Teaser), lo registras contra
el resourceType de tu propio proyecto — el componente proxy que ya
declara sling:resourceSuperType para el lado HTL — y obtienes una
instancia del modelo original del Core Component para el mismo resource
subyacente adaptando a través del resource super type.
Apache Sling Models tiene una anotación creada exactamente para esto:
@Via(type = ResourceSuperType.class)
(org.apache.sling.models.annotations.via.ResourceSuperType). Aplicada a
un campo inyectado con @Self, envuelve el resource (o la request)
actual sustituyendo su tipo de resource por el valor de
sling:resourceSuperType antes de adaptar — así que en lugar de adaptar
otra vez el resource actual (lo que caería en una recursión hacia tu
propio modelo), adapta una copia tipada como el Core Component, que
resuelve al Sling Model propio del Core Component:
<!-- /apps/myproject/components/teaser/.content.xml -->
<jcr:root
jcr:primaryType="cq:Component"
jcr:title="Teaser"
sling:resourceSuperType="core/wcm/components/teaser/v2/teaser"/>
@Self
@Via(type = ResourceSuperType.class)
private Teaser coreTeaser;
En tiempo de ejecución, coreTeaser es exactamente la misma instancia de
TeaserImpl (o la clase interna que use el Core Component) que se habría
adaptado si tu componente proxy no existiera — no estás reimplementando su
lógica, la estás envolviendo.
Evitar el boilerplate con @Delegate de Lombok
Teaser extiende Component y declara más de una docena de métodos
(getTitle(), getPretitle(), getLink(), getImageResource(),
isActionsEnabled(), getActions(), y más, además de todo lo heredado de
Component). Escribir a mano un método de reenvío para cada uno de ellos
solo para cambiar uno es exactamente el copia-y-pega que este patrón
existe para evitar.
Aquí es donde entra @Delegate de Lombok
(lombok.experimental.Delegate — está en el paquete experimental de
Lombok, pero es la herramienta estándar que usa la comunidad de AEM para
esto): colocada en el campo coreTeaser, genera un método de reenvío
para cada método público de Teaser en tiempo de compilación. Para
quedarte un método para ti mismo, lo excluyes con
@Delegate(excludes = ...), apuntando a una pequeña interfaz marcadora
que declara solo la(s) firma(s) que estás sobrescribiendo — de lo
contrario, el método de reenvío generado por Lombok y tu propio
@Override colisionan con un error de compilación de “método duplicado”.
Lombok en sí se añade como dependencia con scope provided en el bundle
core — solo se necesita en tiempo de compilación para el procesamiento de
anotaciones, no en tiempo de ejecución.
Un ejemplo completo: sobrescribir cómo el Teaser elige su imagen
Teaser.getImageResource() (añadido en Core Components 12.4.0) puede
devolver null cuando el teaser no tiene imagen propia y ninguno de sus
contenidos enlazados aporta una. Supongamos que el sistema de diseño del
proyecto exige que un teaser nunca se renderice sin imagen — en ese caso
se debe usar un asset de reserva compartido:
package com.myproject.core.models;
import com.adobe.cq.wcm.core.components.models.Teaser;
import lombok.experimental.Delegate;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.via.ResourceSuperType;
@Model(adaptables = SlingHttpServletRequest.class,
adapters = Teaser.class,
resourceType = MyTeaser.RESOURCE_TYPE)
public class MyTeaser implements Teaser {
static final String RESOURCE_TYPE = "myproject/components/teaser";
private static final String FALLBACK_IMAGE_PATH =
"/content/dam/myproject/defaults/teaser-fallback.png";
@Self
private SlingHttpServletRequest request;
@Self
@Via(type = ResourceSuperType.class)
@Delegate(excludes = Overrides.class)
private Teaser coreTeaser;
/**
* Los métodos listados aquí quedan excluidos del reenvío generado
* por Lombok, para poder sobrescribirlos abajo sin un error de
* compilación de "método duplicado".
*/
private interface Overrides {
Resource getImageResource();
}
@Override
public Resource getImageResource() {
Resource image = coreTeaser.getImageResource();
return image != null
? image
: request.getResourceResolver().getResource(FALLBACK_IMAGE_PATH);
}
}
Cualquier otro método de MyTeaser — getTitle(), getLink(),
isActionsEnabled(), getExportedType() heredado de Component, todo —
lo genera Lombok y simplemente reenvía a coreTeaser. Solo escribiste el
único método que realmente necesitabas cambiar.
Por qué el script HTL no necesita cambiar
Como el script HTL del componente proxy se hereda sin modificar del Core
Component (ese es el mecanismo de sling:resourceSuperType por el lado
HTL), sigue conteniendo algo como
data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" —
adapta a la interfaz, no a una clase concreta. Sling Models decide qué
implementación de @Model registrada usar para esa interfaz según el
sling:resourceType real del resource, prefiriendo la coincidencia más
cercana cuando más de un modelo declara la misma interfaz adaptadora.
Para un resource cuyo sling:resourceType es
myproject/components/teaser, eso es MyTeaser — así que el script HTL
heredado empieza a renderizar tu lógica de imagen de respaldo sin cambiar
una sola línea de markup.
Errores comunes en proyectos AEM reales
- Olvidar
sling:resourceSuperTypeen el componente proxy. Sin él,@Via(type = ResourceSuperType.class)no tiene nada de qué adaptar y el campo delegado vuelvenull. Por defecto@SelfusaInjectionStrategy.DEFAULT, que no hace fallar la activación del modelo por sí solo — así que a menos que declares explícitamente@Self(injectionStrategy = InjectionStrategy.REQUIRED), el modelo se activa sin problema y cada método delegado lanza unNullPointerException, algo mucho más difícil de rastrear hasta unsling:resourceSuperTypeausente. - No excluir el método sobrescrito de
@Delegate. Esto falla en tiempo de compilación con un error de método duplicado — molesto, pero fácil de detectar. El error de runtime de abajo es el difícil de pillar en revisión de código. - Asumir que una sobrescritura cambia cómo el delegado calcula sus
otros métodos. La delegación es composición, no herencia:
coreTeaseres un objeto distinto. SiTeaserImpl.getTitle()internamente reutilizaraTeaserImpl.getLink(), sobrescribirgetLink()enMyTeaserno cambiaría lo que devuelvecoreTeaser.getTitle()— serían dos objetos distintos. Si una propiedad que estás cambiando alimenta otras decisiones de renderizado dentro de la implementación original, comprueba si también necesitas sobrescribir esos métodos relacionados, no solo el que parece obvio. - Saltarte el componente proxy por completo y apuntar el HTL
directamente al
resourceTypedel Core Component. Sin tu propioresourceTypeno hay nada contra lo que registrar tu@Model, y Sling no tiene forma de preferir tu modelo sobre el del Core Component — vuelves al error de overlay que ya advierte el artículo sobre la resolución desling:resourceType.