Sling model: Best Practices


To gain insight into Sling Models and the annotations at your disposal, consult the Sling model annotations: Basics. Now, let’s delve into best practices.

1. Use specific annnotations, rather than @Inject

Lets explore with an example of @Inject v/s @ValueMapValue

  • @Inject is a general-purpose annotation that retrieves values from various injectors
  • @ValueMapValue is an injector-specific annotation designed to fetch values from the ValueMap injector.

When an injected value is exclusively available from a single injector, @Inject and @ValueMapValue will behave the same. However, if a property can be provided by multiple injectors (e.g., script-binding and ValueMap), they may inject different values.

It’s advisable to prefer injector-specific annotations like @ValueMapValue or @ChildResource over @Inject. This is because value injection occurs at runtime, and using @Inject can introduce ambiguity, requiring the framework to make educated guesses. When using @ValueMapValue, especially in the context of retrieving properties from the ValueMap, the process is automatic, resulting in less code to write and increased clarity.

DO:

@ValueMapValue
private String mailAddress;

DON’T:

@Inject
private String mailAddress;

For more details on how annotations affect performance, refer to Sling Model Performance by Jörg Hoh

2. Avoid using both Resource and SlingHttpServletRequest together as adaptables

It’s advisable to refrain from using both Resource and SlingHttpServletRequest as adaptables simultaneously in your AEM Sling Model. This is primarily because having both options can lead to complex injection scenarios that are hard to predict and may result in unexpected issues or bugs.

1. Injection Differences:

Injection behavior differs when adapting from SlingHttpServletRequest versus adapting from Resource. Some injectors, like @Self, behave differently depending on the adaptable. For instance, when adapting from Resource, an injection targeting SlingHttpServletRequest may result in a null value.

3. Potential for Hard-to-Debug Issues:

Mixing adaptables in a single Sling Model can create scenarios that are challenging to test and debug. It becomes difficult to predict the outcome of injections, making troubleshooting complex, and potentially causing hard-to-find bugs.

DON’T: Here’s an example of why it’s problematic:

