0

I'm facing a problem with a conditional observer method that is not being called. Here is the code, starting with a junit test:

    import static org.hamcrest.CoreMatchers.notNullValue;
    import static org.hamcrest.CoreMatchers.nullValue;
    import static org.junit.Assert.assertThat;

    import javax.enterprise.inject.Instance;
    import javax.enterprise.inject.se.SeContainer;
    import javax.enterprise.inject.se.SeContainerInitializer;

    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.junit.After;
    import org.junit.Before;
    import org.junit.Test;

    public class CDIMinimalConditionalObserverTest
    {
        private final static Logger LOGGER = LogManager.getLogger(CDIMinimalConditionalObserverTest.class);

        private SeContainer container;

        @Before public void before()
        {
            LOGGER.debug("before");
        final SeContainerInitializer initialiser = SeContainerInitializer.newInstance();
        container = initialiser.initialize();
    }

    @After public void after()
    {
        container.close();
        LOGGER.debug("after");
    }

    @Test public void testObservation_observationInManagedNonExistentConditionalObservers()
    {
        CDIMinimalConditionalObserverEvent event = new CDIMinimalConditionalObserverEvent();
        container.getBeanManager().fireEvent(event);
        assertThat(event.msg, nullValue());
    }

    @Test public void testObservation_observationInManagedExistentConditionalObservers()
    {
        // create observer by selection
        Instance<CDIMinimalConditionalObserver> instance = container.select(CDIMinimalConditionalObserver.class);
        CDIMinimalConditionalObserver observer = instance.get();
        assertThat(observer, notNullValue());

        CDIMinimalConditionalObserverEvent event = new CDIMinimalConditionalObserverEvent();
        container.getBeanManager().fireEvent(event);
        observer.doSomething();
        assertThat(event.msg, notNullValue());
    }
}

Here is the class with the conditional observer method:

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.jmda.sandbox.cdi.se.CDIMinimalTests.SimpleInnerEvent;

/**
 * {@link Model} annotation assigns non-dependent scope and thereby makes it possible to make {@link
 * #observation(SimpleInnerEvent)} conditional
 */
@ApplicationScoped public class CDIMinimalConditionalObserver
{
    private final static Logger LOGGER = LogManager.getLogger(SimpleConditionalObserver.class);

    public CDIMinimalConditionalObserver()
    {
        LOGGER.debug("constructor");
    }

    @PostConstruct public void postConstruct()
    {
        LOGGER.debug("post construct");
    }

    @PreDestroy public void preDestroy()
    {
        LOGGER.debug("pre destroy");
    }

    public void observation(@Observes(notifyObserver=Reception.IF_EXISTS) CDIMinimalConditionalObserverEvent event)
    {
        event.msg = "observation";
        LOGGER.debug(event.msg);
    }

    public void doSomething()
    {
        LOGGER.debug("doing something");
    }
}

And finally here is the event class:

public class CDIMinimalConditionalObserverEvent { String msg; }

The test fails because event.msg is null though it shouldn't be. Logging output does not show any "observation" output. The test passes if the condition is removed.

Any ideas? Thanks!

ruu
  • 51
  • 1
  • 6

1 Answers1

0

When your @ApplicationScoped Bean gets discovered, it does not get instantiated immediately.
CDI is smart enough to initialize the real object, the one behind the scene, only when needed.

You can see that retrieving an ApplicationScoped Bean via

final Instance<App> select = container.select(App.class);
final App app = select.get();

Does indeed return a proxy instance.
At this stage there is still no App Bean attached to the Application context.

enter image description here

Now, try to interact with that object (even just by calling toString), and only after, fire the event.
You'll notice it does work, because the underlying object has been instantiated via its no-arg constructor.

Removing Reception.IF_EXISTS simply signal CDI that it has to create and attach to the context the underlying instance immediatly, so that it can accept incoming events no matter what.

This proxying behavior is documented in the specification (I need to find the page), and it's why a Bean requires a no-arg constructor.

Dependent scope Beans don't suffer of this problem, as they're created every time it is needed, from scratch, and are not tracked by the framework. For singleton, session, or request scoped Beans, a proxy is required to manage them correctly.


Dependent scope Bean, you can see it's a "pure" instance

enter image description here

LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • @ruu Yeah, to understand really thoroughly CDI you have to read a bit of the spec. However I have only positive things to say about CDI. For DI it's better then Spring. – LppEdd Mar 12 '19 at 18:16
  • I added a "doSomething()" call to the observer just before I fire the event and then the observer method is being called as desired. Thanks a lot, your explanation is excellent! However I must say that this seems pretty weird. I want to avoid CDI constructing instances for each observer method it is able to discover. That's why I made the observer method conditional. But conditional methods are not allowed in dependent beans. That's why I made my bean ApplicationScoped. This now forces me to do the trick you described. What's worse is that CDI ignores the event absolutely silently!!! – ruu Mar 12 '19 at 19:08
  • 1
    Sorry @LppEdd, I replaced my original comment while you answered to it. Our conversation now seems a little strange ;) – ruu Mar 12 '19 at 19:10
  • Did you notice that @PostConstruct methods do not help? – ruu Mar 12 '19 at 19:14
  • @ruu If you remove the Observer condition, and still apply ApplicationScoped, only a single instance will be around. No other will ever be constructed. This single instance would be around anyway, even without the Observer, it's just that it might have been instantiated later in time. – LppEdd Mar 12 '19 at 19:21
  • @ruu well, PostConstruct and PreDestroy are tied to the Bean instantiation and destruction, so they do not change the behavior of CDI – LppEdd Mar 12 '19 at 19:22
  • Yes, apparently if ApplicationScoped is applied I neither need to call a fake method ;) before firing an event nor the condition is necessary. I don't understand what you say about bean instantiation. I create the bean via beanManager.select() and have a PostConstruct method being called. Somehow the proxy seems to handle the PostConstruct method and the observer method does not get called (in the original constellation). – ruu Mar 12 '19 at 19:52
  • @ruu in my case the PostConstruct method isn't called at all. Only if I actually use the Bean (e.g. call toString). Which is consistent with the behavior I exposed in the answer. – LppEdd Mar 12 '19 at 20:07