Servlet 容器默認 Session 超時機制(web.xml session-timeout)存在不準確、無法主動清理、集群不同步等問題。通過實現 HttpSessionListener + ServletContextListener,構建自定義掃描器,實現 Session 創建跟蹤、定時掃描失效、手動刷新等精確控制。
HttpSessionListener,在 sessionCreated 將 Session 放入線程安全容器,sessionDestroyed 移除。ServletContextListener,contextInitialized 啟動 Timer/ScheduledExecutorService,定期遍歷容器檢查 getLastAccessedTime()。Collections.synchronizedList + ReentrantLock 或 CopyOnWriteArrayList 保護容器;迭代刪除用 Iterator.remove() 避免 ConcurrentModificationException。System.currentTimeMillis() - session.getLastAccessedTime() > maxInactiveInterval * 1000 則調用 session.invalidate()。package com.example.listener;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.concurrent.*;
public class SessionScanner implements HttpSessionListener, ServletContextListener {
// 線程安全容器:CopyOnWriteArrayList 讀多寫少場景性能優於 synchronizedList
private final List sessions = new CopyOnWriteArrayList<>();
// 定時任務執行器
private ScheduledExecutorService scheduler;
@Override
public void sessionCreated(HttpSessionEvent se) {
HttpSession session = se.getSession();
sessions.add(session);
System.out.println("Session 創建: " + session.getId() + ", 當前總數: " + sessions.size());
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
sessions.remove(session);
System.out.println("Session 銷毀: " + session.getId() + ", 當前總數: " + sessions.size());
}
@Override
public void contextInitialized(ServletContextEvent sce) {
// 每 30 秒掃描一次
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "Session-Scanner");
t.setDaemon(true);
return t;
});
scheduler.scheduleAtFixedRate(this::scanExpiredSessions, 30, 30, TimeUnit.SECONDS);
System.out.println("SessionScanner 啟動,掃描間隔 30s");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (scheduler != null) scheduler.shutdown();
sessions.clear();
System.out.println("SessionScanner 關閉");
}
private void scanExpiredSessions() {
long now = System.currentTimeMillis();
int expiredCount = 0;
for (HttpSession session : sessions) {
try {
long lastAccess = session.getLastAccessedTime();
int maxInactive = session.getMaxInactiveInterval(); // 秒
if (maxInactive > 0 && (now - lastAccess) > maxInactive * 1000L) {
session.invalidate(); // 觸發 sessionDestroyed 移除
expiredCount++;
}
} catch (IllegalStateException e) {
// Session 已失效,從容器移除
sessions.remove(session);
}
}
if (expiredCount > 0) {
System.out.println("掃描失效 Session: " + expiredCount + " 個, 剩餘: " + sessions.size());
}
}
// 可選:暴露管理接口
public List getActiveSessions() {
return new ArrayList<>(sessions);
}
public void forceInvalidate(String sessionId) {
for (HttpSession session : sessions) {
if (session.getId().equals(sessionId)) {
session.invalidate();
break;
}
}
}
}
<listener>
<listener-class>com.example.listener.SessionScanner</listener-class>
</listener>
| 場景 | 方案 |
|---|---|
| 分布式集群 | 配合 Spring Session + Redis,集中存儲 Session,掃描器部署在單獨節點或利用 Redis Keys 過期通知 |
| 海量 Session | 分桶掃描、異步處理、利用 Redis TTL 自動過期(無需掃描) |
| 精確控制 | 暴露 JMX/Actuator Endpoint,支持手動失效、查看詳情、導出報表 |
| 安全合規 | 記錄 Session 創建/銷毀/失效審計日志(用戶、IP、時間、原因) |
sessions 調用 remove():導致 ConcurrentModificationException。必須用 Iterator.remove() 或 CopyOnWriteArrayList。sessionDestroyed 回調:容器可能延遲調用、崩潰時不調用。掃描器主動 invalidate() 更可靠。getMaxInactiveInterval() 返回 -1:表示永不過期,需跳過。setDaemon(true) 或用 ScheduledExecutorService。自定義 Session 掃描器將會話管理從「被動等待」轉為「主動掌控」,是構建高可用、可審計、可運維 Web 系統的關鍵基礎設施。