4.1 Sling Models Framework, Injectors & Model Exporter
Key Takeaways
- Sling Models are annotation-driven POJOs or interfaces that map JCR resources or HTTP requests to Java objects, eliminating boilerplate JCR API calls and manual type casting.
- The choice of adaptables (Resource.class vs SlingHttpServletRequest.class) dictates injector scope: request-adapted models access request attributes, query parameters, and scripting variables, whereas resource-adapted models are lighter and decoupled from the HTTP layer.
- Specialized injector annotations (@ValueMapValue, @ChildResource, @Self, @OSGiService, @RequestAttribute, @ScriptVariable) replace generic @Inject to provide deterministic, compile-safe, and high-performance injection.
- Setting defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL on @Model prevents complete model adaptation failure when authored properties are missing in the JCR repository.
- The Sling Model Exporter framework utilizes Jackson serialization (@Exporter(name = "jackson", extensions = "json")) to expose structured content via the .model.json selector for Headless CMS and SPA Editor integration.
4.1 Sling Models Framework, Injectors & Model Exporter
Exam Focus: Apache Sling Models represent the standard backend programming model for Adobe Experience Manager Sites. The AD0-E128 exam tests your mastery of
@Modelconfiguration parameters (adaptables,adapters,defaultInjectionStrategy), specialized injector annotations (@ValueMapValue,@ChildResource,@Self,@OSGiService,@RequestAttribute,@ScriptVariable), post-construction lifecycle management (@PostConstruct), the Sling Model Exporter (@Exporterwith Jackson for.model.json), and comprehensive unit testing using AEM Mocks (AemContext).
Architectural Philosophy of Apache Sling Models
Historically, AEM developers extracted content from the Java Content Repository (JCR) using low-level JCR APIs (javax.jcr.Node, javax.jcr.Property) or Apache Sling ValueMap decorators in JSP or Java Use-API classes (WCMUsePojo). This legacy paradigm forced developers to write repetitive, error-prone boilerplate:
- Explicit type casting and null checks (
if (properties.get("title", String.class) != null) ...). - Manual traversal of child nodes and sub-resources.
- Tightly coupled scripting contexts and unhandled repository exceptions.
- Inability to unit test presentation logic without running a full OSGi integration container.
Introduced to solve these challenges, Apache Sling Models is an annotation-driven Object-to-Content Mapping (OCM) framework. Sling Models are standard Plain Old Java Objects (POJOs) or interfaces that bind repository resources or HTTP requests to strongly typed Java instances via reflection and runtime injectors. By offloading resource resolution, type coercion, and service lookup to the framework, developers achieve clean separation of concerns, testability, and enterprise-grade maintainability.
The @Model Annotation & Core Parameters
Every Sling Model class or interface must be annotated with @Model (org.apache.sling.models.annotations.Model). The annotation accepts several critical parameters that define how and where the model can be instantiated.
1. adaptables: Resource.class vs SlingHttpServletRequest.class
The adaptables parameter defines what context object can be adapted into the model:
adaptables = Resource.class: The model adapts directly from anorg.apache.sling.api.resource.Resource.adaptables = SlingHttpServletRequest.class: The model adapts from anorg.apache.sling.api.SlingHttpServletRequest.
| Architectural Attribute | adaptables = Resource.class | adaptables = SlingHttpServletRequest.class |
|---|---|---|
| Context Scope | Scoped strictly to the JCR resource node. | Scoped to the active HTTP request context. |
| Available Injectors | @ValueMapValue, @ChildResource, @Self (Resource), @OSGiService. | All resource injectors plus @RequestAttribute, @ScriptVariable, request parameters, headers, cookies, selectors, and suffix. |
| HTL Usage | <div data-sly-use.m="MyModel"> (adapts current resource). | <div data-sly-use.m="MyModel"> (adapts current request). |
| Off-Request Adaptability | Can be adapted anywhere: Sling Jobs, OSGi Event Listeners, Workflow Process steps, scheduled tasks. | Cannot be adapted outside an active HTTP request (will return null). |
| Unit Testing Overhead | Minimal; requires only mock resources in AemContext. | Requires mock HTTP request, response, attributes, and request parameters. |
| Architectural Recommendation | Default choice: Use unless HTTP request data or HTL arguments are explicitly needed. | Required when reading HTL template parameters, query parameters, or scripting objects like currentPage. |
2. adapters & Interface-Driven Design
Enterprise AEM development mandates separating public interface contracts from internal implementation classes:
package com.myproject.core.models;
public interface Teaser {
String getTitle();
String getDescription();
String getLinkUrl();
boolean isActionEnabled();
}
The implementation class implements the interface and declares it in the adapters array:
package com.myproject.core.models.impl;
import com.myproject.core.models.Teaser;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
@Model(
adaptables = Resource.class,
adapters = Teaser.class,
resourceType = "myproject/components/content/teaser",
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class TeaserImpl implements Teaser {
// fields and getters...
}
HTL scripts reference the public interface: data-sly-use.teaser="com.myproject.core.models.Teaser". The Sling Models runtime discovers TeaserImpl registered as an adapter for Teaser.class and instantiates the implementation transparently.
3. defaultInjectionStrategy: Handling Null Properties
By default, Sling Models operate under DefaultInjectionStrategy.REQUIRED. If any field in the class fails to inject (because the property does not exist on the JCR node and no @Default is defined), the framework considers the adaptation an error:
Critical Rule: Under
DefaultInjectionStrategy.REQUIRED, if a single non-optional injection fails, the entire model adaptation fails and returnsnull.
In content authoring, editors frequently leave optional dialog fields unauthored. If a model containing ten fields fails completely because one optional text field is empty, the entire component disappears from the page.
Therefore, the enterprise standard is to declare:
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
When OPTIONAL is set at the class level, missing properties inject as null (or default primitive values: 0, false), allowing the model to adapt successfully. Individual fields that are strictly mandatory can be marked with the @Required annotation.
Dedicated Injector Annotations
While early versions of Sling Models used the generic @Inject annotation, modern AEM development should prefer injector-specific annotations from org.apache.sling.models.annotations.injectorspecific.*.
Generic @Inject makes the injection source less explicit and can require trying multiple injectors. Injector-specific annotations document intent, reduce ambiguity, and produce clearer diagnostics.
1. @ValueMapValue: Injecting Node Properties
Injects properties from the adaptable's ValueMap:
@ValueMapValue
private String title;
// Inject property with custom name and fallback
@ValueMapValue(name = "jcr:title")
@Default(values = "Default Title")
private String pageTitle;
@ValueMapValue
@Named("cq:lastModifiedBy")
private String modifiedBy;
2. @ChildResource: Injecting Sub-Nodes and Collections
Injects child nodes as Resource, ValueMap, or another nested Sling Model. This is the cornerstone of authoring multifields and composite components:
// Injects a single child resource adapted to a child model
@ChildResource(name = "image")
private ImageModel image;
// Injects child nodes under 'items' as a list of LinkItem models (Multifield pattern)
@ChildResource(name = "items")
private List<LinkItem> items;
3. @Self: Injecting the Adaptable
Injects the adaptable itself into the model or adapts the current adaptable into another object:
// Injecting the underlying Resource in a Resource-adapted model
@Self
private Resource resource;
// In a SlingHttpServletRequest model, adapting the request's resource to Core Component Teaser
@Self
@Via(type = ResourceSuperType.class)
private com.adobe.cq.wcm.core.components.models.Teaser coreTeaser;
4. @OSGiService: Injecting OSGi Services
Injects active OSGi services into the model. Supports optional LDAP target filters:
@OSGiService
private ModelFactory modelFactory;
@OSGiService(filter = "(service.ranking>=500)")
private PaymentGatewayService paymentService;
5. @RequestAttribute: Capturing HTL Parameters
When adapting from SlingHttpServletRequest.class, @RequestAttribute captures parameters passed from HTL data-sly-use or data-sly-template:
<!-- HTL Markup passing dynamic parameters -->
<div data-sly-use.card="${'com.myproject.core.models.Card' @ cardTheme='dark', maxItems=5}">
<p class="theme-${card.cardTheme}">Displaying ${card.maxItems} items</p>
</div>
@Model(adaptables = SlingHttpServletRequest.class, adapters = Card.class)
public class CardImpl implements Card {
@RequestAttribute(name = "cardTheme")
@Default(values = "light")
private String cardTheme;
@RequestAttribute(name = "maxItems")
@Default(intValues = 3)
private int maxItems;
}
6. @ScriptVariable: Injecting Scripting Objects
Injects standard AEM/Sling scripting objects into request-adapted models:
@ScriptVariable
private Page currentPage;
@ScriptVariable
private ResourceResolver resourceResolver;
@ScriptVariable
private Style currentStyle;
@ScriptVariable
private PageManager pageManager;
Post-Construction Lifecycle: @PostConstruct
In many components, raw injected values require post-processing: assembling complex URLs, computing date formats, validating business rules, or loading secondary resources.
The @PostConstruct annotation (javax.annotation.PostConstruct) designates a method executed immediately after all field injections are complete:
@PostConstruct
protected void init() {
if (StringUtils.isNotBlank(linkUrl)) {
// Resolve vanity URLs or external links
this.formattedUrl = resourceResolver.map(linkUrl);
}
this.hasValidData = StringUtils.isNotBlank(this.title) && this.formattedUrl != null;
}
Exception Handling & Failure Rules in @PostConstruct
Understanding @PostConstruct error mechanics is critical for the exam:
- Uncaught Exceptions Abort Adaptation: If an unhandled runtime exception (
NullPointerException,IllegalStateException) is thrown inside@PostConstruct, the Sling Models runtime aborts adaptation and returnsnull. - Checked Exceptions Forbidden: The
@PostConstructmethod cannot declare checked exceptions in its method signature. Any checked exceptions from underlying APIs (e.g.,RepositoryException) must be caught internally:
@PostConstruct
protected void init() {
try {
performComplexInitialization();
} catch (Exception e) {
LOG.error("Failed to initialize model for resource: {}", resource.getPath(), e);
// Set safe fallback state rather than rethrowing
this.items = Collections.emptyList();
}
}
Sling Model Exporter & Headless Delivery (.model.json)
The Sling Model Exporter framework enables Sling Models to serialize directly into JSON (or XML) representations. This is the foundational technology powering the AEM SPA Editor (React/Angular), Headless CMS delivery, and content synchronization with mobile applications.
1. The @Exporter Annotation
To expose a model through the exporter, annotate the class with @Exporter:
@Model(
adaptables = {SlingHttpServletRequest.class, Resource.class},
adapters = {Teaser.class, ComponentExporter.class},
resourceType = "myproject/components/content/teaser",
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = "jackson", extensions = "json", options = {
@ExporterOption(name = "SerializationFeature.WRITE_DATES_AS_TIMESTAMPS", value = "false")
})
public class TeaserImpl implements Teaser, ComponentExporter {
@ValueMapValue
private String title;
@ValueMapValue
private String description;
@ValueMapValue
@JsonIgnore
private String internalTrackingKey; // Excluded from exported JSON
@JsonProperty("heading")
@Override
public String getTitle() {
return title;
}
@Override
public String getExportedType() {
return "myproject/components/content/teaser";
}
}
2. Exporter Annotations & Capabilities
@JsonProperty("key"): Overrides the JSON property key in the serialized payload.@JsonIgnore: Strictly suppresses sensitive, internal, or circular references from appearing in the JSON output.@JsonInclude(JsonInclude.Include.NON_NULL): Suppresses null properties to keep the JSON payload lightweight.ComponentExporter: Interface required by the AEM SPA Editor. ImplementinggetExportedType()returns thesling:resourceTypeso front-end SPAs know which JavaScript component to mount.
3. Exposing Models via .model.json
When @Exporter(name = "jackson", extensions = "json") is registered with a resourceType, Apache Sling's Model Exporter Servlet automatically intercepts requests matching:
https://www.example.com/content/myproject/us/en/jcr:content/root/teaser.model.json
It executes Jackson serialization on the adapted model and streams the JSON response with Content-Type: application/json.
Unit Testing Sling Models with AEM Mocks (AemContext)
Unit testing Sling Models without starting a live AEM instance is achieved using the open-source io.wcm.testing.aem-mock (AEM Mocks) library and JUnit 5.
AemContext simulates an in-memory JCR repository (Apache Sling ResourceResolver Mock or Oak Mock), OSGi bundle context, and Sling HTTP request/response pipeline:
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(AemContextExtension.class)
class TeaserModelTest {
private final AemContext context = new AemContext();
@BeforeEach
void setUp() {
// Register Sling Model classes
context.addModelsForClasses(TeaserImpl.class);
// Load mock JSON content into simulated JCR
context.load().json("/com/myproject/core/models/TeaserTest.json", "/content/teaser");
}
@Test
void testTeaserModelAdaptation() {
// Set current resource context
context.currentResource("/content/teaser");
// Adapt resource to model interface
Teaser teaser = context.currentResource().adaptTo(Teaser.class);
assertNotNull(teaser, "Teaser model must adapt successfully");
assertEquals("Summer Campaign", teaser.getTitle());
assertTrue(teaser.isActionEnabled());
}
}
This testing pattern runs in milliseconds within standard Maven mvn test phases, enforcing continuous code quality gates in Cloud Manager CI/CD pipelines.
Why would an AEM developer choose adaptables = SlingHttpServletRequest.class instead of adaptables = Resource.class when declaring a Sling Model?
In Apache Sling Models, what occurs if an authored JCR property is missing from the repository, and the model class does NOT specify defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL?
A developer needs to expose an existing Sling Model's data as a JSON payload for consumption by a Single Page Application (SPA) using the .model.json URL selector. Additionally, an internal tracking token field in the model must be excluded from this JSON output. Which combination of annotations correctly satisfies this requirement?
In the Sling Models lifecycle, when is a method annotated with @PostConstruct executed, and what occurs if an unhandled runtime exception is thrown during its execution?