开发者

Http Sessions life cycle in Tomcat

I have a task to show to the site admin a list of user names and how many tomcat sessions are currently utilized by each user (along with some other support-related information).

I keep authenticated users as the application context attribute as follows (sparing unnecessary details).

Hashtable<String, UserInfo> logins //maps login to UserInfo

where UserInfo is defined as

class UserInfo implements Serializable {
String login;
private transient Map<HttpSession, String> sessions = 
      Collections.synchronizedMap(
             new WeakHashMap<HttpSession, String>() //maps session to sessionId
      );
...
}

Each successful login stores the session into this sessions map. My HttpSessionsListener implementation in sessionDestroyed() removes the destroyed session from this map and, if sessions.size() == 0, removes UserInfo from logins.

From time to time I have 0 sessions showing up for some users. Peer reviews and unit testing show that the code is correct. All sessions are ser开发者_高级运维ializable.

Is it possible that Tomcat offloads sessions from memory to the hard drive, e.g. when there is a period of inactivity (session timeout is set to 40 minutes)? Are there any other scenarios where sessions are 'lost' from GC point of view, but HttpSessionsListener.sessionDestroyed() wasn't invoked?

J2SE 6, Tomcat 6 or 7 standalone, behaviour is consistent on any OS.


As this question got close to 5k views, I think it would be beneficial to provide an example of a working solution.

The approach outlined in the question is wrong - it will not handle server restarts and will not scale. Here is a better approach.

First, your HttpServlet needs to handle user logins and logouts, something along these lines:

public class ExampleServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) 
              throws ServletException, IOException {
        String action = req.getParameter("do"); 
        HttpSession session =  req.getSession(true);
        //simple plug. Use your own controller here.
        switch (action) {
            case "logout":
                session.removeAttribute("user");
                break;
            case "login":
                User u = new User(session.getId(), req.getParameter("login"));
                //store user on the session
                session.setAttribute("user",u);                    
                break;
        }
    }
}

The User bean has to be Serializable and has to re-register itself upon de-serialization:

class User implements Serializable {

    private String sessionId;
    private String login;

    User(String sessionId, String login) {
        this.sessionId = sessionId;
        this.login = login;
    }

    public String getLogin() { return login; }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        //re-register this user in sessions 
        UserAttributeListener.sessions.put(sessionId,this); 
    }

}

You will also need an HttpAttributeListener to handle session lifecycle properly:

public class UserAttributeListener implements HttpSessionAttributeListener {

    static Map<String, User> sessions = new ConcurrentHashMap<>();

    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        if ("user".equals(event.getName()))
             sessions.put(event.getSession().getId(), (User) event.getValue());
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        if ("user".equals(event.getName()))
            ExampleServlet.sessions.remove(event.getSession().getId());
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {
        if ("user".equals(event.getName()))
            ExampleServlet.sessions.put(event.getSession().getId(),
                      (User)event.getValue());
    }
}

Of course, you will need to register your listener in web.xml:

<listener>
    <listener-class>com.example.servlet.UserAttributeListener</listener-class>
</listener>

After that, you can always access the static map in UserAttributeListener to get an idea of how many sessions are running, how many sessions each user is using etc. Ideally, you would have a bit more complex data structure warranting its own separate singleton class with proper access methods. Using containers with copy-on-write concurrent strategy might also be a good idea, depending on the use case.


Instead of writing something from scratch, check out psi-probe.

http://code.google.com/p/psi-probe/

This may just be a simple drop in that solves your problems.


Do you find that you get the issue following a restart of Tomcat? Tomcat will serialize active sessions to disk during a successful shutdown and then deserialize on startup - I'm not sure whether this will result in a call to your HttpSessionListener.sessionCreated() as the session isn't strictly created, just deserialized (this may be not be correct(!), but can be tested fairly easily).

Have you also compared your results with the Tomcat managers session stats? It keeps track of the number of active sessions, and should tie up with your figures, if not, you know your code is wrong.

Also, probably unrelated to your issue, but is there a good reason why you are using Hashtable and WeakHashMap? I tend to go with ConcurrentHashMap if i need a thread safe Map implementation, its performance is much better.


When a client visits the webapp for the first time and/or the HttpSession is obtained for the first time via request.getSession(), the servlet container creates a new HttpSession object, generates a long and unique ID (which you can get by session.getId()), and store it in the server's memory. The servlet container also sets a Cookie in the Set-Cookie header of the HTTP response with JSESSIONID as its name and the unique session ID as its value.

As per the HTTP cookie specification (a contract a decent web browser and web server have to adhere to), the client (the web browser) is required to send this cookie back in subsequent requests in the Cookie header for as long as the cookie is valid (i.e. the unique ID must refer to an unexpired session and the domain and path are correct). Using your browser's built-in HTTP traffic monitor, you can verify that the cookie is valid (press F12 in Chrome / Firefox 23+ / IE9+, and check the Net/Network tab). The servlet container will check the Cookie header of every incoming HTTP request for the presence of the cookie with the name JSESSIONID and use its value (the session ID) to get the associated HttpSession from server's memory.

The HttpSession stays alive until it has not been used for more than the timeout value specified in <session-timeout>, a setting in web.xml. The timeout value defaults to 30 minutes. So, when the client doesn't visit the web app for longer than the time specified, the servlet container trashes the session. Every subsequent request, even with the cookie specified, will not have access to the same session anymore; the servlet container will create a new session.

On the client side, the session cookie stays alive for as long as the browser instance is running. So, if the client closes the browser instance (all tabs/windows), then the session is trashed on the client's side. In a new browser instance, the cookie associated with the session wouldn't exist, so it would no longer be sent. This causes an entirely new HTTPSession to be created, with an entirely new session cookie begin used.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