Building Sling Models With Custom Injected Fields
Sling Models are one of the most powerful patterns available to AEM developers, providing a clean way to map Java POJOs onto Sling objects such as resources and requests. When a component needs structured data behind a Sightly template, a well-crafted Sling Model removes most of the boilerplate that used to clutter JSP-based code. The framework handles adapter factories, type conversion, and injection from a resource's ValueMap, leaving developers free to focus on business logic.
The built-in injection annotations cover a lot of ground, yet the moment a project demands values pulled from an OSGi service, derived from request attributes, or aggregated across multiple resources, custom injected fields become essential. In Australian development shops from Sydney to Perth, this scenario comes up constantly when teams build retail banking portals, government content sites, or university marketing platforms where the default property model cannot keep up.
Anatomy of a Sling Model and Its Injection Points
A Sling Model is an ordinary Java class annotated with @Model, declaring which adaptable types it supports. A typical model adapts from SlingHttpServletRequest and Resource, exposing getters that front-end templates can call. Behind the scenes, the framework looks up an AdapterFactory service that knows how to instantiate the class, then walks through each field or setter and resolves its value through registered injectors.
The injection system is essentially a chain of responsibility. When Sightly encounters data-sly-use.bean="…" in a template, it asks the model to adapt itself, and the framework iterates through every available injector until one claims the field. By default, several come bundled: a ValueMap injector for node properties, a request attribute injector, a servlet request injector for SlingBindings, and a child resource injector for nested nodes. Each carries a short name used by the @Source annotation.
What many developers miss is that injectors are OSGi services. Adding one is a matter of writing a class that implements Injector and registering it with the right service properties. Once registered, every Sling Model can reach it through the standard @Inject mechanism.
Defining Custom Injectors With the @Inject Annotation
The @Inject annotation is the doorway into the chain. By default it accepts an optional selector string and a default value, but it becomes powerful when paired with @Source, which targets an injector by its service property name such as my-service or request-attribute. Without @Source, the default ValueMap injector handles the field and developers wonder why their service lookup silently returns null.
A field can also be marked @Optional, which tells the injector to skip it gracefully when the value is unavailable. That single annotation often saves hours of debugging, particularly in editorial workflows where authors save a component with only half the dialog filled in. A useful pattern is to inject an OSGi service reference by its component property name and let the model call its methods inside getters. The model stays testable while the service handles its own configuration.
It is worth noting that @Named plays a complementary role. When a model inherits from another class, multiple fields might share the same Java property name, and @Named resolves the ambiguity by telling the injector which ValueMap key or request attribute to read. Teams in Brisbane and Melbourne regularly use @Named together with @Source to build inheritance hierarchies, especially in multi-brand publishing environments where a single component must render differently per site.
| Concern | Built-in injectors | Custom injectors |
|---|---|---|
| Source of value | ValueMap, request, bindings | OSGi services, external systems, derived data |
| Registration | Bundle ships with framework | OSGi @Component with injector.sources property |
Use of @Source |
Optional defaults | Required to target a specific injector |
| Typical scenario | Standard component properties | Tenant context, feature flags, campaign data |
| Test surface | SlingMocks ValueMap helpers | Service mocks plus mock adaptable objects |
Implementing the Injector Interface Step by Step
Writing a custom injector starts with implementing org.apache.sling.models.spi.Injector. The interface defines a single getValue method that receives the adaptable object, field name, type, annotations, and a default. The implementation inspects the annotations and type, returning the resolved value or null. Returning null is critical, because the framework expects each injector to opt in only for the cases it understands.
The class then needs an @Component annotation with service properties, the most important being injector.sources containing the short identifier referenced from @Source. For example, a request-attribute injector would set its source name accordingly so that any Sling Model can declare @Inject @Source("request-attribute") and reach into the current request. Registration happens automatically once the bundle is deployed, and the framework picks the new injector up on the next model adaptation.
A practical pattern wraps configuration service access. Suppose the project needs the current tenant identifier, the active locale, or a feature flag from a centralised service. The custom injector can look up the OSGi service through BundleContext or a @Reference field and return the typed result. Keeping all the lookup logic inside the injector means every model stays free of service plumbing. Pairing them with a setup like the one described in Apache Derby for local development keeps unit tests reproducible across team members in Adelaide and Perth.
Wiring Sightly Templates to Custom Injected Values
Once a model is registered and a custom field is injected correctly, exposing it to the front end takes nothing more than a getter method. Sightly picks up the model through data-sly-use.bean="com.example.core.models.HeroBanner", and every getter becomes a template variable. If the model injects a list of related articles through a service-backed field, the template can iterate over it with data-sly-list.
A common oversight is forgetting the adapter type. Sightly calls the model adaption through adaptTo, and the framework walks the chain only if the requested type matches one declared in @Model. If the type is missing, the model silently returns null and templates render empty. A quick sanity check is to add ${bean.fieldName @ context='html'} near the top of the template, which prints the raw value in author preview.
For larger projects, this pattern shines when marketing teams need to surface campaign metadata across hundreds of pages. A custom campaign injector can read from a third-party API through an OSGi wrapper, and every component can consume the same campaign context. AEM's content tree does the heavy lifting, but the injector handles the cross-cutting data. Anyone planning to deepen their AEM skills can review the agenda and register for the conference ahead of time to watch recorded sessions on Sightly and Sling.
Testing and Troubleshooting Custom Injection Logic
Testing custom injectors begins with mock adaptable objects. SlingMocks ships with helpers for mock requests, mock ValueMaps, and even mock OSGi contexts, so a custom injector can be tested without a full AEM instance. A unit test instantiates the injector, calls getValue with a synthetic request, and asserts that the right value comes back.
Common pitfalls include missing @Optional on fields that may legitimately be absent, an injector that returns an empty string instead of null and silently wins the chain, and service ranking that places a custom injector ahead of the ValueMap injector for a property that should resolve from the node. Debug logs reveal the order of injector queries, making it easy to see whether a field reached the intended one.
The biggest gain is architectural. Once a project has three or four custom injectors, the codebase starts to feel less like a collection of components and more like a coherent platform with shared services, predictable patterns, and tests that mean something. Front-end developers in Melbourne's AEM community often describe this moment as the point where Sightly development stops feeling like guesswork.
Custom injectors unlock a level of reuse that pays off across years of feature work. Teams that build a small library for common needs — tenant context, feature flags, request-scoped config — reach for them instinctively on the next engagement. Whether the work happens in Sydney, Brisbane, or remotely across Australia's time zones, the discipline compounds. Pick a field that frustrates the team, sketch the injector on paper, and ship a first version before the next sprint review. Even creative collectives such as Istanbul dance events show how shared practice moves across disciplines, an analogy for an AEM team investing in shared injector libraries.