首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

基於 HttpSessionBindingListener 的 JSP 在線用戶統計實現原理與代碼實戰

     編輯:關於JSP
需求背景:為什麼需要在線用戶統計?
  • 後台管理:監控系統實時負載、踢出違規用戶、強制下線維護。
  • 社交功能:顯示「誰在線」、發起即時聊天。
  • 許可控制:限制同賬號並發登錄數、座位數管理。

核心難點:Servlet API **不提供獲取所有 Session 列表的標準接口**。Session 由容器私有管理,無法直接遍歷。

解決方案演進

方案原理優點缺點適用版本
HttpSessionBindingListener (經典)實現接口的對象綁定/解綁 Session 時回調,維護 Application 域 `List`Servlet 2.2+ 標準 API;無需容器特定擴展;邏輯清晰需手動將監聽器對象放入每個 Session;分布式環境需共享存儲 (Redis/DB)所有版本
HttpSessionAttributeListener (Servlet 2.3+)全局監聽器,自動感知所有 Session 屬性變更,無需綁定特定對象配置在 web.xml,零侵入業務代碼;自動捕獲所有屬性變更需區分業務屬性 (如 "loginUser") 觸發統計邏輯Servlet 2.3+
HttpSessionListener (Servlet 2.3+)監聽 Session 創建/銷毀事件 (`sessionCreated`/`sessionDestroyed`)最直接監控 Session 生命周期;配置在 web.xml創建時用戶未必登錄;銷毀時 Session 已失效無法獲取屬性 (需配合 `sessionDidActivate`/`sessionWillPassivate` 或提前記錄)Servlet 2.3+
容器特定 MBean/JMX (Tomcat StandardManager)通過 JMX 訪問 `Manager` 組件獲取活躍 Session 集合無需修改應用代碼;獲取容器級真實數據強綁定容器實現 (Tomcat/Jetty/WebLogic 不同);需開啟 JMX 遠程端口;權限敏感運維監控
分布式 Session 方案 (Spring Session + Redis)Session 數據存 Redis,掃描 Keys (`SESSION:*`) 或維護在線用戶 Set天然支援集群;統計查詢高效 (Redis Set/ZSet);數據持久化引入 Redis 依賴;需序列化配置現代微服務/集群首選

經典方案深度實現:HttpSessionBindingListener (單機版)

1. 監聽器實現類


package com.example.listener;

import javax.servlet.http.*;
import javax.servlet.*;
import java.util.*;

public class SessionTrackingListener implements HttpSessionBindingListener {
    // 靜態常量:Application 域存儲鍵
    public static final String ACTIVE_SESSIONS_KEY = "activeSessions";

    @Override
    public void valueBound(HttpSessionBindingEvent event) {
        // 1. 獲取 Application 域 (ServletContext)
        HttpSession session = event.getSession();
        ServletContext application = session.getServletContext();

        // 2. 獲取/初始化活躍 Session 列表 (線程安全:Vector 或 Collections.synchronizedList)
        @SuppressWarnings("unchecked")
        Vector activeSessions = (Vector) application.getAttribute(ACTIVE_SESSIONS_KEY);
        if (activeSessions == null) {
            activeSessions = new Vector<>();
            application.setAttribute(ACTIVE_SESSIONS_KEY, activeSessions);
        }

        // 3. 校驗是否為有效登錄用戶 (假設登錄成功後存放 User 對象,鍵為 "loginUser")
        Object userObj = session.getAttribute("loginUser");
        if (userObj != null) {
            // 避免重複添加 (同一 Session 可能綁定多個實現了 BindingListener 的對象)
            if (!activeSessions.contains(session)) {
                activeSessions.add(session);
            }
        }
    }

