news 2026/10/19 6:12:49

JavaWeb购物车设计:Servlet+Session状态管理实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb购物车设计:Servlet+Session状态管理实战指南

简介:这是一份面向Java Web初学者的Servlet实战项目资源,聚焦购物车核心功能实现,适用于高校Web开发课程实验、自学练手及小型电商模块原型验证。资源基于Java Web基础技术栈,涵盖HTML页面展示、Servlet逻辑处理、Tomcat部署及简单前后端交互,帮助学习者掌握会话管理、请求转发与数据封装等关键技能。压缩包共25个文件,包含3个核心Java源码(如CartServlet、ProductServlet)、3个编译后class文件、2个XML配置文件(web.xml等)、1个HTML页面(shop.html)及8张界面截图,辅以Eclipse项目元数据文件(.project、.classpath等),总大小491KB,结构完整可直接导入IDE运行。目前已有638人学习下载,提供从商品展示→添加购物车→表格化订单呈现的全流程代码实现,含清晰目录组织与基础样式支持,是理解Servlet生命周期与Web应用分层设计的典型轻量级案例。

1. 为什么一个“简单”的购物车,反而最容易在毕设答辩时被问住?

你手里的shop010.zip解压出来只有src/com/...和WebContent/shop.html几个文件,连数据库都没用,纯靠HttpSession存商品列表——可偏偏就是这种“没技术难度”的 JavaWeb 小项目,答辩老师最爱揪着不放:“购物车里加了两件同款商品,是合并数量还是新增两条记录?”“用户关掉浏览器再打开,购物车还剩吗?”“如果两个标签页同时操作同一个购物车,会丢数据吗?”
这不是考你能不能写doGet(),而是考你有没有把「状态管理」这个黑匣子真正拆开看过。这套方案专为学生课设/毕设打磨:它不堆砌 Spring Boot 或 Redis,就用最原始的 Servlet + JSP(或纯 HTML)+ Tomcat 组合,但每一步都预留了可扩展锚点、每处 Session 操作都标注了线程安全边界、每个跳转逻辑都配了 HTTP 状态码级的流转图。适合需要交一份「看着朴素、实则经得起推敲」的 JavaWeb 基础项目的人——尤其当你答辩前夜还在改web.xml的<servlet-mapping>时,你会回来翻这一章。


2. 总体架构设计:三层结构与四个核心 Servlet 的职责切分

这个购物车系统表面看只是几个 HTML 页面跳来跳去,但背后必须有清晰的职责隔离。我把它拆成展示层 → 控制层 → 数据层三层,其中控制层由 4 个 Servlet 承担明确分工,避免把所有逻辑塞进一个CartServlet里——那是新手翻车第一现场。

2.1 展示层:静态资源与动态入口的边界划定

WebContent/shop.html是唯一对外暴露的静态入口,它不写任何 Java 代码,只做三件事:

  • 渲染商品列表(硬编码或从 Servlet 传参生成)
  • 为每个商品生成形如<a href="AddToCartServlet?pid=1001&name=笔记本&price=29.9">添加到购物车</a>的链接
  • 不直接访问WEB-INF下任何资源(这是安全底线)

提示:shop.html必须放在WebContent根目录,不能放WEB-INF下。后者是受保护目录,浏览器直连会 404——很多同学调试时发现点链接没反应,八成是路径放错了。

关键不是页面多炫,而是它和后端的契约要干净。比如链接里带pid(商品 ID)、name、price三个参数,意味着后续所有 Servlet 都默认信任这些值已校验过(实际开发中这里该走表单 POST + 后端校验,但课设阶段可简化为 GET 参数传递,下文会补上防御措施)。

2.2 控制层:四个 Servlet 的精准定位与协作链路

整个流程由 4 个 Servlet 串联完成,每个只做一件事,且命名直白:

Servlet 名称触发时机核心职责跳转目标
AddToCartServlet点击“添加到购物车”链接解析 URL 参数 → 构建Product对象 → 添加到 Session 中的购物车 ListShowCartServlet
ShowCartServletAddToCartServlet重定向后自动触发从 Session 读取购物车 List → 封装为request.setAttribute("cartList", cartList)→ 转发到cart.jspcart.jsp(视图)
UpdateCartServletcart.jsp中修改数量后提交表单接收pid和newQuantity→ 更新 Session 中对应商品数量 → 重定向回ShowCartServletShowCartServlet(强制重定向防重复提交)
ClearCartServletcart.jsp中点击“清空购物车”从 Session 中移除购物车属性 → 重定向到shop.htmlshop.html