@Model(adaptables = { SlingHttpServletRequest.class, Resource.class },
                      defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MySlingModel {

    @Self
    // Will be null if adapted from Resource!!!
    private SlingHttpServletRequest request;

    @Self
    // Will be null if adapted from SlingHttpServletRequest!!!
    private Resource resource;
}

DO:

@Model(adaptables = { SlingHttpServletRequest.class},
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MySlingModel {

@Self
private SlingHttpServletRequest request;
private Resource resource;

    /**
* Initialize the model.
*/
@PostConstruct
protected void initModel() {
resource = request.getResource();
}
}

3. Use Model Interfaces for Extensibility:

Create a Sling model interface class along with its implementation package. This practice is especially beneficial when you intend to add extra features or create versions of the same component. By defining a model interface, you establish a contract that specifies the methods and properties that should be available in various implementations. This makes it easier to extend or customize your Sling Models while maintaining consistency and ensuring that new versions adhere to the established contract. It promotes a clean separation of concerns and facilitates the gradual evolution of your AEM components.

Here’s an example to illustrate this concept with two classes implementing the same interface:

Suppose you have a requirement to create different types of cards for your website. You want to ensure that all card types have some common functionality, such as a getTitle() method and a getDescription() method. However, each card type may have its own unique features. This is where model interfaces come in handy.

  1. First, create a model interface for your card components:
// CardModel.java (Model Interface)
import com.adobe.cq.export.json.ComponentExporter;
import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface CardModel extends ComponentExporter  {
    String getTitle();
    String getDescription();
}
  1. Next, implement this interface in two different card classes:
// TextCard.java (Implementation 1)
@Model(adaptables = {Resource.class}, adapters = {Card.class, ComponentExporter.class},
        resourceType = "techrevel/components/cf/txtcard", defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class TextCard implements CardModel {
    private String title;
    private String description;

    public TextCard(String title, String description) {
        this.title = title;
        this.description = description;
    }

    @Override
    public String getTitle() {
        return title;
    }

    @Override
    public String getDescription() {
        return description;
    }

    // Additional methods and properties specific to TextCard
}

// ImageCard.java (Implementation 2)
@Model(adaptables = {Resource.class}, adapters = {Card.class, ComponentExporter.class},
        resourceType = "techrevel/components/cf/imgcard", defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ImageCard implements CardModel {
    private String title;
    private String description;
    private String imageUrl;

    public ImageCard(String title, String description, String imageUrl) 
    {
        this.title = title;
        this.description = description;
        this.imageUrl = imageUrl;
    }

    @Override
    public String getTitle() {
        return title;
    }

    @Override
    public String getDescription() {
        return description;
    }

    // Additional methods and properties specific to ImageCard
}

By using the CardModel interface, you ensure that both TextCard and ImageCard classes provide the required getTitle() and getDescription() methods while allowing each card type to have its own specific properties and methods. This approach makes your code more maintainable, extensible, and consistent, as it enforces a contract that must be followed by all card implementations.

4. Avoid code duplication with Abstract Classes

Abstract classes offer a standardized framework to avoid code duplication and promote consistency across components. Take, for example, the AbstractImageDelegatingModel.java class from the AEM Core Components repository. By extending this abstract class, developers can effortlessly incorporate image-related features into custom components while ensuring code reusability and maintainability.

Benefits:

  1. Code Reusability: Abstract classes facilitate the reuse of common functionality across multiple components, saving development time.
  2. Maintainability: Centralized code in abstract classes simplifies maintenance tasks, ensuring uniform updates across components.
  3. Consistency: Abstract classes enforce standardized patterns and practices, promoting a cohesive codebase.
  4. Flexibility: While providing a standardized foundation, abstract classes allow for customization to meet specific component requirements.

5. Image Src:

When rendering image links, it’s essential to utilize Sling Models for cache busting, responsive rendering, and consistency. Directly accessing properties in HTL can lead to issues like browser caching, displaying original image sizes, and incorrect URL rendering, compromising website performance and user experience.

6. Generate Adaptive Image Links

Adaptive image links leverages AEM’s built-in adaptive image processing capabilities. This ensures efficient delivery to diverse devices and screen sizes. By emulating core AEM components like the WCM Core Teaser component, Sling Models can readily utilize Image component features to generate adaptive image links, via Image Delegation. On details on how to implement, refer to Adaptive Image Rendering for AEM components

7. Recommended practices around @PostConstruct method

  • Keep It Light: Ensure @PostConstruct logic is fast; avoid resource-intensive operations like network calls or complex database queries.
  • Handle Errors: Implement error handling for missing dependencies or exceptions, and log informative messages.
  • Prevent Loops: Beware of infinite loops when calling methods that could recursively trigger model instantiation in @PostConstruct.

For more details on how PostConstuct can affect performance, please refer to Sling Model Performance by Jörg Hoh

8. Sling Model Instantiation: Prefer modelFactory over adaptTo()

In AEM development, while the adaptTo() method is commonly used for translating objects and creating Sling Models, it may not be the best option in all scenarios. One major drawback is that adaptTo() doesn’t provide clear error handling. Instead, it often returns null when issues arise, making problem diagnosis difficult. To address this, consider using ModelFactory for Sling Model instantiation, a more robust and informative alternative introduced in Sling Models 1.2.0.

Example:

Let’s say you have a Sling Model called ProductModel. With adaptTo(), you might instantiate it like this:

ProductModel product = resource.adaptTo(ProductModel.class);

However, if something goes wrong during instantiation (e.g., missing dependencies or constructor issues), adaptTo() will quietly return null, leaving you unaware of the problem.

Now, with ModelFactory, you can achieve the same instantiation but with more comprehensive error handling:

try {
      if(modelFactory.canCreateFromAdaptable(request, ProductModel.class)){
            ProductModel product = modelFactory.createModel(request, ProductModel.class);
       }
} catch (Exception e) {
    // Handle the exception and display a meaningful error message.
    // You'll know exactly why the model couldn't be instantiated.
    // MissingElementsException - in case no injector was able to inject some required values with the given types
    // InvalidAdaptableException - in case the given class cannot be instantiated from the given adaptable (different adaptable on the model annotation)
    // ModelClassException - in case the model could not be instantiated because model annotation was missing, reflection failed, no valid constructor was found, model was not registered as adapter factory yet, or post-construct could not be called
    // PostConstructException - in case the post-construct method has thrown an exception itself
    // ValidationException - in case validation could not be performed for some reason (e.g. no validation information available)
    // InvalidModelException - in case the given model type could not be validated through the model validation
}

Using ModelFactory, you gain the ability to capture exceptions and obtain detailed information about what caused the instantiation to fail. This makes it easier to troubleshoot issues and write more robust code. ModelFactory also provides additional methods for checking if a class is a valid model and if it can be adapted from a given adaptable.

For example:

  • canCreateFromAdaptable: Indicates whether the given class can be created from the provided adaptable (true/false).
  • isModelAvailableForResource: Determines if a model class is available for the resource’s resource type.

For further details, please refer to link

9. Optimize AEM Development with Core Components

Core Components are standardized Web Content Management (WCM) elements in AEM designed to streamline development and reduce maintenance costs. They are production-ready, cloud-ready, versatile, and configurable. The components align with accessibility standards (WCAG 2.1), promote SEO-friendly HTML output, support AMP, and offer design kits for Adobe XD. Additionally, they are customizable, versioned for stability, localizable, and open-source for community contributions.

It’s advisable to utilize or extend WCM Core components whenever they fulfill the majority of project requirements.

10. Use lombok when extending core components via delegation

In AEM development, the Delegation pattern is often employed to extend core components. This pattern involves creating a custom component that delegates OOTB functionality to the core component, while also providing additional or modified functionality as needed.

More details can be found in a related thread: Delegation Pattern for Sling Models Doesn’t Populate All the Properties

Challenge with delegation pattern:

The issue with using the delegation pattern for Sling Models is that it may result in all properties returning null except for the one being overridden/added. This occurs because when injecting the Core Component model using annotations like @Self, it may not render the implementation of the interface from the Core Component’s model.

To address this issue, one solution is to individually implement all the methods of the Core Model’s interface in your custom Model. However, this approach can be cumbersome and may lead to code duplication.

Solution:

Alternatively, Lombok provides the @Delegate annotation, which can streamline the implementation of delegate methods. Lombok enhances your code in the following ways:

  1. Conciseness: Lombok reduces boilerplate code, making your Sling Models more concise and easier to manage.
  2. Maintainability: It simplifies code maintenance by automatically generating delegate methods, reducing manual updates when class structures change.
  3. Readability: Lombok-generated code is more readable and clearly conveys the intention of delegating methods.
  4. Error Reduction: Fewer coding mistakes occur because Lombok-generated code is less prone to human error.
  5. Consistency: It enforces consistency in delegate methods, ensuring uniformity throughout your codebase.
  6. Time-Saving: Lombok with @Delegate significantly speeds up development by eliminating the need to write and maintain repetitive delegation code.
  7. Automatic Updates: When making changes to your delegation logic, Lombok can automatically update delegate methods based on the modified fields or methods.

For details on how to use lombok, please refer to AEM : Avoiding Delegation Pattern Pitfalls with Core Components by Veena Vikram

To generate links from component properties, leverage the com.adobe.cq.wcm.core.components.commons.link.LinkManager API. This interface can be accessed using the @Self annotation. By utilizing this API, you can customize link targets and attributes, providing greater control and flexibility in link generation. Notably, WCM Core components, such as the Call-to-Action (CTA) link in Teaser, utilize this mechanism for rendering links.

import com.adobe.cq.wcm.core.components.commons.link.Link;
import com.adobe.cq.wcm.core.components.commons.link.LinkManager;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Self;

@Model(adaptables = Resource.class)
public class MyLinkModel {

@Self
protected LinkManager linkManager;

private Link link;

protected void initLink(Resource contentResource, String propertyName) {
// generate link from the contentResource that via propertyName
link = linkManager.get(contentResource).withLinkUrlPropertyName(propertyName).build();
}

public Link getLink() {
return link;
}
}

12. Be cautious when using cache-true with Sling Models

Regarding cache = true and the self injector, it’s crucial to note that storing a reference to the original adaptable using the self injector is highly discouraged.While it won’t crash the JVM, it can still lead to unexpected behavior, such as a model being created twice when it should be cached. This issue was initially reported in SLING-7586.

To avoid this problem, it’s advisable to discard the original adaptable when it’s no longer needed. You can achieve this by setting affected field(s) to null at the end of the @PostConstruct annotated method. Alternatively, you can use constructor injection to perform the necessary actions without storing a reference to the adaptable. Examples available on link

13. Prefer @Optional over defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL

The recommended approach is to use the Optional type for wrapping injected elements because it expresses the “optionality” in the type system. Even when using optional injections, they are still attempted, but if the injection fails, it won’t terminate the Sling Model creation process. Instead, the field value or return value remains at the default value, as provided by the @Default annotation, or the default value of the used type.

If the majority of injected fields or methods in your Sling Model are optional, you can change the default injection strategy for the entire model by adding defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL to the @Model annotation.

14. Usage of Sling Models for Component Development:

Consider using Sling Models even for small components to promote consistency and maintainability.

  • Utilize Sling Models to encapsulate component logic and facilitate easy retrieval and manipulation of component properties.
  • Ensure that all properties retrieved and used within the component are accessed exclusively through the corresponding Sling Model.

Leave a comment