【问题标题】:Http Sessions life cycle in TomcatTomcat 中的 Http Sessions 生命周期
【发布时间】:2011-08-04 07:56:20
【问题描述】:

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

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

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

其中 UserInfo 被定义为

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

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

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

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

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

【问题讨论】:

    标签: java tomcat


    【解决方案1】:

    当客户端第一次访问webapp和/或第一次通过request.getSession()获取HttpSession时,servlet容器创建一个新的HttpSession对象,生成一个长且唯一的ID(你可以通过session.getId()) 获取,并将其存储在服务器的内存中。 servlet 容器还在 HTTP 响应的 Set-Cookie 标头中设置一个 Cookie,其名称为 JSESSIONID,值为唯一的会话 ID。

    根据HTTP cookie 规范(体面的网络浏览器和网络服务器必须遵守的合同),客户端(网络浏览器)需要在随后的请求中通过 Cookie 标头将这个 cookie 发送回因为 cookie 是有效的(即唯一 ID 必须引用未过期的会话,并且域和路径是正确的)。使用浏览器的内置 HTTP 流量监视器,您可以验证 cookie 是否有效(在 Chrome / Firefox 23+ / IE9+ 中按 F12,然后检查网络/网络选项卡)。 servlet 容器将检查每个传入的 HTTP 请求的 Cookie 标头是否存在名称为 JSESSIONID 的 cookie,并使用其值(会话 ID)从服务器内存中获取关联的 HttpSession

    HttpSession 一直保持活动状态,直到它没有被使用超过&lt;session-timeout&gt; 中指定的超时值,web.xml 中的设置。超时值默认为 30 分钟。因此,当客户端不访问 Web 应用程序的时间超过指定的时间时,servlet 容器会丢弃会话。每个后续请求,即使指定了 cookie,也将无法再访问同一会话; servlet 容器将创建一个新会话。

    在客户端,只要浏览器实例正在运行,会话 cookie 就会保持活动状态。因此,如果客户端关闭浏览器实例(所有选项卡/窗口),则会话在客户端被丢弃。在新的浏览器实例中,与会话关联的 cookie 将不存在,因此将不再发送。这会导致创建一个全新的 HTTPSession,并开始使用一个全新的会话 cookie。

    【讨论】:

    • 您的答案包含所有正确的事实,描述了 Web 容器中的会话生命周期,我相信它会对某人有所帮助。但我不确定我是否看到返回“用户名列表以及每个用户当前使用了多少个 tomcat 会话”——这个问题的主要原因。
    【解决方案2】:

    由于这个问题的浏览量接近 5k,我认为提供一个可行的解决方案示例将是有益的。

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

    首先,您的 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;
            }
        }
    }
    

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

    您还需要一个 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());
        }
    }
    

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

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

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

    【讨论】:

    • 谢谢你,但你能解释一下在什么情况下,以及通过什么过程调用User.readObject()
    • @Framcis:如果您重新启动 Tomcat 并在 web.xml 中标有 ,则 Tomcat(或其他 Web 容器)将序列化您的会话属性,重新启动,然后从硬盘读取它。这是您需要反序列化器在全局映射中重新注册用户的地方。
    【解决方案3】:

    不要从头开始写东西,而是检查 psi-probe。

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

    这可能只是解决您的问题的简单方法。

    【讨论】:

    • 很棒的工具,谢谢。肯定会派上用场。不幸的是,我需要的功能是显示具有当前会话数的登录用户以及他们正在使用的客户/他们处于什么状态,以加快支持呼叫。
    • 嗯。刚刚看了你的代码,从发布的内容中不清楚问题是什么..你能发布一个更完整的例子吗?
    【解决方案4】:

    您是否发现在重新启动 Tomcat 后遇到问题? Tomcat 将在成功关闭期间将活动会话序列化到磁盘,然后在启动时反序列化 - 我不确定这是否会导致调用 HttpSessionListener.sessionCreated() 因为会话不是严格创建的,只是反序列化(这可能不正确(!),但可以相当容易地测试)。

    您是否也将您的结果与 Tomcat 管理器会话统计数据进行了比较?它会跟踪活动会话的数量,并且应该与您的数据相关联,如果没有,您就知道您的代码是错误的。

    另外,可能与您的问题无关,但是您使用 Hashtable 和 WeakHashMap 是否有充分的理由?如果我需要线程安全的 Map 实现,我倾向于使用 ConcurrentHashMap,它的性能要好得多。

    【讨论】:

    • ConcurrentHashMap 是个好主意。没有充分的理由,只是一些遗留的考虑,不再相关。我不认为涉及的服务器重新启动,但我会仔细检查 - 我的会话毕竟是短暂的。无论如何我必须解决这个问题,如果你是对的,我会标记你的答案。
    • 是什么让你认为你是 HttpSession 的临时对象?会话 Map 上的瞬态修饰符似乎是多余的,因为 UserInfo 类没有实现 Serializable (这是混乱所在吗?)。并且认为 HttpSession 作为地图的关键并不完全正确,因为它是一个接口,因此 equals() 和 hashcode() 依赖于 Tomcat(或其他应用程序服务器)选择的实现。您最好将密钥交换为会话 ID,并将值交换为 HttpSession。
    • UserInfo 实现了可序列化,因为这个 Web 应用程序在 HA 集群中工作,并且还可以在不丢弃会话的情况下“存活”重启(这也需要应用程序上下文属性的序列化)。在代码示例中省略它是我的错误。
    • HttpSession 作为 WeakHashMap 的键是不对的,我知道。这里的想法是通过不持有对会话的强引用来消除潜在的内存泄漏。如果我拿着它们,Tomcat 端每次错过的sessionDestroyed() 调用都会造成内存泄漏。通过使用弱引用,我确保它不会发生,并且 WeakHashMap 需要弱引用对象作为其工作的关键。我更喜欢 ConcurrentHashSet 之类的东西,其中包含围绕会话的弱包装器,但是对于这样一个简单的功能来说,它变得相当复杂。
    • 归根结底,我的方法不正确。瞬态会话映射使服务器重新启动后无法完全恢复,但序列化也不是一个选项。我无法确定为什么所有会话都从没有 SessionListener 调用的弱引用中消失,但是有太多关于 Web 容器的假设使该代码不适合生产。我确实证明了 SessionListener 在反序列化期间没有被调用(也不应该)。会话无声无息地消失而无需重新启动。对于所有正确的指针,我会将您的答案标记为正确。
    猜你喜欢
    • 2017-12-25
    • 2011-06-16
    • 2015-02-24
    • 2018-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-10
    • 2013-04-06
    相关资源
    最近更新 更多