注意:AddToCartServlet和UpdateCartServlet都用重定向(sendRedirect)跳转,而ShowCartServlet用请求转发(RequestDispatcher.forward)。这是关键设计——重定向能刷新浏览器地址栏,避免用户刷新时重复提交;转发则保持 URL 不变,让cart.jsp能自然显示数据。

2.3 数据层:Session 管理购物车的实现细节与生命周期控制

购物车数据存在HttpSession中,不是全局变量,也不是静态集合。具体做法:

// 在 AddToCartServlet 的 doGet 方法中 HttpSession session = request.getSession(true); // true 表示不存在则创建 List<Product> cartList = (List<Product>) session.getAttribute("cartList"); if (cartList == null) { cartList = new ArrayList<>(); session.setAttribute("cartList", cartList); } // ... 添加商品逻辑

这里有两个易错点必须强调:

  • getSession(true)是安全的,但getSession(false)可能返回 null,需判空;
  • session.setAttribute("cartList", cartList)的 key 必须全小写且无空格,避免不同 Servlet 间因 key 不一致导致读不到数据。

Session 生命周期由 Tomcat 控制,默认 30 分钟无操作即失效。若需延长,在web.xml中配置:

<session-config> <session-timeout>60</session-timeout> <!-- 单位:分钟 --> </session-config>

但要注意:课设演示时若等 60 分钟太长,可在 Tomcatconf/web.xml中全局修改,或干脆在AddToCartServlet开头加一行session.setMaxInactiveInterval(60 * 10);(10 分钟),方便快速验证。

2.4 web.xml 配置:URL 映射与初始化参数的硬性规范

所有 Servlet 必须在WEB-INF/web.xml中显式声明,不能依赖注解(Tomcat 7+ 支持@WebServlet,但课设环境常为 Tomcat 6/7,且答辩老师可能检查配置文件完整性):

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- AddToCartServlet 配置 --> <servlet> <servlet-name>AddToCartServlet</servlet-name> <servlet-class>com.servlet.AddToCartServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>AddToCartServlet</servlet-name> <url-pattern>/AddToCartServlet</url-pattern> </servlet-mapping> <!-- 其他 Servlet 类似配置,此处省略 --> </web-app>

重点检查:

  • <servlet-class>的包路径必须与.java文件实际路径完全一致(com.servlet.AddToCartServlet对应src/com/servlet/AddToCartServlet.java);
  • <url-pattern>的斜杠开头不能漏,/AddToCartServlet≠AddToCartServlet;
  • 若用 Eclipse 导出 WAR 包,确保web.xml在WEB-INF/下,而非src/下。

3. 核心模块实现:从商品添加到购物车渲染的完整代码链

现在把设计落地为可运行代码。以下代码均来自shop010.zip实际结构,已去除冗余注释,保留关键逻辑分支和防御性处理。所有类均放在src/com/servlet/下,符合标准 JavaWeb 包规范。

3.1 Product 实体类:轻量但不可简化的数据载体

购物车本质是Product对象的集合,因此必须先定义清晰的实体。课设中无需 ORM,但字段必须覆盖业务需求:

// src/com/entity/Product.java package com.entity; public class Product { private String pid; // 商品ID,字符串类型便于接收URL参数 private String name; // 商品名称 private double price; // 单价,用double而非float(精度要求不高时可接受) private int quantity; // 购买数量,默认1 // 构造方法:必须提供全参构造,便于Servlet中new对象 public Product(String pid, String name, double price) { this.pid = pid; this.name = name; this.price = price; this.quantity = 1; // 新增商品默认数量为1 } // getter/setter 方法(Eclipse可自动生成,此处省略) public String getPid() { return pid; } public void setPid(String pid) { this.pid = pid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } // 重写toString便于调试(非必须但强烈建议) @Override public String toString() { return "Product{" + "pid='" + pid + '\'' + ", name='" + name + '\'' + ", price=" + price + ", quantity=" + quantity + '}'; } }

为什么不用int price?因为商品价格常含小数(如 29.9),用int会丢失精度;为什么quantity是int?因为数量必为整数,且int运算比double更安全(避免 1.0 + 1.0 = 2.0000000000000004 这类玄学问题)。

3.2 AddToCartServlet:URL 参数解析与购物车追加逻辑

这是第一个被触发的 Servlet,负责将用户点击行为转化为 Session 中的数据变更:

// src/com/servlet/AddToCartServlet.java package com.servlet; import com.entity.Product; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class AddToCartServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取URL参数(GET请求) String pid = request.getParameter("pid"); String name = request.getParameter("name"); String priceStr = request.getParameter("price"); // 2. 基础参数校验(课设级防御) if (pid == null || pid.trim().isEmpty() || name == null || name.trim().isEmpty() || priceStr == null || priceStr.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少必要商品参数"); return; } double price; try { price = Double.parseDouble(priceStr); } catch (NumberFormatException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "价格格式错误"); return; } // 3. 构建Product对象 Product product = new Product(pid, name, price); // 4. 获取或创建购物车List HttpSession session = request.getSession(); @SuppressWarnings("unchecked") List<Product> cartList = (List<Product>) session.getAttribute("cartList"); if (cartList == null) { cartList = new ArrayList<>(); session.setAttribute("cartList", cartList); } // 5. 判断是否已存在同pid商品(关键业务逻辑!) boolean found = false; for (Product p : cartList) { if (p.getPid().equals(pid)) { p.setQuantity(p.getQuantity() + 1); // 合并数量 found = true; break; } } if (!found) { cartList.add(product); // 新增商品 } // 6. 重定向到ShowCartServlet,避免刷新重复添加 response.sendRedirect("ShowCartServlet"); } }

这段代码的核心价值不在“能跑”,而在业务逻辑的严谨性:

  • 第 2 步参数校验防止空指针;
  • 第 5 步遍历判断pid是否已存在,决定是quantity++还是add()—— 这直接回答答辩时“同款商品怎么处理”的问题;
  • response.sendRedirect("ShowCartServlet")强制跳转,URL 变为/YourApp/ShowCartServlet,用户刷新不会重复执行添加逻辑。

3.3 ShowCartServlet:从 Session 读取数据并准备视图模型

它不修改数据,只做“搬运工”:

// src/com/servlet/ShowCartServlet.java package com.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.List; public class ShowCartServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从Session获取购物车List HttpSession session = request.getSession(false); // false表示不创建新session if (session == null) { // Session已失效,重定向回首页 response.sendRedirect("shop.html"); return; } @SuppressWarnings("unchecked") List<Product> cartList = (List<Product>) session.getAttribute("cartList"); if (cartList == null) { cartList = new ArrayList<>(); // 空购物车也传空List,避免JSP报错 } // 2. 计算总金额(课设加分项) double total = 0.0; for (Product p : cartList) { total += p.getPrice() * p.getQuantity(); } request.setAttribute("total", total); // 3. 将cartList放入request作用域,供JSP使用 request.setAttribute("cartList", cartList); // 4. 请求转发到cart.jsp(注意:不是sendRedirect!) request.getRequestDispatcher("cart.jsp").forward(request, response); } }

关键点:

  • getSession(false)防止无意义创建 Session;
  • request.setAttribute("cartList", cartList)是 JSP 能读到数据的前提;
  • total计算虽简单,但体现“数据聚合”能力,答辩时可展开说“这里可以扩展为折扣计算、运费规则”。

3.4 cart.jsp:用 JSTL 标签库实现安全的数据渲染

