资讯动态

JSP内置对象实现页面访问计数器:session标记与application计数

发布时间:2026/10/23 3:42:40 来源:尧图企业网站定制
1. 需求拆解与核心设计思路先把这个需求咀嚼透。标题里藏着两个很关键的信息第一这是一个页面计数器要记录页面被访问的次数第二刷新页面时不计入新增只有真正意义上的“新一次访问”才计数。最后的提示点明了实现方向——用application和session两个内置对象配合。为什么这个需求值得认真做因为它是Web开发里非常典型的“会话级状态管理”问题。很多初学者拿到这种需求会想到用application对象直接存一个数字每次页面加载就加1但这样刷新页面也会计数显然不符合要求。还有的人会想到用Cookie存标记但Cookie可控性差、容量有限而且无法准确触达“会话”的语义。提示里用application和session说明这是一道关于JSP/Servlet内置对象协同工作的经典题目——application代表整个应用级别的共享数据session代表单个用户的一次会话。核心设计思路其实一句话就能讲明白用session记录“当前用户本次会话是否已经访问过该页面”用application存储全局总计数。首次访问时session里没有标记于是将application中的计数加1同时在session里打上“已访问”的标记后续刷新页面session里的标记还在就直接跳过计数逻辑只展示当前计数。这个方案背后的原理值得展开讲讲。session的生命周期是从用户第一次访问应用创建会话开始到会话超时或主动失效结束。同一浏览器在同一会话期间无论怎么刷新、怎么切换页面session对象都是同一个所以session里的标记可以稳定地判断“你是不是已经算过一次了”。而application对象是整个Web应用共享的所有用户的session都能访问到同一个application对象所以总数放在这里才能做到全局累加。我还想强调一个设计层面的细节这个方案之所以比单纯用Cookie好是因为session本质上是服务端状态不受客户端清理Cookie的影响默认情况下session依赖Cookie传递会话ID但即使Cookie临时失效只要会话ID还在URL重写等方式下能传递服务端的session数据就还在。这对计数器的准确性来说非常重要——如果计数器把“客户端清一下Cookie”也算成一次新访问那统计就失真了。另外从“单用户角度”到“多用户角度”的切换也很容易理解页面计数器的本质是多用户共享的累计值而“是否新增一次计数”的判断依据恰恰是单用户维度的会话状态。这种“局部状态决定全局数据是否更新”的思路在很多真实系统里都能看到影子比如每日签到、限时免登录、问卷防重复提交。2. 环境准备与项目结构动手写代码之前先把跑起来的环境理清楚。这个项目虽然小但涉及JSP运行环境选型和配置不做好后面会被各种报错折腾得怀疑人生。2.1 运行环境选择我建议用Tomcat作为Servlet容器版本选择上不用太追新Tomcat 9或10都可以但要注意版本和Java版本的匹配。如果你用的是Jakarta EE命名空间Tomcat 10JSP页面里的内置对象用法不变但如果是自己写Servlet代码导入的包路径会不同javax.servlet vs jakarta.servlet。本项目主要用JSP页面实现内置对象直接可用对命名空间的感知不强所以Tomcat 9配JDK 8或11是比较稳妥的组合。开发工具这块不需要重型IDE一个能写文本的编辑器加Tomcat就能跑。不过为了调试方便我还是建议用成熟的Java IDE比如Eclipse for Enterprise Java或者IntelliJ IDEA社区版因为JSP的语法高亮、错误提示和内置Tomcat集成调试能省不少事。如果你还没装Java环境记得先确认JDK已安装命令行里执行java -version能输出版本号即可。2.2 项目目录结构标准的Java Web项目目录结构是固定的不按这个来Tomcat就不认。以下是本项目会用到的最简结构webapp ├── counter.jsp ├── WEB-INF │ └── web.xml如果你的IDE帮你生成了完整的Maven或Gradle结构本质上也一样最终部署到Tomcat里的核心就是webapp目录。WEB-INF/web.xml是Web应用的描述文件在Servlet 3.0以上版本里纯JSP项目甚至可以不要web.xml但保留一个最简单的版本更稳妥尤其是有些容器对缺失配置会报奇怪的警告。2.3 web.xml配置直接给一个能用的最小配置?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namePageCounterDemo/display-name /web-app这个文件本身不承担业务逻辑它的存在只是为了让Web应用结构合法。如果你用的是Tomcat 9这套schema是通用的如果是旧版本把命名空间改成对应的版本即可。写项目时我给这套配置贴的注释只有一行保持最小化不为不存在的功能写多余的XML。3. 核心代码实现与逐行解析代码是博文的灵魂这一部分我把两个核心场景都写出来最简版页面计数器以及附带在线人数统计的增强版。每一行都讲清楚为什么这么写。3.1 基础版页面计数器% page contentTypetext/html;charsetUTF-8 languagejava % html head title页面访问计数器/title /head body % // 获取全局共享的计数器对象 Integer count (Integer) application.getAttribute(pageCount); if (count null) { count 0; } // 判断当前会话是否已经计数过 Object visited session.getAttribute(hasVisited); if (visited null) { // 第一次访问计数加一打上会话标记 count; application.setAttribute(pageCount, count); session.setAttribute(hasVisited, true); } % h2页面访问计数器/h2 p当前页面总访问量strong% count %/strong/p % if (visited ! null) { out.println(p本次会话中已访问过本页刷新不增加计数/p); } % /body /html这段代码的逻辑非常直白但有几个关键点必须理解到位。第一application.getAttribute(pageCount)返回的是Object类型需要强转为Integer。为什么因为setAttribute方法接收Object取出来自然就是Object如果不转直接做count编译都过不了。我见过不少初学者在这一步报“cannot find symbol”或者类型转换异常其实根源就是忘了强转。第二session.getAttribute(hasVisited)返回null表示当前会话还没访问过。有人会问这里为什么不用session.isNew()判断isNew()表示会话是否刚创建但第一次访问页面时会话可能已经在别的页面创建了所以isNew()不准确。我们自己的标记才是“页面对当前会话”维度的可靠依据。第三application.setAttribute(pageCount, count)和session.setAttribute(hasVisited, true)这两个操作是配对的。前者更新全局计数后者记录本会话状态。这两个动作必须同时发生只做前者会导致刷新继续计数只做后者会导致永远不计数。3.2 增强版统计在线人数与总访问量做产品级计数器通常还要关心“当前有多少用户在线”。这个需求正好可以用session的两个特性来实现session.isNew()判断会话是否刚建立session.getMaxInactiveInterval()配合监听器判断会话是否失效。不过在不引入监听器的前提下也可以在JSP里做“会话活跃用户”的模拟统计——用application里存一个在线数每次有新的session创建时加1session销毁时减1。要精确感知session销毁最规范的做法是注册HttpSessionListener但那是Servlet层面的技术和本题的内置对象方案稍有距离。为了让文章更贴合原题我在基础版之上给出另一种增强统计“本次会话中页面刷新次数”和“总访问量”两个维度。% page contentTypetext/html;charsetUTF-8 languagejava % %! // 声明一个全局同步方法用于安全地更新计数 private synchronized void increaseCount(javax.servlet.http.HttpSession session, javax.servlet.ServletContext application) { Integer count (Integer) application.getAttribute(totalCount); if (count null) { count 0; } count; application.setAttribute(totalCount, count); session.setAttribute(counted, yes); } % % // 当前会话刷新次数 Integer refreshCount (Integer) session.getAttribute(refreshCount); if (refreshCount null) { refreshCount 0; } refreshCount; session.setAttribute(refreshCount, refreshCount); // 是否已计入总访问量 Object counted session.getAttribute(counted); if (counted null) { increaseCount(session, application); } % html head title增强版页面计数器/title /head body h2增强版页面计数器/h2 p本次会话中刷新次数strong% refreshCount %/strong/p p全局总访问量刷新不增加strong% application.getAttribute(totalCount) %/strong/p /body /html这里用了一个JSP声明方法%! ... %里面标记了synchronized关键字。为什么要同步因为多个用户同时访问页面时count这一步不是原子操作在并发场景下可能丢失更新——两个线程同时读到相同的count各自加1后写回结果只增加了1。对于这个演示级计数器虽然并发量不大但写代码时养成“涉及共享可变数据就考虑同步”的习惯很有必要。synchronized修饰方法保证同一时刻只有一个线程能执行这段计数逻辑。不过要说明一点如果只在单个JSP页面内同步只保护了这一台服务器上的这一个页面。如果在集群环境下多个应用实例共享同一个计数那就要考虑分布式锁或外部存储了。这个话题在第六部分会提到。3.3 避免刷新计数的“绕坑”写法对比写这题的同学很容易写出几种错误版本我把它们列出来对照一下帮你看清为什么不对。错误写法一只用application计数没用session标记。结果刷新页面时count无条件执行计数疯涨。错误写法二用session.isNew()判断。问题在于第一次访问页面时session可能已经被创建了比如之前访问过其他JSPisNew()返回false导致首次访问不计数。错误写法三把标记存在request里。request是每次请求都新建的刷新后就没有了等于没存。正确写法用session存自定义标记用application存全局计数。标记的有效期正好是会话期和需求“刷新不增加计数”的语义完全吻合。对比之后能明显感受到内置对象不是随便挑一个就行的各自的生命周期决定了它们适合存什么数据。application适合存全局量session适合存会话量request适合存单次请求量。这个选择逻辑学一次以后很多场景都能套用。4. 完整实操过程与运行验证只贴代码不演示运行过程读者总会觉得隔了一层。我按照自己实际动手的流程把这个项目从创建到验证的完整过程记录下来你可以照着操练。4.1 创建项目我用Maven骨架创建了一个简单的Web项目groupId用的demo.pagecounterartifactId用的page-counter。创建好的目录里src/main/webapp就是webapp根目录。如果你不用Maven直接用IDE的Dynamic Web Project模板也可以最终都是把编译好的class文件和JSP/HTML资源放到指定目录。4.2 编写JSP文件把上一节的counter.jsp完整代码复制到src/main/webapp/counter.jsp。注意文件的编码格式JSP页面里的中文注释和输出建议统一用UTF-8避免乱码。如果用的是IDE通常默认就是UTF-8如果用记事本记得另存为UTF-8编码。4.3 配置Tomcat并部署在IDE里配置好Tomcat运行时然后直接Run on Server。如果是手动部署把项目打成War包后丢到Tomcat的webapps目录启动Tomcat后会自动解压部署。访问地址一般是http://localhost:8080/page-counter/counter.jsp端口和上下文路径以你的实际配置为准。启动过程中如果遇到端口占用可以用lsof -i:8080macOS/Linux或netstat -anoWindows检查是谁占用了8080端口换个端口或者结束占用进程即可。4.4 用浏览器验证计数行为部署成功后按下面步骤验证打开浏览器访问counter.jsp页面显示“当前页面总访问量1”。按F5刷新页面刷新多次页面总访问量始终保持1。关闭所有浏览器窗口重新打开浏览器再访问总访问量变为2。换一个浏览器或者用无痕模式访问总访问量再次加1。等待超过默认会话超时时间Tomcat默认30分钟再访问总访问量加1。验证第5步可能时间太长实操时可以临时在Tomcat的web.xml里修改session超时时间session-config session-timeout1/session-timeout /session-configsession-timeout的单位是分钟设为1表示1分钟不活动后会话失效。改完后重启Tomcat再验证效率会高很多。记得验证完把配置改回来。4.5 并发下的表现验证如果你还想验证并发场景下同步是否生效可以用简单的脚本工具模拟多个并发请求。比如用命令行工具像curl并发访问for i in $(seq 1 20); do curl -s http://localhost:8080/page-counter/counter.jsp /dev/null done wait这里只模拟了并发请求但没维护session Cookie所以每次curl都是新会话计数会增加20左右。如果想模拟同一会话的并发需要加上-b cookies.txt -c cookies.txt参数保持Cookiecurl -s -c cookies.txt -b cookies.txt http://localhost:8080/page-counter/counter.jsp /dev/null curl -s -c cookies.txt -b cookies.txt http://localhost:8080/page-counter/counter.jsp /dev/null curl -s -c cookies.txt -b cookies.txt http://localhost:8080/page-counter/counter.jsp /dev/null第二个和第三个请求因为带了同一个Cookie服务端识别为同一个session计数不会增加。这个实验非常直观能帮你理解session和请求之间“我认为你是谁”的关联机制。5. 常见问题与排查方法实操过程中每个人都会踩几个坑。我把高频问题整理成表每个问题都给了排查思路方便对号入座。问题现象可能原因排查与解决方法刷新页面后计数持续增加忘了判断session标记或标记存储位置错误检查是否在首次访问时调用了session.setAttribute刷新逻辑里是否读取该标记首次访问计数就不对比如直接变2session.isNew()误判或标记存到了request里改用自定义布尔/字符串标记确认用的是session而不是request部署后访问页面404项目未成功部署或URL路径错误检查Tomcat控制台是否报错确认上下文路径和文件名大小写JSP页面出现类型转换异常getAttribute返回Object未强转用(Integer)强转并先判断是否为null中文内容乱码JSP页面编码不一致或Tomcat没有用UTF-8解析在% page %指令中显式指定pageEncodingUTF-8确保源文件本身是UTF-8编码修改JSP后浏览器看到的还是旧内容Tomcat的JSP缓存重启Tomcat或清理Tomcat的work目录开发模式可关闭JSP预编译访问被浏览器缓存导致一次都不计数浏览器对JSP响应做了缓存在JSP响应里设置response.setHeader(Cache-Control, no-store)辅助调试排查时记住一个心法先确认“应用有没有收到请求”再看“请求里session是不是同一个”最后看“application里的值到底变了没有”。可以打日志或者用System.out.println输出关键变量的值比瞎猜快得多。我就见过一个同学代码逻辑完全正确但浏览器开了两个标签页测试以为是两次会话其实两个标签共享同一个session这其实属于正常现象——同一个浏览器无论开多少标签页只要进程内会话ID一致都算同一次会话。这个认知误区很常见但恰恰说明方案“有效”。再补充一个容易忽略的细节如果你在web.xml里配置了欢迎文件welcome-file为counter.jsp访问根路径http://localhost:8080/page-counter/时也会触发一次计数。别惊讶这不是Bug是“进入首页”这个动作本身就算一次页面访问。6. 方案扩展与横向对比写代码不能只满足于“能跑”。既然标题里点名了application和session那么理解这两个对象在更大场景里的位置对面试和实际项目都很有帮助。6.1 与“每次请求计数”的区别如果需求是“统计每个请求的数量”那就完全不需要session直接在页面或过滤器中无条件对application里的计数加1即可。而本题的核心恰恰在于“去重”——对同一会话去重。所以整个方案的技术支点就是“会话内去重”这比单纯累加多了一个维度的判断也更贴近真实统计中的“去重访问”概念。6.2 如果不用JSP内置对象有什么替代方案Servlet 全局变量用Servlet的init()里保存计数但Servlet是单例的多个请求并发访问同一个成员变量时同样要考虑同步。而且Servlet的成员变量生命周期和应用生命周期绑定本质上和application对象的数据存储位置类似。Filter Session把计数逻辑放在Filter里对多个页面统一生效。如果需求是“全站页面计数”这是更合理的位置而不是在每一个JSP里重复写。数据库存储把计数持久化到数据库应用重启后数据不丢失。对小项目来说可能“杀鸡用牛刀”但真实项目里这种统计通常要落库否则服务一重启统计归零。Redis / 外部缓存用在分布式环境不是本章重点但你需要知道有这个方向。6.3 生命周期对照表对象生命周期典型用途本题中的角色pageContext单次请求页面页面内数据共享不涉及request单次请求请求参数、请求级临时数据不涉及session会话生命周期默认30分钟用户登录态、购物车、会话标记存“是否已计数”的标记application应用生命周期从启动到关闭全局配置、全站计数、在线人数存总计数这张表值得贴在任何Web开发入门笔记里。三个对象最大的差别不在“能存什么”而在“数据活多久”。理解生命周期很多设计问题能迎刃而解。6.4 扩展方向把计数器变成“访问统计面板”既然已经能在页面里显示总访问量了再往产品方向做一步就可以变成一个简单的访问统计面板。比如记录每日访问量日期维度的Map记录来源页面request.getHeader(Referer)记录用户浏览器和操作系统解析User-Agent头统计当前在线人数用session监听器实现。这些扩展都不需要改变核心的“session标记去重”思路只是在存储结构上更复杂一些。如果将来做大数据量统计优先考虑用成熟的分析工具或日志平台不要在JSP内存里硬扛。7. 实操心得与几点提醒最后分享一些我个人在这个小项目上沉淀下来的经验。第一重视内置对象的生命周期比背十行代码更重要。我见过太多人把application、session、request的API背得滚瓜烂熟但一遇到“放哪不失效”的问题就懵。这三个对象每个都对应一种数据存活时间选错了就是逻辑漏洞。这个项目恰恰是最小的训练场把生命周期彻底搞懂后面学框架时理解上下文、作用域这些抽象概念会顺很多。第二并发同步要区分场景。synchronized方法在单机单实例下够用但别迷信它为“万能钥匙”。如果应用做了集群部署不同机器上的application对象不是同一个同步方法保护不了跨实例的共享数据。真实项目里这种全局计数应该放到可共享的状态管理中间件或数据库里去。这个“本地可行、分布式失效”的边界从学生时代就要有意识。第三调试时将“浏览器Cookie”纳入考量。session的默认实现依赖Cookie保存会话IDJSESSIONID如果你在浏览器里禁用了Cookie或者测试无痕模式时打开了严格隔离session可能无法维持。用curl -c -b的方式能更精确地控制会话也更容易复现报告里的诡异问题。第四写完代码后一定要测“刷新”和“重开会话”两种操作。这两条路径覆盖了需求的核心边界。打个比方这个任务有点像做一个门禁你要让同一个员工当天多次进出不重复刷卡计费但第二天重新进门要重新计费。实现方案的核心就是在“员工身上盖一个当天的章”。这个生活类比能帮助你向别人描述需求时更清晰也能帮你自查逻辑是否覆盖了所有场景。如果你正在做课程设计或面试准备建议把代码自己手敲一遍不要复制粘贴。手敲的过程中你会注意到类型强转、null判断、变量作用域这些容易漏的细节。等你把“刷新不增加计数”这个需求跑通再试着自己改成“每5分钟内的首次访问才计数”又能加深一层对时间维度的控制理解。这个小项目虽小确是Web开发里“状态管理”的一粒好种子值得多花点时间浇水。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