Il pattern di delegation di Sling: estendere la logica Java dei Core Component senza copia-incolla
Come modificare la logica del Sling Model di un Core Component in AEM — ad esempio il modo in cui il Teaser sceglie la propria immagine — senza forkare la classe, usando @Via(type = ResourceSuperType.class) di Sling e @Delegate di Lombok.
sling:resourceSuperType regala l’ereditarietà degli script HTL: estendi
core/wcm/components/teaser/v2/teaser, sovrascrivi l’unico blocco di cui
hai bisogno e Sling risale la catena per il resto (vedi
come Sling risolve una richiesta verso un componente AEM
per il meccanismo completo). Ma sling:resourceSuperType cambia solo
quale script esegue il rendering — non tocca affatto la classe
Java dietro al componente. Se ti serve che il Teaser scelga
un’immagine di fallback diversa, o che il componente List ordini gli
elementi in un altro modo, devi mettere mano al Sling Model, e
sling:resourceSuperType da solo non ti porta fin lì. È esattamente per
questo che esiste il pattern di delegation documentato da Adobe.
Perché non puoi semplicemente estendere il Sling Model del Core Component
L’istinto è ereditare direttamente dall’implementazione del Core
Component — TeaserImpl, ListImpl e così via. Non farlo. Quelle classi
vivono in package internal (ad esempio
com.adobe.cq.wcm.core.components.internal.models.v2) che non fanno
parte dell’API pubblica esportata dai Core Component. Sei tenuto a
programmare solo contro le interfacce di
com.adobe.cq.wcm.core.components.models — Teaser, List, Image e
così via. Anche quando una classe di implementazione risulta pubblica,
estenderla non è un contratto supportato: Adobe può cambiare costruttori,
visibilità dei campi o aggiungere final in una versione minore dei Core
Component, perché l’ereditarietà non è mai stata il punto di estensione
previsto.
Il punto di estensione supportato è l’interfaccia pubblica, combinata con
la stessa catena di sling:resourceSuperType usata per HTL. Questo è il
pattern di delegation.
Il pattern di delegation: stessa resource, due model
L’idea: scrivi il tuo Sling Model che implementa la stessa interfaccia
pubblica del Core Component (ad esempio Teaser), lo registri sul
resourceType del tuo progetto — il componente proxy che dichiara già
sling:resourceSuperType lato HTL — e ottieni un’istanza del model
originale del Core Component per la stessa resource sottostante
adattando tramite il resource super type.
Apache Sling Models ha un’annotazione pensata esattamente per questo:
@Via(type = ResourceSuperType.class)
(org.apache.sling.models.annotations.via.ResourceSuperType). Applicata a
un campo iniettato con @Self, avvolge la resource (o la request)
corrente sostituendone il resource type con il valore di
sling:resourceSuperType prima di adattare — quindi invece di adattare di
nuovo la resource corrente (il che causerebbe una ricorsione verso il tuo
stesso model), adatta una copia tipizzata come il Core Component, che si
risolve nel Sling Model proprio 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;
A runtime, coreTeaser è esattamente la stessa istanza di TeaserImpl
(o della classe interna usata dal Core Component) che sarebbe stata
adattata se il tuo componente proxy non esistesse — non ne stai
reimplementando la logica, la stai avvolgendo.
Evitare il boilerplate con @Delegate di Lombok
Teaser estende Component e dichiara più di una dozzina di metodi
(getTitle(), getPretitle(), getLink(), getImageResource(),
isActionsEnabled(), getActions() e altri ancora, oltre a tutto ciò che
eredita da Component). Scrivere a mano un metodo di inoltro per ognuno
di essi solo per modificarne uno è esattamente il copia-incolla che
questo pattern esiste per evitare.
Qui entra in gioco @Delegate di Lombok (lombok.experimental.Delegate
— si trova nel package experimental di Lombok, ma è lo strumento
standard usato dalla community AEM per questo scopo): posizionata sul
campo coreTeaser, genera in fase di compilazione un metodo di inoltro
per ogni metodo pubblico di Teaser. Per tenere un metodo per te, lo
escludi con @Delegate(excludes = ...), puntando a una piccola interfaccia
marcatore che dichiara solo la firma (o le firme) che stai sovrascrivendo
— altrimenti il metodo di inoltro generato da Lombok e il tuo @Override
entrano in collisione con un errore di compilazione di “metodo
duplicato”.
Lombok stesso viene aggiunto come dipendenza con scope provided sul
bundle core — serve solo in fase di compilazione per l’elaborazione delle
annotazioni, non a runtime.
Un esempio completo: sovrascrivere come il Teaser sceglie la propria immagine
Teaser.getImageResource() (aggiunto in Core Components 12.4.0) può
restituire null quando il teaser non ha un’immagine propria e nessuno
dei suoi contenuti collegati ne fornisce una. Supponiamo che il design
system del progetto imponga che un teaser non venga mai renderizzato
senza immagine — in tal caso si ricorre a un asset di fallback
condiviso:
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;
/**
* I metodi elencati qui sono esclusi dall'inoltro generato da
* Lombok, così da poterli sovrascrivere qui sotto senza un errore
* di compilazione di "metodo duplicato".
*/
private interface Overrides {
Resource getImageResource();
}
@Override
public Resource getImageResource() {
Resource image = coreTeaser.getImageResource();
return image != null
? image
: request.getResourceResolver().getResource(FALLBACK_IMAGE_PATH);
}
}
Tutti gli altri metodi di MyTeaser — getTitle(), getLink(),
isActionsEnabled(), getExportedType() ereditato da Component, tutto
— sono generati da Lombok e si limitano a inoltrare a coreTeaser. Hai
scritto solo l’unico metodo che dovevi davvero cambiare.
Perché lo script HTL non deve cambiare
Poiché lo script HTL del componente proxy è a sua volta ereditato senza
modifiche dal Core Component (è il meccanismo di
sling:resourceSuperType lato HTL), continua a contenere qualcosa come
data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" —
adatta all’interfaccia, non a una classe concreta. Sling Models decide
quale implementazione @Model registrata usare per quell’interfaccia in
base al sling:resourceType effettivo della resource, preferendo la
corrispondenza più vicina quando più di un model dichiara la stessa
interfaccia adapter. Per una resource il cui sling:resourceType è
myproject/components/teaser, quella è MyTeaser — quindi lo script HTL
ereditato inizia a mostrare la tua logica di immagine di fallback senza
che una sola riga di markup cambi.
Errori comuni nei progetti AEM reali
- Dimenticare
sling:resourceSuperTypesul componente proxy. Senza di esso,@Via(type = ResourceSuperType.class)non ha nulla da adattare e il campo delegato tornanull. Per default@SelfusaInjectionStrategy.DEFAULT, che da solo non fa fallire l’attivazione del model — quindi, a meno di dichiarare esplicitamente@Self(injectionStrategy = InjectionStrategy.REQUIRED), il model si attiva senza problemi e ogni metodo delegato lancia unaNullPointerException, molto più difficile da ricondurre a unosling:resourceSuperTypemancante. - Non escludere il metodo sovrascritto da
@Delegate. Questo fallisce in fase di compilazione con un errore di metodo duplicato — fastidioso, ma facile da individuare. L’errore a runtime qui sotto è quello difficile da cogliere in fase di code review. - Supporre che una sovrascrittura cambi il modo in cui il delegato
calcola i suoi altri metodi. La delegation è composizione, non
ereditarietà:
coreTeaserè un oggetto distinto. SeTeaserImpl.getTitle()riutilizzasse internamenteTeaserImpl.getLink(), sovrascriveregetLink()inMyTeasernon cambierebbe ciò che restituiscecoreTeaser.getTitle()— sarebbero due oggetti diversi. Se una proprietà che stai modificando alimenta altre decisioni di rendering all’interno dell’implementazione originale, verifica se devi sovrascrivere anche quei metodi correlati, non solo quello che sembra ovvio. - Saltare del tutto il componente proxy e puntare l’HTL direttamente al
resourceTypedel Core Component. Senza un tuoresourceTypenon c’è nulla contro cui registrare il tuo@Model, e Sling non ha modo di preferire il tuo model a quello del Core Component — si ricade nell’errore di overlay già segnalato dall’articolo sulla risoluzione disling:resourceType.