在企業級應用中,常見兩大會話管控需求:
本文基於 HttpSessionAttributeListener 實現一個零侵入、集中式、線程安全的會話管控組件,核心邏輯集中在監聽器,Action 僅需讀取結果碼。
// 保存「用戶名 ↔ Session」映射
static class SessionAndUser {
String userName;
HttpSession session;
String sid; // session.getId()
}
全局靜態列表:
private static final List SESSIONS =
Collections.synchronizedList(new ArrayList<>());
package com.example.listener;
import javax.servlet.http.*;
import java.util.*;
import com.opensymphony.xwork2.ActionContext;
public class OnlineListener implements HttpSessionListener, HttpSessionAttributeListener {
// 輔助標記,處理 attributeReplaced → attributeRemoved 串聯
private static int pendingRemoveIndex = -1;
private static boolean replaceTriggered = false;
@Override
public void sessionCreated(HttpSessionEvent se) {
// 可選:記錄創建日誌
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 會話銷毀(超時/手動 invalidate)時,從列表移除
HttpSession session = se.getSession();
String sid = session.getId();
synchronized (SESSIONS) {
SESSIONS.removeIf(sau -> sau.sid.equals(sid));
}
}
// === 核心:屬性新增(登入時 setAttribute("username", ...))===
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
if (!"username".equals(event.getName())) return;
HttpSession session = event.getSession();
String username = (String) event.getValue();
ActionContext ctx = ActionContext.getContext();
synchronized (SESSIONS) {
// 1. 並發人數限制
if (SESSIONS.size() >= Constant.USER_LIMIT) {
ctx.put("timeoutMSG", "serverBusy");
return;
}
// 2. 同賬號檢查:已登入則標記
for (SessionAndUser sau : SESSIONS) {
if (sau.userName.equals(username)) {
ctx.put("timeoutMSG", "beenLoged");
return;
}
}
// 3. 新登入記錄
SessionAndUser sau = new SessionAndUser();
sau.userName = username;
sau.session = session;
sau.sid = session.getId();
SESSIONS.add(sau);
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
if (!"username".equals(event.getName())) return;
// 正常登出/超時觸發:直接移除
if (pendingRemoveIndex == -1) {
String username = (String) event.getValue();
synchronized (SESSIONS) {
SESSIONS.removeIf(sau -> sau.userName.equals(username));
}
} else {
// 由 attributeReplaced 觸發的踢人流程:延遲移除
if (replaceTriggered) {
synchronized (SESSIONS) {
SESSIONS.remove(pendingRemoveIndex);
}
pendingRemoveIndex = -1;
replaceTriggered = false;
}
}
}
// === 關鍵:屬性替換(同賬號在另一處登入,觸發替換)===
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
if (!"username".equals(event.getName())) return;
HttpSession newSession = event.getSession();
String newUsername = (String) newSession.getAttribute("username"); // 新值
String oldUsername = (String) event.getValue(); // 舊值
synchronized (SESSIONS) {
for (int i = SESSIONS.size() - 1; i >= 0; i--) {
SessionAndUser sau = SESSIONS.get(i);
// 情況 A:同用戶名在不同 Session 登入 → 踢掉舊 Session
if (sau.userName.equals(newUsername) && !sau.sid.equals(newSession.getId())) {
pendingRemoveIndex = i;
replaceTriggered = true;
sau.session.invalidate(); // 觸發 attributeRemoved → 移除列表
break;
}
// 情況 B:同 Session 內用戶名變更 → 更新記錄
if (sau.sid.equals(newSession.getId())) {
sau.userName = newUsername;
}
}
}
}
}
public class Constant {
public static final String USER_NAME = "username";
public static final int USER_LIMIT = 100; // 最大並發數,可從配置讀取
}
com.example.listener.OnlineListener
public class LoginAction extends ActionSupport {
// ... 登入校驗邏輯 ...
// 登入成功後設置 username 到 Session,觸發監聽器
session.setAttribute(Constant.USER_NAME, operator.getUsername());
// 讀取監聽器寫入的結果碼
ActionContext ctx = ActionContext.getContext();
if ("serverBusy".equals(ctx.get("timeoutMSG"))) {
ctx.put("timeoutMSG", "系統繁忙,請稍後再試");
return "login";
}
if ("beenLoged".equals(ctx.get("timeoutMSG"))) {
ctx.put("timeoutMSG", "該賬號已在別處登入");
return "login";
}
return "success";
}
<%@ taglib prefix="s" uri="/struts-tags"%>
[用戶 A 登入]
→ session.setAttribute("username", "A")
→ attributeAdded: 列表無 A → 添加 (A, Session1)
[用戶 A 在另一瀏覽器登入]
→ session.setAttribute("username", "A") (Session2)
→ attributeReplaced: 發現列表有 A 且 sid 不同
→ 標記 pendingRemoveIndex, replaceTriggered=true
→ Session1.invalidate()
→ attributeRemoved (由 Session1 銷毀觸發)
→ pendingRemoveIndex != -1 且 replaceTriggered
→ 移除列表索引 pendingRemoveIndex (即 Session1 記錄)
→ attributeAdded (Session2)
→ 列表無 A (已被移除) → 添加 (A, Session2)
結果:Session1 被踢下線,Session2 成為唯一合法會話
Collections.synchronizedList 保證列表操作原子性。synchronized (SESSIONS) 進一步保證複合操作原子性。pendingRemoveIndex + replaceTriggered 雙標記解決 attributeReplaced → session.invalidate() → attributeRemoved 異步串聯問題。user:sessions:{username} → Set,配合 Spring Session 實現分布式會話管控。利用 HttpSessionAttributeListener 監聽 username 屬性的增刪改,配合靜態同步列表維護「用戶↔Session」映射,可在零侵入業務代碼的前提下實現:
此方案適用於單機/Session 不共享的傳統 Web 應用;微服務/集群環境請遷移至 Spring Session + Redis 方案。