Description: Sling Models: From Scriptlets to POJOs: Apache Sling from a practitioner’s perspective with timeless lessons.
A quick chat in the team room
Me: Why is there Java in this JSP that looks like spaghetti?
Teammate: It started small. A getter here, a null check there. Then a deadline hit. You know the rest.
Me: What if we move the logic into a POJO, adapt it from the Resource, and keep the template clean?
Teammate: Like a model per component?
Me: Exactly. Apache Sling Models. Annotations. Injection. No custom base class. And unit tests that do not need a running container.
Teammate: Sold. Show me.
Evidence that this helps
Here is what we see when we pull logic out of JSP scriptlets and into Sling Models:
- Separation of concerns: JSP renders. The model prepares data. Review diffs without hunting through tags and scriptlets.
- Less repetition: Normalize dates, images, and links once in the model, reuse across components, and stop copying checks into every JSP.
- Testability: Create the model as a plain object and verify behavior. No container, no mocks for request binding unless you need them.
- Safer rendering: The template only reads values. Your null and fallback logic lives in one place. Fewer surprises in production.
- Performance that makes sense: Model instances are light and short lived. Adapt from the Resource or Request and move on. You can also compute once and cache per request when needed.
A small before and after to make it real.
Old approach in a JSP, mixing concerns:
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%
String title = currentPage.getProperties().get("jcr:title", "");
if (StringUtils.isBlank(title)) {
title = currentPage.getName();
}
String byline = properties.get("byline", "Staff");
%>
<h2><%= xssAPI.encodeForHTML(title) %></h2>
<p class="by"><%= xssAPI.encodeForHTML(byline) %></p>
New approach with a Model that keeps the JSP clean:
package com.example.site.models;
import javax.annotation.PostConstruct;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.Inject;
import org.apache.sling.models.annotations.injectorspecific.Named;
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class Article {
@Inject @Named("jcr:title")
private String title;
@Inject
private String byline;
@Inject
private Resource resource;
private String safeTitle;
private String safeByline;
@PostConstruct
protected void init() {
safeTitle = isBlank(title) ? resource.getName() : title;
safeByline = isBlank(byline) ? "Staff" : byline;
}
public String getTitle() { return safeTitle; }
public String getByline() { return safeByline; }
private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }
}
And the JSP gets very small:
<%@ page import="com.example.site.models.Article" %>
<% Article m = resource.adaptTo(Article.class); %>
<h2><%= xssAPI.encodeForHTML(m.getTitle()) %></h2>
<p class="by"><%= xssAPI.encodeForHTML(m.getByline()) %></p>Build notes for Sling Models in your project
You only need the API and the model engine. Bring them into your OSGi bundle and you are ready.
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.models.api</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.models.impl</artifactId>
<version>1.0.0</version>
</dependency>
A few bite sized tips:
- Choose the right adaptable: If you only need content, adapt from Resource. If you need selectors or suffix or user info, adapt from SlingHttpServletRequest.
- Inject what you need: Use
@Injectfor properties and services. For a property name that does not match your field, use@Named("jcr:title"). - Default safely: Compute fallbacks in
@PostConstructso getters are simple. Keep templates dumb. - Only escape in the view: Keep raw values in the model. Let JSP handle XSS escaping with
xssAPI. - Keep models small: A model per component or per concern. No god classes.
Example with request adaptable and a service:
package com.example.site.models;
import javax.annotation.PostConstruct;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Inject;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
@Model(adaptables = SlingHttpServletRequest.class)
public class Hero {
@Inject
private String title;
@OSGiService
private LinkResolver linkResolver;
@Inject
private SlingHttpServletRequest request;
private String link;
@PostConstruct
void init() {
ResourceResolver rr = request.getResourceResolver();
link = linkResolver.resolve(rr, request.getRequestPathInfo().getSuffix());
}
public String getTitle() { return title; }
public String getLink() { return link; }
}Risks and gotchas
- Version alignment: On some stacks you might not have models on day one. Bring the API and the engine bundle, and confirm they start cleanly with no package conflicts.
- Null injection: Missing content becomes null. Mark optional fields or set defaults in
@PostConstructto avoid surprises. - Heavy work in getters: Do the work once in
@PostConstruct. Getters should be cheap and boring. - Over injection: Models are not service bags. Inject what you need. Pass the rest in via the adaptable.
- Thread safety: Each model instance is short lived. Do not store static state. Do not cache across requests unless you know the life cycle.
- Rendering safety: Escape at the point of output. The model should not return HTML mixed with content unless you control it fully.
- Adapting from the wrong thing: If injection fails, check the adaptable. A field that needs request context will not resolve when you adapt from resource.
A simple way to wrap this up
Start small. Pick one component that always collects little utilities in its JSP. Create a Sling Model named after it. Move the data shaping code into the model, add @Inject for the properties you use, and write a couple of unit tests for the edge cases you know users hit. Then switch the JSP to adapt the model and read getters. Ship it. Watch your diffs get smaller and your reviews get friendlier.
This is not just cleaner code. It is a way to keep Apache Sling doing what it does best: adapt from content and request to a plain object that fits your component. From there, testing and maintenance get easier, and your pages stop carrying hidden logic in template land.
If your team is juggling new builds and lots of front end work, this shift buys you clarity without a big rewrite. You can migrate component by component, keep the same markup, and keep moving. When you look back after a few sprints, you will notice fewer bugs caused by last minute scriptlet edits and more confidence touching old components.
If you are working in AEM or plain Sling, or you are just curious about better patterns around JSP and OSGi, give Sling Models a try on the next ticket. Your future self will send you a thank you note.