2011-04-13 34 views
4

我有一个任务,向站点管理员显示用户名列表以及每个用户当前使用多少个Tomcat会话(以及一些其他支持相关信息)。Tomcat中的Http Sessions生命周期

我保持身份验证用户作为应用程序上下文属性如下(省略不必要的细节)。

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地图。 我的HttpSessionsListener实现sessionDestroyed()从此映射中删除销毁的会话,并且如果sessions.size()== 0从logins中删除UserInfo。

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

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

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

回答

1

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

你是否还将结果与Tomcat经理会话统计进行了比较?它会跟踪活动会话的数量,并且应该与您的数字保持一致,否则,您知道您的代码是错误的。

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

+0

ConcurrentHashMap是个不错的主意。没有什么理由,只是一些遗留的考虑,不应再相关。我不认为涉及的服务器重新启动,但我会仔细检查 - 我的会话毕竟是暂时的。无论如何我必须解决这个问题,如果你是对的,我会标记你的答案。 – 2011-04-14 14:10:35

+0

是什么让你认为你是HttpSession的瞬态?会话映射上的transient修饰符似乎是多余的,因为UserInfo类没有实现Serializable(这是混淆的地方吗?)。而后来认为HttpSession作为一个映射关键字并不完全正确,因此它的equals()和hashcode()依赖于Tomcat(或其他应用服务器)选择的实现。将交换密钥作为会话ID和值作为HttpSession将会更好。 – martin 2011-04-14 20:38:25

+0

UserInfo实现了Serializable,因为这个Web应用程序可以在HA集群中工作,并在不删除会话(也需要序列化应用程序上下文属性)的情况下“继续”重新启动。在代码示例中省略它是我的错误。 – 2011-04-20 17:02:06

1

而不是从头开始写东西,请检查psi探针。

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

这可能仅仅是一个简单的降解决您的问题。

+0

真棒工具,谢谢。肯定会派上用场。不幸的是,我需要的功能是向登录用户显示当前的会话数量以及他们使用的客户端/他们在加速支持呼叫时所处的状态。 – 2011-04-21 17:15:32

+0

嗯。只是看着你的代码,不知道发布什么是什么问题。你能发布一个更完整的例子吗? – 2011-04-21 19:38:08

3

由于这个问题接近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; 
     } 
    } 
} 

用户豆必须是序列化,并具有在反序列化到自己重新注册:

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的静态地图获得多少会话正在运行,有多少会话的想法每个用户都在使用等等。理想情况下,您会有一些更复杂的数据结构,保证自己的独立单例类具有适当的访问方法。取决于用例,使用具有写入时复制并发策略的容器也可能是一个好主意。

+0

谢谢你,但你能解释一下在什么情况下,通过什么过程调用User.readObject()? – Black 2015-07-13 12:46:53

+1

@Framcis:如果您重新启动Tomcat并使用标记web.xml,Tomcat(或其他Web容器)将序列化您的会话属性,然后重新启动,然后从硬盘读取它。这是您需要反序列化器在全局映射中重新注册用户的地方。 – 2015-07-13 12:59:51