> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# 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

```java
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.

```java
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.OPTIONAL` and handle missing values.
- Keep logic out of HTL; expose computed values from the model.
- Avoid heavy repository queries in models.

```java
@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.

```java
@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:

```json
{
  "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.

```java
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.

```java
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"),
        QueryObjectModelConstants.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 `warn` for recoverable problems and `error` for failures.

```java
try {
    doWork();
} catch (Exception e) {
    LOG.error("Failed to process content for path {}", path, e);
}
```

## Threading and schedulers

- Prefer `SlingScheduler` or OSGi schedulers.
- Do not spawn your own threads without a clear reason.
- Keep scheduled jobs idempotent.

```java
@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.

```java
@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()` or `Encode.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 `href` or `src` without validation.

Use DOMPurify when you must render HTML from user-controlled sources:

```js
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:

```js
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.

```java
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());
}
```
