Expand my Community achievements bar.

SOLVED

Sling Models Constructor, What, Why, How?

Avatar

Level 6

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

1 Accepted Solution

Avatar

Correct answer by
Level 10

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 

View solution in original post

1 Reply

Avatar

Correct answer by
Level 10

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