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: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.models —
Teaser, 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 MyTeaser — getTitle(), 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
- Forgetting
sling:resourceSuperTypeon the proxy component. Without it,@Via(type = ResourceSuperType.class)has nothing to adapt from and the delegate field comes backnull. By default@SelfusesInjectionStrategy.DEFAULT, which won’t fail model activation on its own — so unless you explicitly set@Self(injectionStrategy = InjectionStrategy.REQUIRED), the model activates fine and every delegated method throws aNullPointerExceptioninstead, which is harder to trace back to a missingsling:resourceSuperType. - Not excluding the overridden method from
@Delegate. This fails at compile time with a duplicate method error — annoying, but easy to spot. It’s the runtime mistake below that’s harder to catch in review. - Assuming an override changes how the delegate computes its other
methods. Delegation is composition, not inheritance:
coreTeaseris a separate object. IfTeaserImpl.getTitle()internally reusedTeaserImpl.getLink(), overridinggetLink()onMyTeaserwould not change whatcoreTeaser.getTitle()returns — you’d be calling into two different objects. If a property you’re changing feeds other rendering decisions inside the original implementation, check whether you need to override those related methods too, not just the one that seems obvious. - Skipping the proxy component entirely and pointing HTL directly at the
Core Component’s
resourceType. Without your ownresourceType, there’s nothing for your@Modelto register against, and Sling has no way to prefer your model over the Core Component’s — you’re back to the overlay mistake the sibling article onsling:resourceTyperesolution already warns against.