5

Good evening,

In a test JSF 2.0 web app, I am trying to get the number of active sessions but there is a problem in the sessionDestroyed method of the HttpSessionListener. Indeed, when a user logs in, the number of active session increases by 1, but when a user logs off, the same number remains as it is (no desincrementation happens) and the worse is that, when the same user logs in again (even though he unvalidated the session), the same number is incremented. To put that in different words :

1- I log in, the active sessions number is incremented by 1. 2- I Logout (the session gets unvalidated) 3- I login again, the sessions number is incremented by 1. The display is = 2. 4- I repeat the operation, and the sessions number keeps being incremented, while there is only one user logged in.

So I thought that method sessionDestroyed is not properly called, or maybe effectively called after the session timeout which is a parameter in WEB.XML (mine is 60 minutes). That is weird as this is a Session Listener and there is nothing wrong with my Class.

Does someone please have a clue?

package mybeans;

import entities.Users;
import java.io.*;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import jsf.util.JsfUtil;

/**
 * Session Listener.
 * @author TOTO
 */
@ManagedBean
public class SessionEar implements HttpSessionListener {

    public String ctext;
    File file = new File("sessionlog.csv");
    BufferedWriter output = null;
    public static int activesessions = 0;
    public static long creationTime = 0;
    public static int remTime = 0;
    String separator = ",";
    String headtext = "Session Creation Time" + separator + "Session Destruction Time" + separator + "User";

    /**
     * 
     * @return Remnant session time
     */
    public static int getRemTime() {
        return remTime;
    }

    /**
     * 
     * @return Session creation time
     */
    public static long getCreationTime() {
        return creationTime;
    }

    /**
     * 
     * @return System time
     */
    private String getTime() {
        return new Date(System.currentTimeMillis()).toString();
    }

    /**
     * 
     * @return active sessions number
     */
    public static int getActivesessions() {
        return activesessions;
    }

    @Override
    public void sessionCreated(HttpSessionEvent hse) {
        //  Insert value of remnant session time
        remTime = hse.getSession().getMaxInactiveInterval();

        // Insert value of  Session creation time (in seconds)
        creationTime = new Date(hse.getSession().getCreationTime()).getTime() / 1000;
        if (hse.getSession().isNew()) {
            activesessions++;
        } // Increment the session number
        System.out.println("Session Created at: " + getTime());
        // We write into a file information about the session created
        ctext = String.valueOf(new Date(hse.getSession().getCreationTime()) + separator);
        String userstring = FacesContext.getCurrentInstance().getExternalContext().getRemoteUser();

// If the file does not exist, create it
        try {
            if (!file.exists()) {
                file.createNewFile();

                output = new BufferedWriter(new FileWriter(file.getName(), true));
                // output.newLine();
                output.write(headtext);
                output.flush();
                output.close();
            }

            output = new BufferedWriter(new FileWriter(file.getName(), true));
            //output.newLine();
            output.write(ctext + userstring);
            output.flush();
            output.close();
        } catch (IOException ex) {
            Logger.getLogger(SessionEar.class.getName()).log(Level.SEVERE, null, ex);
            JsfUtil.addErrorMessage(ex, "Cannot append session Info to File");
        }

        System.out.println("Session File has been written to sessionlog.txt");

    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // Desincrement the active sessions number
            activesessions--;


        // Appen Infos about session destruction into CSV FILE
        String stext = "\n" + new Date(se.getSession().getCreationTime()) + separator;

        try {
            if (!file.exists()) {
                file.createNewFile();
                output = new BufferedWriter(new FileWriter(file.getName(), true));
                // output.newLine();
                output.write(headtext);
                output.flush();
                output.close();
            }
            output = new BufferedWriter(new FileWriter(file.getName(), true));
            // output.newLine();
            output.write(stext);
            output.flush();
            output.close();
        } catch (IOException ex) {
            Logger.getLogger(SessionEar.class.getName()).log(Level.SEVERE, null, ex);
            JsfUtil.addErrorMessage(ex, "Cannot append session Info to File");
        }

    }
} // END OF CLASS

I am retrieving the active sessions number this way:

<h:outputText id="sessionsfacet" value="#{UserBean.activeSessionsNumber}"/> 

from another managedBean:

public String getActiveSessionsNumber() {
        return String.valueOf(SessionEar.getActivesessions());
    }

My logout method is as follow:

 public String logout() {
        HttpSession lsession = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false);
        if (lsession != null) {
            lsession.invalidate();
        }
        JsfUtil.addSuccessMessage("You are now logged out.");
        return "Logout";
    }
    // end of logout
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Hanynowsky
  • 2,970
  • 5
  • 32
  • 43

3 Answers3

10

I'm not sure. This seems to work fine for a single visitor. But some things definitely doesn't look right in your HttpSessionListener.


@ManagedBean
public class SessionEar implements HttpSessionListener {

Why is it a @ManagedBean? It makes no sense, remove it. In Java EE 6 you'd use @WebListener instead.


    BufferedWriter output = null;

This should definitely not be an instance variable. It's not threadsafe. Declare it methodlocal. For every HttpSessionListener implementation there's only one instance throughout the application's lifetime. When there are simultaneous session creations/destroys, then your output get overridden by another one while busy and your file would get corrupted.


    public static long creationTime = 0;
    public static int remTime = 0;

Those should also not be an instance variable. Every new session creation would override it and it would get reflected into the presentation of all other users. I.e. it is not threadsafe. Get rid of them and make use of #{session.creationTime} and #{session.maxInactiveInterval} in EL if you need to get it over there for some reason. Or just get it straight from the HttpSession instance within a HTTP request.


