Sling Models Constructor, What, Why, How?

Avatar

Avatar

AEMWizard

Avatar

AEMWizard

AEMWizard

31-03-2020

Hi Community and Friends,

I'm taking a look at the Sling Models Injections using the Constructor, and I am wondering who has experience with this feature? 

  • Is this feature even being used by developers?
  • Why should we use the Sling Model's constructor method? 
  • What can the constructor expect in the method parameters?
  • How can we inject Sling Model Injector-Specific Annotations in the constructor?
  • Isn't it must easier to use the Sling Model Injector-Specific Annotations?
  • Example Code:

 

@Model(adaptables=Resource.class)
public class MyModel {    
    
    public MyModel(@Named("propertyName") String propertyName) {
      // constructor code
    }
}

 

Documentation: https://sling.apache.org/documentation/bundles/models.html#basic-usage

Accepted Solutions (1)

Accepted Solutions (1)

Avatar

Avatar

Theo_Pendle

MVP

Avatar

Theo_Pendle

MVP

Theo_Pendle
MVP

02-04-2020

Hi @AEMWizard,

Good question! I'll answer as best I can but I won't pretend to be a Sling expert 😉

So firstly, if you wanted to use constructor injection rather than field injection, you can do it quite easily using much of the same syntax. To illustrate my point, let's have a look at the HelloWorldModel generated by the latest AEM Archetype 23 (simplified a bit to be less verbose):

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

    @ValueMapValue(name = PROPERTY_RESOURCE_TYPE, injectionStrategy = InjectionStrategy.OPTIONAL)
    @Default(values = "No resourceType")
    protected String resourceType;

    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    @Getter
    private String message;

    @PostConstruct
    protected void init() {
        final PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
        final String currentPagePath = Optional.ofNullable(pageManager)
                .map(pm -> pm.getContainingPage(currentResource))
                .map(Page::getPath).orElse("");

        message = "\tHello World!\n"
                + "\tResource type is: " + resourceType + "\n"
                + "\tCurrent page is: " + currentPagePath + "\n";
    }
}

As we can see, it uses field injection. Now let's copy this component and create a version which uses constructor injection: 

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

    protected String resourceType;
    private final Resource currentResource;
    private final ResourceResolver resourceResolver;

    @Getter
    private String message;

    @Inject
    public HelloWorldModelConstructorInjection(@ValueMapValue(name = PROPERTY_RESOURCE_TYPE, injectionStrategy = InjectionStrategy.OPTIONAL)
                                               @Default(values = "No resourceType") final String resourceType,
                                               @SlingObject final Resource currentResource,
                                               @SlingObject final ResourceResolver resourceResolver) {
        this.resourceType = resourceType;
        this.currentResource = currentResource;
        this.resourceResolver = resourceResolver;
    }

    @PostConstruct
    protected void init() {
        final PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
        final String currentPagePath = Optional.ofNullable(pageManager)
                .map(pm -> pm.getContainingPage(currentResource))
                .map(Page::getPath).orElse("");

        message = "\tHello World!\n"
                + "\tResource type is: " + resourceType + "\n"
                + "\tCurrent page is: " + currentPagePath + "\n";
    }
}

So basically you just:

  1. Create a constructor that takes all the parameters you need.
  2. Annotate the constructor with @Inject.
  3. Move all the injector-specific annotation from the fields to the arguments.

We can test this to make sure it works. Here is are the components side by side:

Selection_077.png

Works like a charm 🙂

Now regarding the actual usefulness of this feature: as a general rule, constructor is preferable to field injection for two main reasons:

  1. Field-injected dependencies are hidden from the outside, they are not part of the interface.
  2. You cannot instantiate or inject your own objects without using reflection. This is particularly bothersome in unit testing.

However, when using Sling models in an AEM context I think there is a trade-off to be made between observing best-practices and writing readable, maintanable code.

  • Point 1 is not particularly important because we only ever call models instatntiated by a ModelFactory so we don't need to instantiate the dependencies ourselves.
  • Point 2 isn't critical either if you're using AEM mocks for your unit tests as we can use that library to easily mock dependencies (no need for a constructor).

Personally, I use field injection for models.

Hope that all makes sense 🙂

Answers (0)