Java Best Practices in AEM
This page collects practical Java guidelines for AEM projects. The goal is consistent, safe code that is easy to test and maintain.
Core principles
- Keep Sling Models small and focused.
- Prefer services for reusable business logic.
- Fail safely and log only what you need.
- Avoid direct JCR APIs unless necessary.
Prefer Resource API over JCR when possible
Resource resource = resolver.resolve("/content/my-site/en");
if (ResourceUtil.isNonExistingResource(resource)) {
return;
}
ValueMap props = resource.getValueMap();
String title = props.get("jcr:title", String.class);
Prefer early returns
Early returns make code easier to read and reduce deep nesting, especially around error handling.
public String getTitle(ResourceResolver resolver, String path) {
Resource resource = resolver.resolve(path);
if (ResourceUtil.isNonExistingResource(resource)) {
return "";
}
String title = resource.getValueMap().get("jcr:title", String.class);
return StringUtils.defaultIfBlank(title, "Untitled");
}
Sling Models
- Use
DefaultInjectionStrategy.OPTIONALand handle missing values. - Keep logic out of HTL; expose computed values from the model.
- Avoid heavy repository queries in models.
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = {ComponentExporter.class},
resourceType = MyComponent.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class MyComponent implements ComponentExporter {
public static final String RESOURCE_TYPE = "myproject/components/mycomponent";
@ValueMapValue
private String title;
public String getTitle() {
return StringUtils.defaultIfBlank(title, "Default title");
}
}
Services and OSGi
- Keep service interfaces small and explicit.
- Use OSGi configs for endpoints and flags.
- Avoid static singletons; rely on OSGi lifecycle.
@ObjectClassDefinition(
name = "My Service",
description = "Example configuration for MyService"
)
public @interface MyServiceConfig {
@AttributeDefinition(name = "Enabled")
boolean enabled() default true;
@AttributeDefinition(name = "API Base URL")
String apiBaseUrl() default "https://api.example.com";
}
@Component(service = MyService.class)
@Designate(ocd = MyServiceConfig.class)
public class MyServiceImpl implements MyService {
private boolean enabled;
private String apiBaseUrl;
@Activate
protected void activate(MyServiceConfig config) {
enabled = config.enabled();
apiBaseUrl = config.apiBaseUrl();
}
}
Example config file:
{
"enabled": true,
"apiBaseUrl": "https://api.example.com"
}
ResourceResolver usage
- Use a service user for background tasks.
- Always close resolvers you open.
- Avoid leaking resolver instances across threads.
try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(authInfo)) {
Resource resource = resolver.getResource(path);
// work with resource
}
Querying content
- Prefer QueryBuilder with explicit predicates.
- Make sure queries are indexed and scoped by path.
- Avoid building SQL2 with raw user input.
SQL2 example with safe parameter handling
JCR SQL2 does not support prepared statements. For user-controlled values, prefer the Query Object Model (QOM) API, which lets you bind values safely.
public List<Resource> findPagesByTitle(ResourceResolver resolver, String rootPath, String title) throws RepositoryException {
Session session = resolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();
QueryObjectModelFactory qom = qm.getQOMFactory();
ValueFactory vf = session.getValueFactory();
Constraint pathConstraint = qom.descendantNode("p", rootPath);
Constraint titleConstraint = qom.comparison(
qom.propertyValue("p", "jcr:title"),
QueryObjectModelFactory.JCR_OPERATOR_LIKE,
qom.literal(vf.createValue(title))
);
QueryObjectModel query = qom.createQuery(
qom.selector("cq:PageContent", "p"),
qom.and(pathConstraint, titleConstraint),
null,
null
);
QueryResult result = query.execute();
List<Resource> resources = new ArrayList<>();
NodeIterator nodes = result.getNodes();
while (nodes.hasNext()) {
Node node = nodes.nextNode();
Resource resource = resolver.getResource(node.getPath());
if (resource != null) {
resources.add(resource);
}
}
return resources;
}
If you must build a raw SQL2 string, allowlist the input and escape it before interpolation. Avoid concatenating unsanitized values.
Error handling and logging
- Log with intent; avoid noisy info logs in hot paths.
- Include context in errors, but avoid sensitive data.
- Use
warnfor recoverable problems anderrorfor failures.
try {
doWork();
} catch (Exception e) {
LOG.error("Failed to process content for path {}", path, e);
}
Threading and schedulers
- Prefer
SlingScheduleror OSGi schedulers. - Do not spawn your own threads without a clear reason.
- Keep scheduled jobs idempotent.
@Component(service = Runnable.class,
property = {
"scheduler.expression=0 0 * * * ?",
"scheduler.concurrent=false"
})
public class HourlyJob implements Runnable {
@Override
public void run() {
// work must be idempotent
}
}
Security basics
Validate all user input before use
Treat anything that comes from requests, content properties, or external services as untrusted. Validate early and close to the boundary.
Backend validation + encoding (AEM/Sling)
Use AEM’s XSSAPI to validate and encode user input before it reaches templates or logs.
@Reference
private XSSAPI xssApi;
public String safeText(String input) {
// Strictly validate: returns null if invalid
String valid = xssApi.getValidString(input, 200, true);
if (valid == null) {
return "";
}
// Encode for HTML output
return xssApi.encodeForHTML(valid);
}
public String safeUrl(String url) {
// Validates and normalizes URLs
return xssApi.getValidHref(url);
}
Other backend techniques:
- Use HTL contexts (
@ context='uri',@ context='html') when rendering values. - Use
Text.escape()orEncode.forHtml()for non-HTL contexts. - Prefer allowlists over regex for values like hostnames, paths, or enums.
Frontend pitfalls and sanitization
Even if backend is safe, frontend code can reintroduce XSS:
- Injecting raw HTML into the DOM (
innerHTML) without sanitization. - Using template helpers that output raw HTML by default.
- Binding untrusted values to
hreforsrcwithout validation.
Use DOMPurify when you must render HTML from user-controlled sources:
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(untrustedHtml, {USE_PROFILES: {html: true}});
element.innerHTML = clean;
Handlebars auto-escapes, but it does not sanitize URLs. Add a helper for href values:
Handlebars.registerHelper('safeHref', (value) => {
try {
const url = new URL(value, window.location.origin);
return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.toString() : '#';
} catch (e) {
return '#';
}
});
Additional security notes
- Do not hardcode credentials.
- Use allowlists for any external hostnames.
private static final Set<String> ALLOWED_HOSTS =
Set.of("api.example.com", "services.example.com");
public boolean isAllowedHost(URI uri) {
return ALLOWED_HOSTS.contains(uri.getHost());
}