我有一项任务是向网站管理员显示一个用户名列表以及每个用户当前使用的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
地图中。
HttpSessionsListener
中的sessionDestroyed()
实现从此映射中删除了已销毁的会话,如果sessions.size()== 0,则从logins
中删除UserInfo。
我有时会为一些用户显示0个会话。同行评审和单元测试表明代码是正确的。所有会话都是可序列化的。
Tomcat是否有可能将会话从内存卸载到硬盘驱动器,例如当有一段不活动时间(会话超时设置为40分钟)?从GC的角度来看,还有其他会话被“丢失”的情况,但是没有调用HttpSessionsListener.sessionDestroyed()吗?
J2SE 6,Tomcat 6或7独立,行为在任何操作系统上都是一致的。
答案 0 :(得分: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;
}
}
}
User bean必须是Serializable,并且必须在反序列化时重新注册:
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中的静态映射,以了解正在运行的会话数,每个用户使用的会话数等等。理想情况下,您需要更复杂的数据结构才能保证其自身独立具有适当访问方法的单例类。根据用例,使用具有写时复制并发策略的容器也可能是一个好主意。
答案 1 :(得分:2)
答案 2 :(得分:1)
您是否发现重启Tomcat后出现问题? Tomcat会在成功关闭期间将活动会话序列化为磁盘,然后在启动时反序列化 - 我不确定这是否会导致对HttpSessionListener.sessionCreated()的调用,因为会话未严格创建,只是反序列化(这可能不正确(!),但可以相当容易地测试。)
您是否还将结果与Tomcat经理会话统计数据进行了比较?它跟踪活动会话的数量,并且应该与您的数据相关联,如果没有,您知道您的代码是错误的。
此外,可能与您的问题无关,但是您有充分的理由使用Hashtable和WeakHashMap吗?如果我需要一个线程安全的Map实现,我倾向于使用ConcurrentHashMap,它的性能要好得多。
答案 3 :(得分:1)
当客户端首次访问Web应用程序和/或通过request.getSession()
首次获得HttpSession时,Servlet容器将创建一个新的HttpSession
对象,并生成一个长且唯一的ID (您可以通过session.getId()
获得),并将其存储在服务器的内存中。 Servlet容器还在HTTP响应的Set-Cookie标头中设置了一个Cookie,其名称为JSESSIONID,其唯一的会话ID为其值。
根据HTTP cookie
规范(体面的Web浏览器和Web服务器必须遵守的合同),要求客户端(Web浏览器)在Cookie标头中的后续请求中将该Cookie发送回只要cookie有效(即,唯一ID必须引用未过期的会话,并且域和路径正确)。使用浏览器的内置HTTP流量监控器,您可以验证Cookie是否有效(在Chrome / Firefox 23 + / IE9 +中按F12,然后检查“网络/网络”标签)。 Servlet容器将检查每个传入的HTTP
请求的Cookie标头中是否存在名称为JSESSIONID
的cookie,并使用其值(会话ID)从中获取关联的HttpSession
服务器的内存。
HttpSession
保持活动状态,直到未使用超过<session-timeout>
中指定的超时值(web.xml
中的设置)为止。超时值默认为30分钟。因此,当客户端访问Web应用程序的时间不超过指定的时间时,Servlet容器将破坏会话。每个后续请求,即使指定了cookie,也将无法再访问同一会话。 servlet容器将创建一个新会话。
在客户端,只要浏览器实例正在运行,会话cookie就会保持活动状态。因此,如果客户端关闭浏览器实例(所有选项卡/窗口),则会话将被丢弃在客户端侧。在新的浏览器实例中,与会话关联的cookie将不存在,因此将不再发送。这将导致创建一个全新的HTTPSession,并开始使用一个全新的会话cookie。