AEM Guide

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-modelscore-componentsjavalombok

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.modelsTeaser, 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 MyTeasergetTitle(), 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