实现当前登录在线人数统计
实现当前登录在线人数统计
设计思路
在线人数统计通常需要考虑以下几种情况:
- 用户正常登录
- 用户主动退出
- Session超时自动退出
- 服务器重启/应用重新加载
方案一:基于HttpSessionListener的实现(推荐)
1. 核心监听器
package com.example.listener;import javax.servlet.ServletContext;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** 在线用户统计监听器* 通过监听Session的创建和销毁来统计在线人数*/
@WebListener
public class OnlineUserListener implements HttpSessionListener {// 在线用户数(线程安全)private AtomicInteger onlineCount = new AtomicInteger(0);// 存储在线用户信息(SessionID -> 用户信息)private ConcurrentHashMap<String, UserSessionInfo> onlineUsers = new ConcurrentHashMap<>();/*** Session创建时触发(用户访问网站,无论是否登录)*/@Overridepublic void sessionCreated(HttpSessionEvent se) {HttpSession session = se.getSession();String sessionId = session.getId();// 初始化用户会话信息UserSessionInfo userInfo = new UserSessionInfo();userInfo.setSessionId(sessionId);userInfo.setCreateTime(System.currentTimeMillis());userInfo.setLastAccessTime(System.currentTimeMillis());userInfo.setIpAddress(getClientIP(session));onlineUsers.put(sessionId, userInfo);int count = onlineCount.incrementAndGet();updateApplicationContext(session, count);System.out.println("Session创建 - ID: " + sessionId + ", 当前在线会话数: " + count);}/*** Session销毁时触发(用户退出或Session超时)*/@Overridepublic void sessionDestroyed(HttpSessionEvent se) {HttpSession session = se.getSession();String sessionId = session.getId();// 从在线用户列表中移除UserSessionInfo removedUser = onlineUsers.remove(sessionId);if (removedUser != null) {int count = onlineCount.decrementAndGet();updateApplicationContext(session, count);System.out.println("Session销毁 - ID: " + sessionId + ", 用户: " + removedUser.getUsername() +", 剩余在线人数: " + count);// 记录用户在线时长long onlineDuration = System.currentTimeMillis() - removedUser.getLoginTime();System.out.println("用户 " + removedUser.getUsername() + " 在线时长: " + (onlineDuration / 1000) + "秒");}}/*** 用户登录成功时调用此方法*/public void userLoggedIn(HttpSession session, String username) {String sessionId = session.getId();UserSessionInfo userInfo = onlineUsers.get(sessionId);if (userInfo != null) {userInfo.setUsername(username);userInfo.setLoginTime(System.currentTimeMillis());userInfo.setLoggedIn(true);System.out.println("用户登录: " + username + ", SessionID: " + sessionId);// 更新登录用户数到应用上下文updateLoggedInUserCount(session, true);}}/*** 用户退出时调用此方法*/public void userLoggedOut(HttpSession session) {String sessionId = session.getId();UserSessionInfo userInfo = onlineUsers.get(sessionId);if (userInfo != null && userInfo.isLoggedIn()) {System.out.println("用户退出: " + userInfo.getUsername());userInfo.setLoggedIn(false);// 更新登录用户数updateLoggedInUserCount(session, false);}}/*** 更新应用上下文中的在线人数统计*/private void updateApplicationContext(HttpSession session, int totalCount) {ServletContext context = session.getServletContext();context.setAttribute("onlineSessionCount", totalCount);context.setAttribute("onlineUsersMap", onlineUsers);}/*** 更新已登录用户数*/private void updateLoggedInUserCount(HttpSession session, boolean isLogin) {ServletContext context = session.getServletContext();AtomicInteger loggedInCount = (AtomicInteger) context.getAttribute("loggedInUserCount");if (loggedInCount == null) {loggedInCount = new AtomicInteger(0);context.setAttribute("loggedInUserCount", loggedInCount);}int count = isLogin ? loggedInCount.incrementAndGet() : loggedInCount.decrementAndGet();context.setAttribute("loggedInUserCount", count);System.out.println("当前已登录用户数: " + count);}/*** 获取客户端IP地址*/private String getClientIP(HttpSession session) {try {// 在实际应用中,需要通过ServletRequest获取IP// 这里简化为示例javax.servlet.ServletRequest request = null;// 需要通过其他方式获取request,这里只是示例return "127.0.0.1";} catch (Exception e) {return "unknown";}}// 获取在线用户统计信息public int getOnlineCount() {return onlineCount.get();}public ConcurrentHashMap<String, UserSessionInfo> getOnlineUsers() {return onlineUsers;}
}
2. 用户会话信息类
package com.example.model;import java.io.Serializable;/*** 用户会话信息实体类*/
public class UserSessionInfo implements Serializable {private String sessionId;private String username;private boolean loggedIn;private long createTime;private long loginTime;private long lastAccessTime;private String ipAddress;// 构造函数public UserSessionInfo() {this.createTime = System.currentTimeMillis();this.lastAccessTime = this.createTime;}// getter 和 setter 方法public String getSessionId() { return sessionId; }public void setSessionId(String sessionId) { this.sessionId = sessionId; }public String getUsername() { return username; }public void setUsername(String username) { this.username = username; }public boolean isLoggedIn() { return loggedIn; }public void setLoggedIn(boolean loggedIn) { this.loggedIn = loggedIn; }public long getCreateTime() { return createTime; }public void setCreateTime(long createTime) { this.createTime = createTime; }public long getLoginTime() { return loginTime; }public void setLoginTime(long loginTime) { this.loginTime = loginTime; }public long getLastAccessTime() { return lastAccessTime; }public void setLastAccessTime(long lastAccessTime) { this.lastAccessTime = lastAccessTime; }public String getIpAddress() { return ipAddress; }public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }@Overridepublic String toString() {return "UserSessionInfo{" +"username='" + username + '\'' +", loggedIn=" + loggedIn +", onlineTime=" + ((System.currentTimeMillis() - loginTime) / 1000) + "s" +", ipAddress='" + ipAddress + '\'' +'}';}
}
3. 登录Servlet
package com.example.servlet;import com.example.listener.OnlineUserListener;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;/*** 用户登录Servlet*/
@WebServlet("/login")
public class LoginServlet extends HttpServlet {@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String username = request.getParameter("username");String password = request.getParameter("password");// 模拟用户验证if (authenticate(username, password)) {HttpSession session = request.getSession();// 将用户信息存入Sessionsession.setAttribute("currentUser", username);session.setAttribute("loginTime", System.currentTimeMillis());// 获取在线用户监听器并更新登录状态OnlineUserListener listener = getOnlineUserListener();if (listener != null) {listener.userLoggedIn(session, username);}// 登录成功,重定向到主页response.sendRedirect("index.jsp");} else {// 登录失败request.setAttribute("error", "用户名或密码错误");request.getRequestDispatcher("login.jsp").forward(request, response);}}/*** 用户验证(示例)*/private boolean authenticate(String username, String password) {// 实际项目中应该查询数据库进行验证return "admin".equals(username) && "123456".equals(password);}/*** 获取OnlineUserListener实例*/private OnlineUserListener getOnlineUserListener() {return (OnlineUserListener) getServletContext().getAttribute("onlineUserListener");}
}
4. 退出Servlet
package com.example.servlet;import com.example.listener.OnlineUserListener;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;/*** 用户退出Servlet*/
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {HttpSession session = request.getSession(false);if (session != null) {String username = (String) session.getAttribute("currentUser");// 通知监听器用户退出OnlineUserListener listener = getOnlineUserListener();if (listener != null) {listener.userLoggedOut(session);}// 使Session失效session.invalidate();System.out.println("用户 " + username + " 已退出");}response.sendRedirect("login.jsp");}private OnlineUserListener getOnlineUserListener() {return (OnlineUserListener) getServletContext().getAttribute("onlineUserListener");}
}
5. 初始化监听器(可选)
如果需要更精细的控制,可以添加ServletContextListener来初始化:
package com.example.listener;import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;/*** 应用初始化监听器*/
@WebListener
public class AppInitListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) {ServletContext context = sce.getServletContext();// 创建并设置在线用户监听器OnlineUserListener onlineUserListener = new OnlineUserListener();context.setAttribute("onlineUserListener", onlineUserListener);System.out.println("在线用户统计系统初始化完成");}@Overridepublic void contextDestroyed(ServletContextEvent sce) {System.out.println("应用关闭,清理在线用户统计资源");}
}
方案二:基于HttpSessionBindingListener的实现
用户实体类(实现绑定监听)
package com.example.model;import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import java.util.concurrent.atomic.AtomicInteger;/*** 用户实体类,实现Session绑定监听*/
public class OnlineUser implements HttpSessionBindingListener {private String username;private String ipAddress;private long loginTime;public OnlineUser(String username, String ipAddress) {this.username = username;this.ipAddress = ipAddress;this.loginTime = System.currentTimeMillis();}/*** 当用户对象被绑定到Session时触发(登录)*/@Overridepublic void valueBound(HttpSessionBindingEvent event) {ServletContext context = event.getSession().getServletContext();AtomicInteger onlineCount = getOnlineCount(context);int count = onlineCount.incrementAndGet();context.setAttribute("loggedInUserCount", onlineCount);System.out.println("用户绑定到Session: " + username + ", 当前登录人数: " + count);}/*** 当用户对象从Session解除绑定时触发(退出或超时)*/@Overridepublic void valueUnbound(HttpSessionBindingEvent event) {ServletContext context = event.getSession().getServletContext();AtomicInteger onlineCount = getOnlineCount(context);int count = onlineCount.decrementAndGet();context.setAttribute("loggedInUserCount", onlineCount);long onlineTime = (System.currentTimeMillis() - loginTime) / 1000;System.out.println("用户从Session解除绑定: " + username + ", 在线时长: " + onlineTime + "秒" +", 剩余登录人数: " + count);}private AtomicInteger getOnlineCount(ServletContext context) {AtomicInteger count = (AtomicInteger) context.getAttribute("loggedInUserCount");if (count == null) {count = new AtomicInteger(0);context.setAttribute("loggedInUserCount", count);}return count;}// getter方法public String getUsername() { return username; }public String getIpAddress() { return ipAddress; }public long getLoginTime() { return loginTime; }
}
使用方式
// 在登录Servlet中
HttpSession session = request.getSession();
OnlineUser onlineUser = new OnlineUser(username, getClientIP(request));
session.setAttribute("currentUser", onlineUser); // 自动触发valueBound方法// 在退出时,只需要移除属性或使session失效
session.removeAttribute("currentUser"); // 自动触发valueUnbound方法
// 或者
session.invalidate();
JSP页面显示在线人数
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.concurrent.atomic.AtomicInteger" %>
<html>
<head><title>首页 - 在线人数统计</title><style>.online-stats {background: #f5f5f5;padding: 10px;margin: 10px 0;border-radius: 5px;}</style>
</head>
<body><h1>欢迎来到网站</h1><div class="online-stats"><h3>在线统计信息</h3><%// 从应用上下文获取统计信息AtomicInteger sessionCount = (AtomicInteger) application.getAttribute("onlineSessionCount");AtomicInteger loggedInCount = (AtomicInteger) application.getAttribute("loggedInUserCount");int totalSessions = (sessionCount != null) ? sessionCount.get() : 0;int loggedInUsers = (loggedInCount != null) ? loggedInCount.get() : 0;int anonymousUsers = totalSessions - loggedInUsers;%><p>总在线会话数: <strong><%= totalSessions %></strong></p><p>已登录用户数: <strong><%= loggedInUsers %></strong></p><p>匿名访客数: <strong><%= anonymousUsers %></strong></p><%if (session.getAttribute("currentUser") != null) {%><p>当前用户: <strong><%= session.getAttribute("currentUser") %></strong></p><a href="logout">退出登录</a><%}%></div>
</body>
</html>
web.xml配置(如果使用注解可忽略)
<?xml version="1.0" encoding="UTF-8"?>
<web-app><!-- 注册监听器 --><listener><listener-class>com.example.listener.OnlineUserListener</listener-class></listener><listener><listener-class>com.example.listener.AppInitListener</listener-class></listener><!-- Session配置 --><session-config><session-timeout>30</session-timeout> <!-- 30分钟超时 --></session-config>
</web-app>
关键特性说明
1. 线程安全
- 使用
AtomicInteger
保证计数操作的原子性 - 使用
ConcurrentHashMap
存储在线用户信息,保证并发安全
2. 统计维度
- 总在线会话数:所有活跃的Session数量
- 已登录用户数:成功登录的用户数量
- 匿名访客数:未登录的访问者数量
3. 自动清理
- Session超时自动触发销毁监听,确保统计准确
- 服务器重启时自动重新初始化
4. 扩展性
- 可以轻松添加更多统计信息(如用户IP、登录时间、最后访问时间等)
- 支持集群环境下的扩展(需要配合分布式Session方案)
这种实现方式能够准确统计在线用户数,并且能够处理各种正常和异常退出情况。