@PostConstruct is not invoked for @ApplicationScoped on initialisation?
Asked Answered
P

4

10

I have faced with the following issue. I am using Weld implementation of the CDI.

I have found that if a service is annotated with @ApplicationScoped then @PostConstruct section is not invoked until the first usage of the service. Here is a code to reproduce this behaviour:

import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer; 

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.CDI;

public class TestCdi {

    public static void main(String[] args) {
        try (WeldContainer weldContainer = new Weld().containerId("test").initialize()) {
             FooService fooService = CDI.current().select(FooService.class).get();

             fooService.test();
             System.out.println("Done");
        }
    }

    @ApplicationScoped
    public static class FooService {

        @PostConstruct
        public void  init() {
            System.out.println("Post construct");
        }

        public void test() {
            System.out.println("test");
        }
    }
}

So, if fooService.test(); is commented, then FooService.init() is not invoked. But remove @ApplicationScoped and it is working again!

This seems strange for me and I can't find and description of such behaviour.

Furthermore, the specification of javax.inject.Provider.get() says that:

Provides a fully-constructed and injected instance of T.

So, what's the issue? Is it designed so or this is a bug? And what is more important for me: how to bypass this issue? I need my service to be @ApplicationScoped.

Polynomial answered 14/3, 2018 at 13:19 Comment(2)
Why does it matter to you when the @PostConstruct method is called?Lordly
@SteveC For the two reasons. 1. Practical one. There is some logic inside my init method which should be performed immediately after service initialisation. Say I use some fragile resource for init and want be sure that it will be used in the init stage, but not in the middle of my app work. and 2) I am curios about reasons for this inconsistencyPolynomial
K
14

What you are seeing is Weld's lazy approach to bean initialization. With all normal scoped beans (anything except @Dependent from CDI-provided scopes), you in fact inject a proxy which delegates calls to contextual instance. And until you try to invoke any bean method on that proxy, the contextual instance is not created.

CDI specification does not mandate beans to be eager or lazy, this is implementation-based choice (I am not sure whether Weld docs mention this now). In case of Weld this is mainly performance choice as many of those beans would be initialized for nothing (never used, for instance) and it would slow down bootstrap a lot.

Please note that this is not an inconsistent state, it works like this for every scope Weld provides. It is also not a contradiction to javax.inject.Provider.get() as it does not state that @PostConstruct has to be invoked before you get the instance back. Furthermore the instance you in fact get is the proxy instance and that one is fully initialized anyway.

So it boils to to general problem of lazy versus eager init and which is better and/or which feels more natural.

As for a "solution":

  • You can use EJB's @javax.ejb.Singleton and use @Startup annotation. This will behave pretty much like @ApplicationScoped would so it might be good enough if you are in EE environment of course.
  • Or you can create a dummy ping() method on your @ApplicationScoped bean and invoke it as soon as your application starts. This will force the creation of the bean hence invoking @PostConstruct - much like you did with test() method in your code sample above.

As a side note - in your example the @Inject annotation on your constructor is of no use. It is only required for constructors with params.