    if (hse.getSession().isNew()) {

This is always true inside sessionCreated() method. This makes no sense. Remove it.


        JsfUtil.addErrorMessage(ex, "Cannot append session Info to File");

I don't know what that method exactly is doing, but I just want to warn that there is no guarantee that the FacesContext is present in the thread when the session is about to be created or destroyed. It may take place in a non-JSF request. Or there may be no means of a HTTP request at all. So you risk NPE's because the FacesContext is null then.


Nonetheless, I created the following test snippet and it works fine for me. The @SessionScoped bean implicitly creates the session. The commandbutton invalidates the session. All methods are called as expected. How many times you also press the button in the same browser tab, the count is always 1.

<h:form>
    <h:commandButton value="logout" action="#{bean.logout}" />
    <h:outputText value="#{bean.sessionCount}" />
</h:form>

with

@ManagedBean
@SessionScoped
public class Bean implements Serializable {

    public void logout() {
        System.out.println("logout action invoked");
        FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
    }

    public int getSessionCount() {
        System.out.println("session count getter invoked");
        return SessionCounter.getCount();
    }

}

and

@WebListener
public class SessionCounter implements HttpSessionListener {

    private static int count;

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        System.out.println("session created: " + event.getSession().getId());
        count++;
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        System.out.println("session destroyed: " + event.getSession().getId());
        count--;
    }

    public static int getCount() {
        return count;
    }

}

(note on Java EE 5 you need to register it as <listener> in web.xml the usual way)

<listener>
    <listener-class>com.example.SessionCounter</listener-class>
</listener>

If the above example works for you, then your problem likely lies somewhere else. Perhaps you didn't register it as <listener> in web.xml at all and you're simply manually creating a new instance of the listener everytime inside some login method. Regardless, now you at least have a minimum kickoff example to build further on.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • meticulous as usual, it is great stackoverflow has you! Yes, I am under Java-EE-5, and the listener is registred in the web.xml. You're right through the whole line for un-needed extras on listener. Actually those were for testing. I now will test your snippet and feedback! – Hanynowsky Jun 10 '11 at 22:51
  • Tested with your snippet and it makes no difference! I modified the Listener and The CSV file is appended correctly. Also, using System.out gives me a correct trace when session is created or destroyed (the 2 methods are effectively called). Actually, When the user logs out, the session is unvalidated but as faces-config redirects to login page upon logout, a new session is created automatically and I found out that session timeout and time creation are related to the first session created when automatic redirection to login page happens. – Hanynowsky Jun 12 '11 at 19:17
  • Besides, I am using Apache JK mode to be able to use the web application in a network `(http://someip/mywebapp)` instead of (http://localhost:myport/mywebapp). This may be for something as I was getting continuously a `NullPointerException` generated by Apache from the listener and this is because (`FaceContext.......getRemoteUser()` ) returns null before the user logs in! Still the getCount() is incremented more and more even though the session is destroyed! Well I have to profile this in a simple web app without Apache and check! – Hanynowsky Jun 12 '11 at 19:17
  • the increment/desincrement operations work fine. The issue is that : - When the LOGIN page is displayed a session is created, so before the user logs in, the sessions count is incremented by one, and this session is never destroyed as long as the session timeout does not expire. When the user logs in the incrementation happens and when he/she logs out, the desincrementation happens but only for the user session. – Hanynowsky Jun 14 '11 at 16:03
  • In other words, you're incrementing the counter as well in the `login()` method or something? Why? Do you want to count the logged-in users? If so, then you should not count the sessions, but the logged-in users. The session does not represent a logged-in user, it just represents a HTTP session between client and server. To count the logged-in users, check my answer on your previous question: http://stackoverflow.com/questions/6141237/jsf-j-security-check-how-to-get-number-of-connected-users-and-their-role – BalusC Jun 14 '11 at 16:08
  • @BalusC to increment and decrement like that, is it thread safe? Thanks for the great answer. – Karl Kildén Nov 05 '12 at 18:43
  • 1
    @Karl: The increment/decrement is threadsafe, yes. The only "problem" may be in retrieving the result. If the result is retrieved at the same moment as an increment or decrement is done, then there's no guarantee that the result is accurate. If the accuracy of the result is however a strong requirement, use `AtomicInteger` instead of `int`. In this particular use case, I would however personally not care about the result sometimes being 1 on or off or so. – BalusC Nov 05 '12 at 20:40
  • @BalusC I see. Thanks for suggesting AtomicInteger - great to have in mind for more complex requirements. If I understood you correctly it is practically thread safe because any "problem" is a non issue. However if the practical use case was more complex and did the wrong stuff it could be a problem right? Speaking specifically of a static field like that. – Karl Kildén Nov 05 '12 at 21:01
3

Something in a completely different direction - tomcat supports JMX. There is a JMX MBean that will tell you the number of active sessions. (If your container is not tomcat, it should still support JMX and provide some way to track that)

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • My Container is Glassfish 3.1, I am going to investigate how to retrieve the information using JMX! Thanks for that useful hint. – Hanynowsky Jun 10 '11 at 22:33
1

Is your public void sessionDestroyed(HttpSessionEvent se) { called ? I don't see why it won't increment. After the user calls session.invalidate() through logout, the session is destroyed, and for the next request a new one is created. This is normal behavior.

Cosmin Cosmin
  • 1,526
  • 1
  • 16
  • 34
  • Yes, it is called since the csv file is appended when the session is destroyed. I am going to tweak back the Listener as advised by @BalusC and see the results. – Hanynowsky Jun 10 '11 at 22:46