    @Override
    public void valueUnbound(HttpSessionBindingEvent event) {
        // Session 失效/超時/手動 invalidate 時觸發
        HttpSession session = event.getSession();
        ServletContext application = session.getServletContext();

        @SuppressWarnings("unchecked")
        Vector activeSessions = (Vector) application.getAttribute(ACTIVE_SESSIONS_KEY);
        if (activeSessions != null) {
            // 移除 Session (注意:此時 session.getAttribute("loginUser") 可能已為 null)
            activeSessions.remove(session);
        }
    }
}

2. 登錄邏輯綁定監聽器 (關鍵步驟!)

**必須在用戶登錄成功、將用戶信息存入 Session 時,同時將 `SessionTrackingListener` 實例存入 Session**,觸發 `valueBound`。


// LoginServlet / LoginAction
public void doPost(HttpServletRequest request, HttpServletResponse response) {
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    
    User user = userService.login(username, password); // 業務驗證
    
    if (user != null) {
        HttpSession session = request.getSession(true); // 獲取或創建 Session
        session.setAttribute("loginUser", user);        // 1. 存入業務用戶對象
        
        // 2. **核心:綁定監聽器實例** (名稱任意,如 "sessionTracker")
        // 這將觸發 valueBound -> 加入在線列表
        session.setAttribute("sessionTracker", new SessionTrackingListener());
        
        // 3. 可選:設置 Session 超時時間 (秒)
        session.setMaxInactiveInterval(30 * 60); // 30 分鐘
        
        response.sendRedirect("index.jsp");
    } else {
        request.setAttribute("error", "用戶名或密碼錯誤");
        request.getRequestDispatcher("login.jsp").forward(request, response);
    }
}

3. 登出邏輯主動清理


// LogoutServlet
public void doGet(HttpServletRequest request, HttpServletResponse response) {
    HttpSession session = request.getSession(false); // 不創建新 Session
    if (session != null) {
        // 1. 從 Application 域移除 (觸發 valueUnbound 邏輯)
        // 直接操作 Application 域列表更可靠,不依賴 Session 狀態
        ServletContext application = session.getServletContext();
        @SuppressWarnings("unchecked")
        Vector activeSessions = (Vector) application.getAttribute(SessionTrackingListener.ACTIVE_SESSIONS_KEY);
        if (activeSessions != null) {
            activeSessions.remove(session);
        }
        
        // 2. 使 Session 失效 (觸發 HttpSessionListener.sessionDestroyed / 容器清理)
        session.invalidate();
    }
    response.sendRedirect("login.jsp");
}

4. 前端展示在線用戶列表 JSP


<%@ page import="java.util.*, javax.servlet.http.*, com.example.model.User" %>
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="https://java.sun.com/jsp/jstl/fmt" %>
<html>
<body>
<h2>當前在線用戶 (${applicationScope.activeSessions.size()} 人)</h2>
<table border="1" cellpadding="5">
    <tr><th>Session ID</th><th>用戶名</th><th>登錄時間</th><th>最後活動時間</th><th>IP 地址</th></tr>
    <c:forEach var="session" items="${applicationScope.activeSessions}">
        <c:set var="user" value="${session.getAttribute('loginUser')}" />
        <tr>
            <td>${session.id}</td>
            <td>${user != null ? user.username : '未登錄/異常'}</td>
            <td><fmt:formatDate value="${session.creationTime}" pattern="yyyy-MM-dd HH:mm:ss" /></td>
            <td><fmt:formatDate value="${session.lastAccessedTime}" pattern="yyyy-MM-dd HH:mm:ss" /></td>
            <td>${session.getAttribute('clientIP')}</td> <!-- 需登錄時記錄 request.getRemoteAddr() -->
        </tr>
    </c:forEach>
</table>
</body>
</html>

進階模式:心跳/定時刷新防超時 (適用長連接/寫作/視頻場景)

