aemsling-models

What is the use of @postconstructor in AEM SlingModel concept?


When can I use @postconstructor when I'm using in the SlingModel?

@Model(adaptables=SlingHttpServletRequest.class)
public class MyModelTest {

    @Inject
    private PrintWriter out;

    @Inject
    @Named("log")
    private Logger logger;

    @PostConstruct
    protected void sayHelloTest() {
        logger.info("hello world");
    }
}

Solution

  • The documentation states:

    The @PostConstruct annotation can be used to add methods which are invoked upon completion of all injections:

    In the example code you provided the "injections", the documentation talks about, are the fields you have annotated with the @Inject annotation.

    When the Sling Model is instantiated those fields will be "injected" by Sling. That means you do not have to set those fields yourself, but Sling will take care of it. This is also referred to as Dependency Injection.

    To come back to your question: Once all those fields were injected by Sling the method with the @PostConstruct annotation will be called. Usually, this method is used by developers to do further initialisation of a Sling Model.

    The whole process looks like this:

    1. Sling creates a new instance of your model (e.g. new MyModelTest()).
    2. Sling injects all the dependencies you declared (see @Inject annotation).
    3. Sling will call the method annotated with the @PostConstruct annotation.

    The @PostConstruct annotation is basically a replacement for a constructor. If you would write a constructor for your model you would notice that when the constructor is called all the fields with the @Inject annotation are not set yet. If you would try to do further initialisation with those fields you would get a NullPointerException.

    That is why the @PostConstruct annotation was introduced. It allows you to do those further initialisation you would usually do in the constructor.

    Additional note

    Injecting dependencies into fields is referred to as "field injection". There is another way to inject those dependencies through the constructor. This is called "constructor injection".

    Personally, I like to use constructor injection with Sling Models when possible. Using constructor injection helps to increase immutability of your model, can help to reduce state and improve testability. That is something you usually should strive for in your code.

    Your code example would like follows with constructor injection:

    @Model(adaptables=SlingHttpServletRequest.class)
    public class MyModelTest {
    
        private final Logger logger;
        private final PrintWriter out;
    
        @Inject
        public MyModelTest(
            @ScriptVariable @Named("log") final Logger logger,
            @ScriptVariable @Named("out") final PrintWriter out
        ) {
            this.logger = logger;
            this.out = out;
        }
    }
    

    The class fields logger and out can now be set final which increases immutability because they can not be changed anymore.

    When you inject services etc. to do further initialisation you can store the result in a final class field as well instead of injecting the service reference itself as a class field. Those references - in theory - could point to a non-existing service (services can come and go in OSGi). For example: If you have a service that stores configurations you need, you could just read the configuration value you are interested in and then store it in a class field.

    Last but not least testability is improved because you can now create instances by simply calling new MyModelTest([...]) and passing a mock of logger and out (new MyModelTest(mockLogger, mockOut)). If you would use field injection you would have to use reflection which is normally not what you want to do in your code. Although it has to be said that the Sling project contains support for testing models that use field injection.