Servlet 点击计数器
用 ServletContext 与 Application 作用域做全站计数,以及并发与持久化。
三种作用域
| Scope | Lifetime | Shared by |
|---|---|---|
| request | 一次请求 | 当前请求的 Servlet 与 JSP |
| session | 一次会话 | 同一用户的多个请求 |
| application | 应用启动到停止 | 所有用户、所有请求 |
最简单的计数器
@WebServlet("/hits")
public class HitsServlet extends HttpServlet {
private static final String KEY = "hits";
@Override
public void init() {
getServletContext().setAttribute(KEY, new AtomicLong());
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
AtomicLong counter = (AtomicLong) getServletContext().getAttribute(KEY);
long n = counter.incrementAndGet(); // 原子自增,无需加锁
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().println("本站访问次数:" + n);
}
}本站访问次数:128AtomicLong is mandatory here. Writing int n = (int) ctx.getAttribute(KEY); ctx.setAttribute(KEY, n + 1); loses counts under concurrency because of the gap between read and write.
按会话计数
HttpSession s = req.getSession();
Integer mine = (Integer) s.getAttribute("myHits");
mine = (mine == null ? 1 : mine + 1);
s.setAttribute("myHits", mine); // 单用户串行,不需要原子类型Requests within one session are essentially serial from a browser, so a plain Integer suffices. Only cross-user sharing needs atomics or locks.
重启不丢:持久化
public class HitListener implements ServletContextListener {
private static final Path FILE = Path.of(System.getProperty("java.io.tmpdir"), "hits.txt");
@Override
public void contextInitialized(ServletContextEvent e) {
long init = 0;
try {
if (Files.exists(FILE)) {
init = Long.parseLong(Files.readString(FILE).trim());
}
} catch (Exception ignore) {
// 文件损坏就从头计数
}
e.getServletContext().setAttribute("hits", new AtomicLong(init));
}
@Override
public void contextDestroyed(ServletContextEvent e) {
AtomicLong c = (AtomicLong) e.getServletContext().getAttribute("hits");
try {
Files.writeString(FILE, String.valueOf(c.get()));
} catch (IOException ignore) {
// 关停阶段不再抛异常
}
}
}contextDestroyed is not guaranteed — a kill -9 skips it. Reliable counting should write asynchronously to a database on each increment, not only flush at shutdown.什么时候不该自己数
Page-view analytics belong in Nginx log analysis or a dedicated service: no app memory, survives restarts, and aggregates over time. A hand-rolled counter is fine for demos and the simplest cases only.
三种作用域
| 作用域 | 生命周期 | 共享范围 |
|---|---|---|
| 请求 | 一次请求 | 当前请求的 Servlet 与 JSP |
| 会话 | 一次会话 | 同一用户的多个请求 |
| 应用 | 应用启动到停止 | 所有用户、所有请求 |
最简单的计数器
@WebServlet("/hits")
public class HitsServlet extends HttpServlet {
private static final String KEY = "hits";
@Override
public void init() {
getServletContext().setAttribute(KEY, new AtomicLong());
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
AtomicLong counter = (AtomicLong) getServletContext().getAttribute(KEY);
long n = counter.incrementAndGet(); // 原子自增,无需加锁
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().println("本站访问次数:" + n);
}
}本站访问次数:128这里必须用 AtomicLong。写成 int n = (int) ctx.getAttribute(KEY); ctx.setAttribute(KEY, n + 1); 在高并发下会丢计数,因为读和写之间有窗口。
按会话计数
HttpSession s = req.getSession();
Integer mine = (Integer) s.getAttribute("myHits");
mine = (mine == null ? 1 : mine + 1);
s.setAttribute("myHits", mine); // 单用户串行,不需要原子类型同一个会话的请求在浏览器端基本是串行的,简单 Integer 就够。跨用户共享才需要原子类型或锁。
重启不丢:持久化
public class HitListener implements ServletContextListener {
private static final Path FILE = Path.of(System.getProperty("java.io.tmpdir"), "hits.txt");
@Override
public void contextInitialized(ServletContextEvent e) {
long init = 0;
try {
if (Files.exists(FILE)) {
init = Long.parseLong(Files.readString(FILE).trim());
}
} catch (Exception ignore) {
// 文件损坏就从头计数
}
e.getServletContext().setAttribute("hits", new AtomicLong(init));
}
@Override
public void contextDestroyed(ServletContextEvent e) {
AtomicLong c = (AtomicLong) e.getServletContext().getAttribute("hits");
try {
Files.writeString(FILE, String.valueOf(c.get()));
} catch (IOException ignore) {
// 关停阶段不再抛异常
}
}
}contextDestroyed 不保证一定被调用——进程被 kill -9 时直接消失。真正要可靠的计数应该在每次自增后异步写库,而不是只在关闭时刷盘。什么时候不该自己数
PV/UV 这类统计交给 Nginx 日志分析或专业统计服务更合适:它们不占应用内存、不受重启影响、还能按时间维度聚合。自己写计数器只适合演示或极简单的场景。