Kemp answered 14/3, 2018 at 14:53 Comment(12)
I agree about @Inject annotation, but do not agree about the rest.Polynomial
1. It doesn't produce a proxy in case is there is no explicitly specified scope (just remove @ApplicationScope and observe the result).Polynomial
2. You haven't explained why does not this behaviour comply with javax.Provider.get() specification which states that it returns a fully-constructed instancePolynomial
I'll remove my upvote as soon as you provide the full answer on my questionsPolynomial
1. Obviously, you need normal scoped bean to create a proxy, @ApplicationScoped is normal scope. If you remove it, you get default, which is @Dependent and that is not normal scoped, hence no proxy in place.Kemp
This answer on the first question is accepted ;) Please include it into your entire answer. Go on ;)Polynomial
1. + 2. CDI.current().select(FooService.class).get() -> gives you a proxy (because its normal scoped bean). This proxy is a fully constructed instance indeed, but still a proxy which only delegates calls to actual underlying contextual instance. Until you first invoke any method on the contextual instance the proxy delegates to, the contextual instance will not be created.Kemp
This second one is not accepted. I still see a contradiction: the specification of javax.inject.Provider.get() says that: Provides a fully-constructed and injected instance of T.Polynomial
Well don't accept it then ;-) I am merely telling you how things are with this. T here is still going to be a proxy instance, not the underlying contextual instance. And proxy just is fully constructed. And even if you deny that, then Provider doesn't mandate that PostConstruct is called before you get the object. Once you try invoking anything on the proxy, it will work and you truly have a fully-constructed and injected instance. What you are dealing with is just an optimization.Kemp
It's just a different approach than you would perhaps expect; things can be either lazy or eager by default. When it comes to bigger deployments, eager doesn't really perform well, so Weld chooses to be lazy. It does not change the result though.Kemp
Okay than. Please update your answer with these details, so I could remove my downvote.Polynomial
Done, thought I am not sure if it's visible right away. Hope it clears out your doubts. If you feel like there should be an option to enforce eager init on @ApplicationScoped beans, please create CDI or Weld JIRA issue, it might be useful.Kemp
M
5

The other answers seem to be outdated. What worked for me was adding a method that takes an @Observes jakarta.enterprise.event.Startup parameter:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@ApplicationScoped
public class SystemStartStop {
  
  /**
   * The parameter `@Observes Startup` makes sure that the dependency injection framework
   * calls this method on system startup. And to do that, it needs to
   * call `@PostConstruct start()` first.
   */
  private void forceEagerInitialization(@Observes Startup startup) {
  }

  @PostConstruct
  public void start() {
    // This runs first.
  }

  @PreDestroy
  public void stop() {
    // This runs when the server shuts down.
  }
}

You need to be careful to import the annotations from the correct package, as some of them have deprecated versions that no longer work.

Martinmartina answered 15/6, 2022 at 8:51 Comment(1)
Thanks, it works. The @PostConstruct method worked for two days now without any @Observable in my project, until it stopped out of a sudden. But is this behavior by intention or rather a bug? Makes no sense to me.Shwa
P
2

User @PostConstruct annotation + @Observes ContainerInitialized event

  1. Use @PostConstruct to make sure instance will be properly initialized
@PostConstruct
public void setup() {
    // will be executed before it's going to be injected somewhere
}
  1. Use ContainerInitialized CDI event listening to make sure initialisation process eventually will be done during container startup:
private void on(@Observes ContainerInitialized event) {
    // will be executed during container bootstrap
}

You can use one of these or both, depends on your needs...

PostConstruct (if needed) happens earlier then ContainerInitialized event will occur, but! PostConstruct is so called Lazy, while ContainerInitialized event will be eventually produced during application bootstrap with 100% guarantees. You can control which bean is going to be initialized before its usage, or you can be just make sure that start up process is going to trigger some business logic depends on your needs.

In you case, use is 2 in 1:

public class TestCdi {

    public static void main(String[] args) {
        try (WeldContainer weldContainer = new Weld().containerId("test").initialize()) {
            System.out.println("Done");
        }
    }

    @ApplicationScoped
    public static class FooService {

        // with @PostConstruct you will make sure
        // your bean is going to be properly configure on its creation...
        @PostConstruct
        public void init() {
            System.out.println("Post construct");
        }

        // with @Observes ContainerInitialized event you will make sure
        // needed business logic will be executed on container startup...
        private void test(@Observes ContainerInitialized event) {
            System.out.println("test");
        }
    }
}

Regards

Ptolemaic answered 31/8, 2019 at 18:39 Comment(1)
Curiously, the method with @PostConstruct invoked only with sibling (@Observes ...). Thanks!Rambort
S
0

CDI is very extensible. An example of this is given in this other stackoverflow Q/A: CDI Eager Application scoped bean where they create an @eager annotation to be used. Although this other question is in the scope of JSF, the answer is generically applicable

Skyway answered 1/9, 2019 at 7:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.