WebContent/cart.jsp是最终视图,必须用标准 JSP + JSTL,禁用脚本片段(<% %>)——这是现代 JavaWeb 的基本素养:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>我的购物车</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } th { background-color: #f2f2f2; } </style> </head> <body> <h2>购物车清单</h2> <!-- 判断购物车是否为空 --> <c:choose> <c:when test="${empty cartList || fn:length(cartList) == 0}"> <p>购物车为空,请<a href="shop.html">返回商品页</a>继续选购。</p> </c:when> <c:otherwise> <table> <thead> <tr> <th>商品ID</th> <th>商品名称</th> <th>单价(元)</th> <th>数量</th> <th>小计(元)</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach var="product" items="${cartList}"> <tr> <td>${product.pid}</td> <td>${product.name}</td> <td>${product.price}</td> <td> <form action="UpdateCartServlet" method="post" style="display:inline;"> <input type="hidden" name="pid" value="${product.pid}"> <input type="number" name="newQuantity" value="${product.quantity}" min="1" style="width:60px;"> <input type="submit" value="更新"> </form> </td> <td>${product.price * product.quantity}</td> <td> <a href="DeleteCartItemServlet?pid=${product.pid}">删除</a> </td> </tr> </c:forEach> </tbody> </table> <p><strong>总计:${total} 元</strong></p> <p><a href="ClearCartServlet">清空购物车</a> | <a href="shop.html">继续购物</a></p> </c:otherwise> </c:choose> </body> </html>

注意:

  • <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>是 JSTL 核心标签库,必须引入;
  • ${empty cartList}比cartList == null更安全,JSTL 自动处理 null;
  • 表单用method="post"提交数量更新,符合 HTTP 语义(GET 用于查询,POST 用于修改);
  • 删除链接用href="DeleteCartItemServlet?pid=...",需额外实现DeleteCartItemServlet(见下节)。

3.5 DeleteCartItemServlet:安全删除单个商品的原子操作

这是对cart.jsp中“删除”链接的响应:

// src/com/servlet/DeleteCartItemServlet.java package com.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class DeleteCartItemServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pid = request.getParameter("pid"); if (pid == null || pid.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少商品ID"); return; } HttpSession session = request.getSession(false); if (session == null) { response.sendRedirect("shop.html"); return; } @SuppressWarnings("unchecked") List<Product> cartList = (List<Product>) session.getAttribute("cartList"); if (cartList != null) { // 使用迭代器安全删除(避免ConcurrentModificationException) for (int i = 0; i < cartList.size(); i++) { if (cartList.get(i).getPid().equals(pid)) { cartList.remove(i); break; // 找到即删,不继续遍历 } } } response.sendRedirect("ShowCartServlet"); // 重定向刷新页面 } }

为什么用for循环 +remove(i)而不用for(Product p : cartList)?因为增强 for 循环底层用迭代器,直接cartList.remove(p)会抛ConcurrentModificationException。这是学生高频踩坑点。


4. 避坑指南:这5个点不提前处理,答辩当天必卡壳

哪怕代码全对,部署和交互细节的疏忽也会让答辩变成灾难。以下是我在带某高校课程设计时,学生反馈最高频、最影响演示效果的 5 个真实问题,按「现象 → 原因 → 解决」给出可立即执行的方案。

4.1 现象:点击“添加到购物车”没反应,浏览器地址栏 URL 变成http://localhost:8080/YourApp/AddToCartServlet?...但页面空白

原因:AddToCartServlet的doGet方法中未正确调用response.sendRedirect(...),或重定向路径写错(如写成"ShowCartServlet"但实际映射是"/ShowCartServlet")。更隐蔽的原因是web.xml中<servlet-mapping>的<url-pattern>与链接中的路径不匹配。
解决:

  • 检查AddToCartServlet末尾是否为response.sendRedirect("ShowCartServlet")(相对路径)或response.sendRedirect(request.getContextPath() + "/ShowCartServlet")(绝对路径,推荐);
  • 在web.xml中确认<url-pattern>是/AddToCartServlet,且shop.html中链接的href也是/AddToCartServlet(注意开头斜杠);
  • 启动 Tomcat 后,手动在浏览器输入http://localhost:8080/YourApp/AddToCartServlet?pid=1&name=test&price=1.0,看是否返回 302 重定向。若返回 404,说明web.xml配置或类路径有误。

4.2 现象:购物车页面显示java.lang.NullPointerException,堆栈指向cart.jsp的<c:forEach>标签

原因:ShowCartServlet中session.getAttribute("cartList")返回 null,但 JSP 未做空判断,直接遍历null。常见于用户首次访问cart.jsp(未经过AddToCartServlet)或 Session 失效后未重定向。
解决:

  • 在ShowCartServlet中,当cartList为 null 时,request.setAttribute("cartList", new ArrayList<>())传空 List,而非 null;
  • 在cart.jsp中,必须用<c:choose><c:when test="${empty cartList}">...</c:when></c:choose>包裹表格,严禁直接<c:forEach>;
  • 验证方法:直接浏览器访问http://localhost:8080/YourApp/cart.jsp,应显示“购物车为空”,而非报错。

4.3 现象:修改商品数量后点击“更新”,购物车数量没变,甚至清空了

原因:UpdateCartServlet用request.getParameter("pid")获取 ID,但cart.jsp表单中name="pid"的 input 是隐藏域,其value写成了${product.pid},而 JSTL 表达式在服务端已解析,前端查看源码应看到<input type="hidden" name="pid" value="1001">。若看到的是${product.pid}字面量,说明 JSTL 标签库未正确加载。
解决:

  • 确认WebContent/WEB-INF/lib/下有jstl-1.2.jar和standard-1.1.2.jar(Tomcat 7 及以下)或jakarta.servlet.jsp.jstl-api-2.0.0.jar(Tomcat 10+);
  • cart.jsp顶部必须有<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,且 URI 与 jar 包版本匹配;
  • 在 Eclipse 中右键项目 → Properties → Targeted Runtimes → 勾选已安装的 Tomcat 版本,确保编译环境一致。

4.4 现象:两个浏览器标签页同时操作购物车,A 标签页加了商品,B 标签页刷新后看不到,或数量错乱

原因:HttpSession是用户级的,同一浏览器所有标签页共享一个 Session,但ArrayList不是线程安全的。当两个请求并发修改cartList(如 A 添加、B 删除),可能出现ConcurrentModificationException或数据丢失。
解决:

  • 在AddToCartServlet和DeleteCartItemServlet中,对cartList的修改操作加同步块:
    synchronized (cartList) { // 执行 add() 或 remove() 操作 }
  • 更彻底的方案:将cartList替换为Collections.synchronizedList(new ArrayList<>()),并在每次读写时用synchronized (cartList)包裹;
  • 课设演示时,可主动告知老师:“当前实现基于单用户场景,若需高并发支持,可引入 Redis 存储购物车,Session 仅存 Token”,展现扩展思维。

4.5 现象:Tomcat 启动报错java.lang.ClassNotFoundException: com.servlet.AddToCartServlet

原因:.class文件未编译到build/classes/目录,或 Eclipse 未启用自动构建(Project → Build Automatically 未勾选),或web.xml中<servlet-class>的包名与实际.java文件物理路径不一致(如.java在src/com/servlet/,但写成了com.AddToCartServlet)。
解决:

  • 在 Eclipse 中,右键项目 → Refresh,确保build/classes/com/servlet/AddToCartServlet.class存在;
  • 检查src/com/servlet/AddToCartServlet.java文件首行是否为package com.servlet;;
  • 若用命令行编译,进入src目录执行:
    javac -d ../build/classes -cp ".:../WebContent/WEB-INF/lib/servlet-api.jar" com/servlet/AddToCartServlet.java
    (servlet-api.jar路径需根据 Tomcat 安装位置调整)

5. 验收与答辩:用这3个动作让老师觉得你真做过

验收不是“跑起来就行”,而是证明你理解了每个环节的因果关系。答辩时老师不会逐行看代码,但会通过三个典型问题检验你的掌握深度。我带过的某高校课程设计中,90% 的学生栽在这三个动作没做全——下面直接给你可复现的 checklist。

5.1 动作一:亲手画一张“HTTP 请求流转图”,标出每个环节的状态变化

不要用 PPT 画精美流程图,就拿一张 A4 纸,手绘从shop.html点击开始,到最终cart.jsp渲染结束的完整链路。重点标出三类信息:

环节必须标注的内容为什么重要
shop.html链接href="/AddToCartServlet?pid=1001&name=鼠标&price=45.0"证明你理解 GET 请求参数传递机制,且 URL 路径与web.xml映射一致
AddToCartServletresponse.sendRedirect("/ShowCartServlet")+ 状态码 302说明你懂重定向防重复提交,且知道浏览器地址栏会变
ShowCartServletrequest.setAttribute("cartList", cartList)+forward区分forward(服务器内部跳转,URL 不变)和redirect(客户端跳转,URL 变)

提示:答辩时把这张纸放在手边,老师问“点击添加后发生了什么”,你就摊开图,手指着箭头讲:“这里浏览器发 GET 请求 → Tomcat 查web.xml找到AddToCartServlet→ Servlet 处理完重定向 → 浏览器收到 302 后自动发新请求到/ShowCartServlet→ 后者从 Session 取数据,forward给cart.jsp”。全程不背代码,全是 HTTP 协议级描述,老师立刻觉得你懂本质。

5.2 动作二:准备一份“参数校验对比表”,展示你对健壮性的思考

课设常被质疑“没考虑异常”,其实只需一张小表格就能破局。在答辩 PPT 最后一页,放这个表:

校验点你做了什么为什么这么做如果不做会怎样
URL 参数非空`if (pid == nullpid.trim().isEmpty()) {...}`
价格格式合法Double.parseDouble(priceStr)包在 try-catch防止用户恶意传price=abc导致 500后台崩溃,购物车功能瘫痪
Session 是否有效getSession(false)+null判空避免 Session 失效后继续操作cart.jsp报 NPE,体验断裂
并发修改安全synchronized (cartList)保证多标签页操作不丢数据A 标签页加商品,B 标签页删商品,结果购物车只剩一条

这张表的价值在于:它把“我写了 if 判断”升维成“我预判了用户的 4 种错误操作”。老师一眼看出你不是照抄教程,而是带着问题意识在编码。

5.3 动作三:现场演示一个“故意制造的 Bug”,再修复给老师看

这是最震撼的答辩技巧。提前在AddToCartServlet中注释掉synchronized块,或把cart.jsp的<c:forEach>改成<% for(...) { %>脚本片段。然后当着老师面:

  1. 启动 Tomcat,打开两个标签页;
  2. 标签页 A 点击添加商品;
  3. 标签页 B 立即点击删除同一商品;
  4. 刷新两个页面,展示ConcurrentModificationException或页面空白;
  5. 打开 Eclipse,取消注释synchronized,保存,Tomcat 自动热部署;
  6. 重新操作,问题消失。

注意:演示前备份原始代码,确保 5 分钟内能还原。这个动作传递的信息是:“我知道哪里脆弱,而且我能定位、修复它”。比说一百遍“我考虑了线程安全”都有力。

从那以后我每次带学生做 JavaWeb 课设,都会强制他们在答辩前夜,用 Chrome 开两个隐身窗口,反复执行“添加→删除→刷新”组合,直到连续 10 次不报错才允许提交。因为真正的验收,不是代码跑通,而是你敢在老师面前,把系统逼到临界点,再优雅地拉回来。
希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 6:12:12

GISBox实测:倾斜摄影数据快速发布为3D Tiles服务的全流程指南

在GIS这个圈子里待久了&#xff0c;你会发现一个很有意思的现象&#xff1a;一个工具的好坏&#xff0c;从不取决于厂商宣传册上写了什么功能&#xff0c;而是取决于一线从业者愿意在私下交流里主动推荐给同事多少次。最近几个月&#xff0c;“GISBox”这个名字在技术群、项目组…

作者头像 李华
网站建设 2026/10/19 6:12:06

六自由度机械臂轨迹规划:多项式插值vs B样条实战解析

六自由度机械臂的轨迹规划&#xff0c;说简单也简单&#xff0c;说复杂那是真复杂。我刚入行那会儿&#xff0c;以为给机械臂写个路径点&#xff0c;让它从A点走到B点就算完事。结果第一次实测就翻车——末端执行器到了指定位置&#xff0c;但整个臂身抖得跟筛糠似的&#xff0…

作者头像 李华
网站建设 2026/10/19 6:11:30

手写数字识别实战:纯NumPy手撕CNN实现99%准确率

简介&#xff1a;本资源是一份基于Python与卷积神经网络&#xff08;CNN&#xff09;实现手写数字识别的完整实践项目&#xff0c;面向机器学习初学者、计算机视觉入门者及高校课程设计学生&#xff0c;聚焦图像分类核心任务&#xff0c;解决MNIST类手写体识别建模与部署问题。…

作者头像 李华
网站建设 2026/10/19 6:11:27

版本对比逆向分析:figugegl.1与figugegl.2改动全解析

拿到这个标题&#xff0c;第一反应是“figugegl.1和figugegl.2”这种命名方式太有迷惑性了&#xff0c;像随手起的临时文件名&#xff0c;也可能是内部代号。实际逆向分析时&#xff0c;这种命名恰恰是突破口之一&#xff1a;版本号的递增往往意味着功能迭代、漏洞修复&#xf…

作者头像 李华
网站建设 2026/10/19 6:10:53

eNSP校园网实战工程:OSPF多区域+VRRP热备+防火墙策略全链路解析

简介&#xff1a;本资源是一套面向计算机网络课程学习、毕业设计与期末大作业的高分实践方案&#xff0c;专为中等难度网络工程实训打造&#xff0c;适用于高校学生、助教及教学一线教师。方案基于华为eNSP平台完成大型校园网络拓扑设计&#xff0c;覆盖核心层、汇聚层、接入层…

作者头像 李华
网站建设 2026/10/19 6:09:56

外包CRUD一年后如何自救:技术成长与破局实操指南

很久没有写东西了&#xff0c;正好这个选题我想聊一聊。经常有人问我&#xff1a;在外包公司整天写 CRUD&#xff0c;到底多久会废掉&#xff1f;我的答案是&#xff1a;1 年左右。不是我吓你&#xff0c;因为这个时间点恰好是我自己进外包之后最明显的分水岭。刚开始觉得什么系…

作者头像 李华