若用戶長時間閱讀/編輯無請求,Session 會超時被踢。解決方案:

  1. 隱藏 Iframe / AJAX 輪詢:頁面加載 JS 定時 (如每 5 分鐘) 請求一個輕量端點 (`/keepAlive`)。
  2. 端點實現:request.getSession(false) 獲取 Session (不創建),若存在則 `session.setAttribute("lastHeartbeat", System.currentTimeMillis())` 更新屬性 (觸發 `lastAccessedTime` 刷新)。
  3. 統計邏輯調整:展示頁判斷「在線」標準 = `lastAccessedTime` 在 `heartbeatInterval * 2` 內,而非單純 Session 是否存在。

分布式/集群環境遷移:Spring Session + Redis

單機 `Application` 域列表**不共享**,集群下每節點只見自己 Session。**必須遷移至共享存儲**:


// Spring Boot 配置
spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379

// 統計在線用戶 (Service 層)
@Autowired StringRedisTemplate redisTemplate;

public Set getOnlineUsernames() {
    // Spring Session 默認 Key 前綴: "spring:session:sessions:"
    // 掃描所有 Session Key (生產用 SCAN 非 KEYS)
    Set keys = redisTemplate.keys("spring:session:sessions:*");
    Set usernames = new HashSet<>();
    for (String key : keys) {
        // 獲取 Session 中的 loginUser 序列化數據 (需反序列化或用 HashOperations 讀取字段)
        // 簡化:假設存儲為 JSON 或提取 principalName
        Object principal = redisTemplate.opsForHash().get(key, "sessionAttr:loginUser");
        if (principal != null) {
            // 解析 username (視序列化格式而定)
            usernames.add(parseUsername(principal));
        }
    }
    return usernames;
}

// 更高效:登錄時維護 Redis Set "online:users" (ZSet 分值為最後心跳時間)
public void onLogin(String username, String sessionId) {
    redisTemplate.opsForZSet().add("online:users", username + ":" + sessionId, System.currentTimeMillis());
    redisTemplate.expire("online:users", 30, TimeUnit.MINUTES); // 配合心跳刷新
}
public void onHeartbeat(String username, String sessionId) {
    redisTemplate.opsForZSet().incrementScore("online:users", username + ":" + sessionId, 0); // 更新分值為當前時間
}
public void onLogout(String username, String sessionId) {
    redisTemplate.opsForZSet().remove("online:users", username + ":" + sessionId);
}
// 定時任務清理過期 (分值 < 當前時間 - 超時閾值)

總結與最佳實踐建議

  1. 單機/簡單應用:使用 `HttpSessionListener` (web.xml 配置) + `HttpSessionAttributeListener`** 監聽 `loginUser` 屬性變更,維護 `ServletContext` 域 `ConcurrentHashMap` (比 Vector 更高效線程安全)。**避免在 Session 存放監聽器實例** (侵入性、序列化問題)。
  2. 集群/微服務:強制使用 Spring Session + Redis。Session 數據自動同步,**在線統計直接查詢 Redis (Set/ZSet)**,性能極佳、數據一致、支持橫向擴展。
  3. 數據準確性:「在線」定義需業務明確:
    - 嚴格:Session 存在且 `loginUser` 非空。
    - 寬鬆:最後請求/心跳在 N 分鐘內 (需心跳機制)。
    - 實時通訊:WebSocket 連接狀態為準。
  4. 性能與內存:
    - 僅存儲 **用戶 ID、名稱、登錄時間、IP、SessionId** 等輕量信息至統計結構,**勿存完整 Session/大對象**。
    - 定時任務清理過期統計數據 (配合 Session 超時時間)。
  5. 安全:在線列表頁面**需權限控制** (僅管理員可見),防止用戶枚舉洩露隱私。

從 `HttpSessionBindingListener` 到 `Spring Session + Redis`,技術棧演進解決了**共享、擴展、性能**三大核心痛點。理解底層原理 (監聽器回調、Session 生命周期、容器管理機制) 是排查「用戶莫名掉線、統計數字不準、集群 Session 不同步」等疑難雜症的關鍵。

]]>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved