Sling models: the story behind @Inject

Reyn van de Craats
WeAreIDA
Published in
6 min readMay 26, 2021

This blog post is based on an issue encountered on a project I’ve worked on. I’ve been working with AEM for a while and in that time I wrote my share of Sling models and unit tests for them. Reading through lots of references on the internet explaining which annotations you can use and how they work. Also using some third-party custom annotations and even adding a custom annotation myself to be used in Sling models.

Yet all this time, the way Sling models work their magic stayed magical somehow... Especially the mysterious @Inject annotation. However, I never encountered really weird behaviour that couldn’t be accounted for one way or another. Until recently.

Before presenting the issue I want to indicate what DOES work. Take a look at the code snippets below:

In an OOTB AEM instance, the Sling model based on the SlingHttpServletRequest works and injects the current page fine, however, the one based on the Resource does not work!

This makes a lot of sense since the current page is present on the request in the sling bindings which can be obtained from the attributes. However, the resource doesn’t keep a reference to the current page nor request. Note: the current page is different from the containing page. It is possible to get the containing page for a resource.

Imagine my surprise when the second snippet of code suddenly starts working for me! From time to time I test my luck and try to inject the current page when adapting from a resource anyway as I’m not always that fond to jump into refactoring code to adapt from a SlingHttpServletRequest. I did some testing on AEM and verified with some colleagues to make sure this wasn’t some weird quirk but this definitely did work!

Obviously, this didn’t suddenly start working out of the blue. But this really baffled me back then. It worked and was consistent but I had no clue where the magic came from to resolve the page. Also, it proved this behaviour wasn’t supported by AemContext which made it more and more mystical. It led to some creative unit test setup. I do want to share the following 2 snippets of unit tests, that both make the unit test work yet really don’t test the actual working of what we implemented. At the end of the article, I’ll refer back to these snippets to see what actually is going on, why they work and how to make it consistent with the actual implementation.

While onboarding a new colleague, this functionality didn’t work on his local environment. It seemed it was caused because he was missing the legacy project code on his local environment. The consistency I mentioned before was gone, which ignited the search to what was causing this oddity!

Searching through the code base, looking at custom injectors and investigating the changes done to the global.jsp to see if anything might explain why the injection of the current page for a resource did work on this project! Not finding anything, I realised I was going completely wrong about this!

How does @Inject actually work, how does it know to pick up a property, or a service?

First, let us look at how an implementation of an injector looks like:

Basically, it’s just an OSGI service! That means we can look at http:localhost:4502/system/console/services to see which implementations actually exist for this! However, this is a cumbersome process. There had to be a better way to see the registered injectors. With some googling, I stumbled upon this article. It pointed me to the page in AEM I was looking for, which connected a lot of dots for me. The system console has the http:localhost:4502/system/console/status-slingmodels page which lists the following topics:

  • Sling Models Injectors
  • Sling Models Inject Annotation Processor Factories
  • Sling Models Implementation Pickers
  • Sling Models Bound to Resource Types *For Resources*
  • Sling Models Bound to Resource Types *For Requests*
  • Sling Models Exporter Servlets
Sling Models Injectors example

The first point, Sling Models Injectors, is what is interesting for this story. This is a list of all the active injectors on the instance. By comparing another instance where the behaviour was not appearing it was easy to find injectors that weren’t present on both systems. By eliminating injectors one by one it’s possible to see when this odd behaviour described earlier stopped working. In my case it was a little easier as the oddity was still working with all bundles stopped from the legacy project, leaving only 3rd party injectors and OOTB injectors. I was able to identify the origin by the name of the inject: io.wcm.sling.models.injectors.impl.AemObjectInjector. This 3rd party injector was added to a dependency package of the legacy code base, therefore not trackable in the codebase itself. The injector itself will inject the containing page when the adaptable is a resource. Mystery solved!

So what happens when we use the @Inject annotation? AEM will process all injectors available and the first one that is able to inject the value “wins”. The order of this list depends on the service ranking of the implementations. So what happens in case the adaptable is a SlingHttpServletRequest? Because org.apache.sling.models.impl.injectors.BindingsInjector is higher on the list, it will “win” and the currentPage variable will actually be the current page. As it is an OOTB injector, it will be available in the AemContext as well.

So if @Inject goes over a list of Injectors and picks the first one that is able to resolve something, couldn’t this lead to unwanted behaviour? Definitely! Therefore I would recommend using the specific annotations. You can find the specific annotations that come with Sling here. In our example discussed here, we should replace @Inject with @AemObject. This would make it also more clear! Some Injectors do an additional check-in their logic to NOT be executed/handled when @Inject is being used. Take this into consideration when using 3rd party annotations or even when writing an annotation yourself!

Now we know the origin of this now less magical injection, I’d like to conclude with a snippet that shows how you should actually expand your unit test in case you come across a similar situation:

The example above requires a bit more code since it depends on 2 OSGI services to be present to work properly. However, by adding one line of code, it’s possible to verify the actual AEM logic in your unit tests. It’s also a possibility to mock the AemObjectInjector. This can lead to strange behaviours. Remember how the first injector can resolve the annotated variable “wins”. This means it’s potentially possible to always return the mocked response for all injected variables.
Note: in the end, we are injecting the containing page in the sling model, NOT the current page.

So what happens in the 2 unit tests I showed earlier?

  1. Registering Page.class as an OSGI service

In this unit test, the page is registered as an OSGI service. Which means OSGI services could reference this now, but also Sling models can inject this OSGI service! This injector is provided OOTB by Sling: org.apache.sling.models.impl.injectors.OSGiServiceInjector.

So in this example, we are actually verifying if an OSGI service will be injected for the currentPage variable instead of the actual current page.

2. Adding a property “currentPage” to the Resource

Simple but effective, to make the test work, yet also NOT verifying the logic that is actually running on the AEM instance. Injection of JCR properties is probably one of the first things you will explore starting with Sling Models, so this should be no surprise to be working. This behavior is made possible by Sling providing org.apache.sling.models.impl.injectors.ValueMapInjector.

After many years working with AEM, I’m happy I’m still able to stumble into these weird little quirks that aren’t that weird anymore after looking a bit further. I hope this post will help you get a better understanding of how the injection in Sling models work.

--

--