Search code examples
javaglassfishejbjavabeanscdi

Injecting a SessionScoped Stateful bean in EntityListener


I'm trying to implement some sort of auditing in a Java EE JPA (2.0) application on GlassFish 3.

I have added a @EntityListeners annotation on my @MappedSuperclass entity, the listener has the @PrePersist and @PreUpdate annotation on its methods which are invoked happily at runtime.

In these methods, I'm trying to use (@Inject) a @Named, @Stateful, @SessionScoped bean (UserSession) in order to get current user's id. The listener class has no annotations at all.

The problem is that I can't get the UserSession bean injected; I always end up with a null value. To this time, I tried the plain @Inject UserSession us; which always injects a null value.I also tried UserSession us = (UserSession) ctx.lookup("java:global/application/module/UserSession"); which always returns a new object (I verified the constructor call, plus the object is empty).

I'm pretty sure I have missed something very important regarding CDI but I can't figure out what. Could someone please point me to the right direction?


Solution

  • I eventually found a workaround, which allows me to get a reference of the @Stateful bean:

    I created a @Named @Singleton @Startup bean SessionController which holds a local HashMap<String, UserSession> sessionMap with the references of my @Stateful beans:

    @Named
    @Singleton
    @Startup
    @ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
    public class SessionController {
    
    private HashMap<String, UserSession> sessionMap;
    
    @PostConstruct
    void init() {
        sessionMap = new HashMap<String, UserSession>();
    }
    
    @PreDestroy
    void terminate() {
        for (UserSession us : sessionMap.values()) {
            us.logoutCleanUp(); //This is annotated as @Remove
        }
        sessionMap.clear();
    }
    
    public void addSession(String sessionId, UserSession us) {
        sessionMap.put(sessionId, us);
        System.out.println("New Session added: " + sessionId);
    }
    
    public UserSession getCurrentUserSession() {
        FacesContext context = FacesContext.getCurrentInstance();
        String sessionId = ((HttpSession) context.getExternalContext().getSession(false)).getId();
        return sessionMap.get(sessionId);
    }
    

    }

    I add the references from within each bean's @PostConstruct method:

    public class UserSession implements Serializable {
    @Inject SessionController sc;
    ...
        @PostConstruct
        void init() {
        FacesContext context = FacesContext.getCurrentInstance();
        String sessionId = ((HttpSession) context.getExternalContext().getSession(true)).getId();
        sc.addSession(sessionId, this);
    }
    

    Notice the .getSession(true) which is required since the Session might not be created yet. Also notice that this is safely passed since the @PostConstruct is not the constructor...

    After all these, I can get the reference in my EntityListener (and any other place) like this:

    SessionController sc = (SessionController) new InitialContext().lookup("java:module/SessionController");
        UserSession us = sc.getCurrentUserSession();
    

    or like this in CDI beans

    @Inject SessionController sc;
    

    The only drawback I see is that this approach works well only for web applications (where FacesContext context = FacesContext.getCurrentInstance() is meaningful). Some of my beans (and finally my EntityListeners) are also exposed via @javax.jws.WebService as @Stateless beans. In this context (actually: absence of), my Singleton wouldn't work (haven't tested yet) since there is no sessionId of any kind (no session at all to be exact). I will have to use a workaround for this, possibly using SessionContext of the bean or inventing a usable sessionId of some sort. I will post back if I create something usable...