AEM Guide

The Sling Delegation Pattern: Extending Core Component Java Logic Without Copy-Pasting It

How to change a Core Component's Sling Model logic in AEM — such as how the Teaser picks its image — without forking the class, using Sling's @Via(type = ResourceSuperType.class) and Lombok's @Delegate.

sling-modelscore-componentsjavalombok

sling:resourceSuperType gets you HTL script inheritance for free: extend core/wcm/components/teaser/v2/teaser, override the one block you need, and Sling walks up the chain for the rest (see how Sling resolves a request to an AEM component for the full mechanism). But sling:resourceSuperType only changes which script renders — it does nothing to the Java class backing the component. If you need the Teaser to pick a different fallback image, or the List component to sort items differently, you have to touch the Sling Model, and sling:resourceSuperType alone doesn’t get you there. This is what Adobe’s delegation pattern is for.

Why You Can’t Just Extend the Core Component’s Sling Model

The instinct is to subclass the Core Component’s implementation directly — TeaserImpl, ListImpl, and so on. Don’t. Those classes live in internal packages (e.g. com.adobe.cq.wcm.core.components.internal.models.v2) that aren’t part of the Core Components’ exported public API. You’re only meant to code against the interfaces in com.adobe.cq.wcm.core.components.modelsTeaser, List, Image, and so on. Even where an implementation class happens to be public, extending it isn’t a supported contract: Adobe is free to change constructors, field visibility, or add final in a minor Core Components release, because subclassing was never the intended extension point.

The supported extension point is the public interface, combined with the same sling:resourceSuperType chain used for HTL. That’s the delegation pattern.

The Delegation Pattern: Same Resource, Two Models

The idea: write your own Sling Model that implements the same public interface as the Core Component (e.g. Teaser), register it against your own project’s resourceType — the proxy component that already declares sling:resourceSuperType for the HTL side — and obtain an instance of the original Core Component model for the same underlying resource by adapting via the resource’s super type.

Apache Sling Models has a purpose-built annotation for exactly this: @Via(type = ResourceSuperType.class) (org.apache.sling.models.annotations.via.ResourceSuperType). Applied to an @Self-injected field, it wraps the current resource (or request) with its resource type swapped for the sling:resourceSuperType value before adapting — so instead of adapting the current resource again (which would recurse into your own model), it adapts a copy of it typed as the Core Component, which resolves to the Core Component’s own Sling Model:

<!-- /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;

At runtime, coreTeaser is the exact same TeaserImpl instance (or whatever internal class the Core Component uses) that would have been adapted if your proxy component didn’t exist — you’re not reimplementing its logic, you’re wrapping it.

Avoiding the Boilerplate with Lombok’s @Delegate

Teaser extends Component and declares over a dozen methods (getTitle(), getPretitle(), getLink(), getImageResource(), isActionsEnabled(), getActions(), and more, plus everything inherited from Component). Hand-writing a forwarding method for every one of them just to change a single method is exactly the copy-paste this pattern exists to avoid.

This is where Lombok’s @Delegate (lombok.experimental.Delegate — it’s in Lombok’s experimental package, but it’s the standard tool the AEM community uses for this) comes in: placed on the coreTeaser field, it generates a forwarding method for every public method of Teaser at compile time. To keep a method for yourself, exclude it with @Delegate(excludes = ...), pointing at a small marker interface that declares just the signature(s) you’re overriding — otherwise Lombok’s generated forwarding method and your own @Override collide with a “duplicate method” compile error.

Lombok itself is added as a provided-scope dependency on the core bundle — it’s only needed at compile time for annotation processing, not at runtime.

A Complete Example: Overriding How the Teaser Picks Its Image

Teaser.getImageResource() (added in Core Components 12.4.0) can return null when the teaser has no image of its own and none of its linked content provides one. Suppose the project’s design system says a teaser should never render without an image — fall back to a shared placeholder asset instead:

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;

    /**
     * Methods listed here are excluded from Lombok's generated
     * forwarding so we can override them below without a
     * "duplicate method" compile error.
     */
    private interface Overrides {
        Resource getImageResource();
    }

    @Override
    public Resource getImageResource() {
        Resource image = coreTeaser.getImageResource();
        return image != null
                ? image
                : request.getResourceResolver().getResource(FALLBACK_IMAGE_PATH);
    }
}

Every other method on MyTeasergetTitle(), getLink(), isActionsEnabled(), getExportedType() inherited from Component, all of it — is generated by Lombok and simply forwards to coreTeaser. You only wrote the one method you actually needed to change.

Why the HTL Script Doesn’t Need to Change

Because the proxy component’s HTL script is itself inherited unchanged from the Core Component (that’s the sling:resourceSuperType mechanism from the HTL side), it still contains something like data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" — it adapts to the interface, not to a concrete class. Sling Models resolves which registered @Model implementation to use for that interface based on the resource’s actual sling:resourceType, preferring the closest match when more than one model declares the same adapter interface. For a resource whose sling:resourceType is myproject/components/teaser, that’s MyTeaser — so the inherited HTL script starts rendering your fallback image logic without a single line of markup changing.

Common Mistakes on Real AEM Projects