Servlet 点击计数器

用 ServletContext 与 Application 作用域做全站计数,以及并发与持久化。

Intermediate
Key Points全站共享的计数放 ServletContext,用 AtomicLong 保证原子性;重启不丢要落库或写文件。

三种作用域

ScopeLifetimeShared by
request一次请求当前请求的 Servlet 与 JSP
session一次会话同一用户的多个请求
application应用启动到停止所有用户、所有请求

最简单的计数器

Java
@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);
    }
}
Output
本站访问次数:128

AtomicLong 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.

按会话计数

Java
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.

重启不丢:持久化

Java
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.

Friends & Partners