Le pattern de délégation Sling : étendre la logique Java des Core Components sans copier-coller
Comment modifier la logique du Sling Model d'un Core Component dans AEM — par exemple la façon dont le Teaser choisit son image — sans forker la classe, grâce à @Via(type = ResourceSuperType.class) de Sling et @Delegate de Lombok.
sling:resourceSuperType offre gratuitement l’héritage des scripts HTL :
vous étendez core/wcm/components/teaser/v2/teaser, vous surchargez le
seul bloc dont vous avez besoin, et Sling remonte la chaîne pour le reste
(voir
comment Sling résout une requête vers un composant AEM
pour le mécanisme complet). Mais sling:resourceSuperType ne change que
le script utilisé pour le rendu — il ne touche en rien la classe
Java qui se trouve derrière le composant. Si vous devez faire en sorte
que le Teaser choisisse une image de repli différente, ou que le composant
List trie ses éléments autrement, il faut modifier le Sling Model, et
sling:resourceSuperType seul ne suffit pas. C’est exactement à cela que
sert le pattern de délégation documenté par Adobe.
Pourquoi on ne peut pas simplement étendre le Sling Model du Core Component
Le réflexe naturel est d’hériter directement de l’implémentation du Core
Component — TeaserImpl, ListImpl, etc. Ne le faites pas. Ces classes
vivent dans des packages internal (par exemple
com.adobe.cq.wcm.core.components.internal.models.v2) qui ne font pas
partie de l’API publique exportée par les Core Components. Vous êtes
censé coder uniquement contre les interfaces de
com.adobe.cq.wcm.core.components.models — Teaser, List, Image,
etc. Même lorsqu’une classe d’implémentation est publique, l’étendre
n’est pas un contrat supporté : Adobe peut modifier des constructeurs, la
visibilité de champs, ou ajouter final dans une version mineure des
Core Components, car l’héritage n’a jamais été le point d’extension
prévu.
Le point d’extension supporté est l’interface publique, combinée à la
même chaîne sling:resourceSuperType que celle utilisée pour HTL. C’est
le pattern de délégation.
Le pattern de délégation : même resource, deux modèles
L’idée : vous écrivez votre propre Sling Model qui implémente la même
interface publique que le Core Component (par exemple Teaser), vous
l’enregistrez sur le resourceType de votre propre projet — le composant
proxy qui déclare déjà sling:resourceSuperType côté HTL — et vous
obtenez une instance du modèle original du Core Component pour la même
resource sous-jacente en adaptant via le resource super type.
Apache Sling Models dispose d’une annotation conçue exactement pour cela :
@Via(type = ResourceSuperType.class)
(org.apache.sling.models.annotations.via.ResourceSuperType). Appliquée
à un champ injecté avec @Self, elle enveloppe la resource (ou la
request) courante en substituant son type de resource par la valeur de
sling:resourceSuperType avant d’adapter — donc au lieu d’adapter à
nouveau la resource courante (ce qui provoquerait une récursion vers
votre propre modèle), elle adapte une copie typée comme le Core
Component, ce qui résout vers le Sling Model propre du 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;
À l’exécution, coreTeaser est exactement la même instance de
TeaserImpl (ou la classe interne utilisée par le Core Component) que
celle qui aurait été adaptée si votre composant proxy n’existait pas —
vous ne réimplémentez pas sa logique, vous l’enveloppez.
Éviter le boilerplate avec @Delegate de Lombok
Teaser étend Component et déclare une bonne dizaine de méthodes
(getTitle(), getPretitle(), getLink(), getImageResource(),
isActionsEnabled(), getActions(), et bien d’autres, en plus de tout ce
qui est hérité de Component). Écrire à la main une méthode de
redirection pour chacune d’elles juste pour en modifier une seule, c’est
exactement le copier-coller que ce pattern vise à éviter.
C’est là qu’intervient @Delegate de Lombok
(lombok.experimental.Delegate — elle se trouve dans le package
expérimental de Lombok, mais c’est l’outil standard utilisé par la
communauté AEM pour cela) : placée sur le champ coreTeaser, elle génère
à la compilation une méthode de redirection pour chaque méthode publique
de Teaser. Pour garder une méthode pour vous, vous l’excluez avec
@Delegate(excludes = ...), en pointant vers une petite interface
marqueur qui ne déclare que la ou les signatures que vous surchargez —
sinon, la méthode de redirection générée par Lombok et votre propre
@Override entrent en collision avec une erreur de compilation « méthode
dupliquée ».
Lombok lui-même est ajouté comme dépendance en scope provided sur le
bundle core — il n’est nécessaire qu’à la compilation, pour le traitement
des annotations, pas à l’exécution.
Un exemple complet : surcharger la façon dont le Teaser choisit son image
Teaser.getImageResource() (ajoutée dans Core Components 12.4.0) peut
retourner null lorsque le teaser n’a pas d’image propre et qu’aucun de
ses contenus liés n’en fournit une. Supposons que le design system du
projet impose qu’un teaser ne soit jamais rendu sans image — dans ce cas
on utilise un asset de repli partagé :
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;
/**
* Les méthodes listées ici sont exclues de la redirection générée
* par Lombok afin de pouvoir les surcharger ci-dessous sans erreur
* de compilation « méthode dupliquée ».
*/
private interface Overrides {
Resource getImageResource();
}
@Override
public Resource getImageResource() {
Resource image = coreTeaser.getImageResource();
return image != null
? image
: request.getResourceResolver().getResource(FALLBACK_IMAGE_PATH);
}
}
Toutes les autres méthodes de MyTeaser — getTitle(), getLink(),
isActionsEnabled(), getExportedType() héritée de Component, tout —
sont générées par Lombok et redirigent simplement vers coreTeaser. Vous
n’avez écrit que la seule méthode que vous deviez réellement modifier.
Pourquoi le script HTL n’a pas besoin de changer
Comme le script HTL du composant proxy est lui-même hérité sans
modification du Core Component (c’est le mécanisme
sling:resourceSuperType côté HTL), il contient toujours quelque chose
comme
data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" —
il adapte vers l’interface, pas vers une classe concrète. Sling Models
détermine quelle implémentation @Model enregistrée utiliser pour cette
interface en fonction du sling:resourceType réel de la resource, en
préférant la correspondance la plus proche lorsque plusieurs modèles
déclarent la même interface d’adaptation. Pour une resource dont le
sling:resourceType est myproject/components/teaser, c’est
MyTeaser — le script HTL hérité se met donc à afficher votre logique
d’image de repli sans qu’une seule ligne de markup ne change.
Erreurs courantes sur de vrais projets AEM
- Oublier
sling:resourceSuperTypesur le composant proxy. Sans lui,@Via(type = ResourceSuperType.class)n’a rien à adapter et le champ délégué revientnull. Par défaut,@SelfutiliseInjectionStrategy.DEFAULT, ce qui ne fait pas échouer l’activation du modèle — donc à moins de déclarer explicitement@Self(injectionStrategy = InjectionStrategy.REQUIRED), le modèle s’active normalement et chaque méthode déléguée lève uneNullPointerException, bien plus difficile à relier à unsling:resourceSuperTypemanquant. - Ne pas exclure la méthode surchargée de
@Delegate. Cela échoue à la compilation avec une erreur de méthode dupliquée — gênant, mais facile à repérer. L’erreur d’exécution ci-dessous est bien plus difficile à détecter en revue de code. - Supposer qu’une surcharge change la façon dont le délégué calcule ses
autres méthodes. La délégation, c’est de la composition, pas de
l’héritage :
coreTeaserest un objet distinct. SiTeaserImpl.getTitle()réutilisait en interneTeaserImpl.getLink(), surchargergetLink()dansMyTeaserne changerait rien à ce que retournecoreTeaser.getTitle()— ce sont deux objets différents. Si une propriété que vous modifiez alimente d’autres décisions de rendu dans l’implémentation d’origine, vérifiez si vous devez aussi surcharger ces méthodes liées, pas seulement celle qui semble évidente. - Sauter complètement le composant proxy et pointer le HTL directement
vers le
resourceTypedu Core Component. Sans votre propreresourceType, il n’y a rien contre quoi enregistrer votre@Model, et Sling n’a aucun moyen de préférer votre modèle à celui du Core Component — vous retombez dans l’erreur d’overlay déjà signalée par l’article sur la résolution desling:resourceType.