Tomcat 中的 Http Session 生命周期

Ale*_*kka 4 java tomcat

我的任务是向站点管理员显示用户名列表以及每个用户当前使用的 Tomcat 会话数量(以及其他一些与支持相关的信息)。

我将经过身份验证的用户保留为应用程序上下文属性,如下所示(省略不必要的细节)。

Hashtable<String, UserInfo> logins //maps login to UserInfo
Run Code Online (Sandbox Code Playgroud)

其中 UserInfo 定义为

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

每次成功登录都会将会话存储到此sessions映射中。我的HttpSessionsListener实现sessionDestroyed()从该映射中删除了已销毁的会话,如果sessions.size() == 0,则从 中删除 UserInfo logins

有时我会为某些用户显示 0 个会话。同行评审和单元测试表明代码是正确的。所有会话都是可序列化的。

Tomcat 是否有可能将会话从内存卸载到硬盘驱动器,例如,当有一段时间不活动时(会话超时设置为 40 分钟)?从 GC 的角度来看,是否存在会话“丢失”但未调用 HttpSessionsListener.sessionDestroyed() 的其他情况?

J2SE 6、Tomcat 6 或 7 独立运行,在任何操作系统上的行为都是一致的。

Ale*_*kka 5

由于这个问题的浏览量接近 5000 次,我认为提供一个可行的解决方案示例会很有帮助。

问题中概述的方法是错误的 - 它不会处理服务器重新启动,也不会扩展。这是一个更好的方法。

首先,您的 HttpServlet 需要处理用户登录和注销,大致如下:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

User bean 必须是可序列化的,并且必须在反序列化时重新注册自身:

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); 
    }

}
Run Code Online (Sandbox Code Playgroud)

您还需要一个 HttpAttributeListener 来正确处理会话生命周期:

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());
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,您需要在 web.xml 中注册您的监听器:

<listener>
    <listener-class>com.example.servlet.UserAttributeListener</listener-class>
</listener>
Run Code Online (Sandbox Code Playgroud)

之后,您始终可以访问 UserAttributeListener 中的静态映射,以了解正在运行的会话数量、每个用户正在使用的会话数量等。理想情况下,您将拥有更复杂的数据结构,以保证其自己单独的单例类正确的访问方法。使用具有写时复制并发策略的容器也可能是一个好主意,具体取决于用例。