简介这是一份面向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. 为什么一个“简单购物车”要死磕 Servlet 生命周期和会话管理很多同学交毕设/课设时把add-to-cart点一下跳转、页面刷新后显示商品列表就以为“功能完成了”。结果答辩被问“用户 A 加了商品用户 B 刷新页面怎么也看到 A 的商品”——当场卡壳。这不是代码写错了是根本没立住需求购物车不是全局共享的购物篮而是每个用户私有的、有生命周期的状态容器。本方案用纯 JavaWeb Servlet 实现不依赖 Spring、不碰框架黑匣子全程暴露HttpSession如何绑定用户、request.getParameter()怎么解析 URL 参数、response.sendRedirect()和RequestDispatcher.forward()的本质区别。它适合两类人一是需要交一份“看得清、讲得明、改得动”的课程设计的学生二是想补全 Web 底层链路、避免在真实项目里因 session 丢失或 request 域污染而翻车的初级开发者。整套代码跑在 Tomcat 9 上所有 HTML/CSS 静态资源与 Servlet 逻辑完全解耦连web.xml都保留不强制注解方便你对照教材逐行理解部署描述符的作用。2. 总体架构设计三层结构与四个核心 Servlet 的职责划分本系统采用经典 JavaWeb 三层结构表现层HTML 页面、控制层Servlet、数据层内存模拟。没有数据库所有商品信息和购物车数据均驻留在 JVM 内存中通过ServletContext全局初始化商品目录通过HttpSession绑定用户级购物车。这种设计不是偷懒而是教学场景下的合理取舍它让你一眼看清数据从哪来、到哪去、何时创建、何时销毁。整个流程由四个 Servlet 串联驱动每个只做一件事绝不越界ProductListServlet负责加载并渲染商品列表页shop.htmlAddToCartServlet接收添加请求解析商品 ID 和数量更新当前用户 Session 中的购物车ShowCartServlet读取 Session 中的购物车组装成 List 传给 JSP 或转发至展示页ClearCartServlet清空当前用户购物车可选扩展点。提示所有 Servlet 均继承HttpServlet重写doGet()即可满足本需求。doPost()暂未使用但你在shop.html中若将链接改为form methodpost就必须同步改写doPost()—— 这正是理解 HTTP 方法语义的第一课。2.1 商品目录初始化ServletContext 的全局数据托管商品信息不能每次请求都硬编码必须在应用启动时一次性加载。我们利用ServletContextListener在contextInitialized()中初始化一个MapString, Product键为商品 ID如p001值为封装了名称、价格、库存的Product对象。该 Map 存入ServletContext供所有 Servlet 共享读取。// src/com/listener/ContextLoaderListener.java public class ContextLoaderListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { ServletContext ctx sce.getServletContext(); MapString, Product products new HashMap(); products.put(p001, new Product(p001, 无线鼠标, 89.9, 150)); products.put(p002, new Product(p002, 机械键盘, 329.0, 80)); products.put(p003, new Product(p003, 27寸显示器, 1299.0, 25)); ctx.setAttribute(productMap, products); // 全局只存一份 } }web.xml中注册监听器listener listener-classcom.listener.ContextLoaderListener/listener-class /listener设计说明ServletContext是 Web 应用的“总控室”其生命周期与 Tomcat 启动/停止完全一致。这里存放的是只读商品目录不涉及并发修改因此无需加锁。学生常犯错误是把购物车也放这里——那所有用户就共用一个购物车了这是典型的需求理解偏差。2.2 购物车数据结构Session 中的 Cart 对象封装每个用户的购物车是一个独立对象类型为Cart内部用MapString, CartItem存储键为商品 ID值为CartItem含商品 ID、数量、单价、小计。Cart类提供addItem(String productId, int quantity)和getItems()方法屏蔽底层操作细节。// src/com/model/Cart.java public class Cart { private MapString, CartItem items new HashMap(); public void addItem(String productId, int quantity, double unitPrice) { if (items.containsKey(productId)) { CartItem existing items.get(productId); existing.setQuantity(existing.getQuantity() quantity); } else { items.put(productId, new CartItem(productId, quantity, unitPrice)); } } public CollectionCartItem getItems() { return items.values(); } public double getTotalAmount() { return items.values().stream() .mapToDouble(CartItem::getSubtotal) .sum(); } }关键参数说明addItem()方法中unitPrice必须传入而非从productMap查——因为价格可能随促销变动购物车应记录下单瞬间的价格快照。这也是为什么CartItem要冗余存储单价而不是只存 ID 去实时查。2.3 请求流转控制forward 与 redirect 的选择逻辑用户点击“添加到购物车”链接URL 形如/AddToCartServlet?pidp001qty1。AddToCartServlet处理完后必须跳转回购物车展示页。这里有两个选择request.getRequestDispatcher(/ShowCartServlet).forward(request, response)服务器端内部跳转URL 不变request域对象可传递response.sendRedirect(/ShowCartServlet)客户端重定向URL 变为/ShowCartServletrequest域丢失但能防止表单重复提交F5 刷新不会再次添加。本方案采用redirect理由很实际避免用户误刷新导致重复加购。虽然forward能省一次 HTTP 请求但在购物车这种有副作用的操作中安全优先于性能。// AddToCartServlet.java 片段 String pid request.getParameter(pid); String qtyStr request.getParameter(qty); int qty Integer.parseInt(qtyStr); // 1. 从ServletContext获取商品信息 ServletContext ctx getServletContext(); MapString, Product productMap (MapString, Product) ctx.getAttribute(productMap); Product product productMap.get(pid); // 2. 获取或创建用户Session中的Cart HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } // 3. 添加商品 cart.addItem(pid, qty, product.getPrice()); // 4. 重定向到展示页防F5重复提交 response.sendRedirect(request.getContextPath() /ShowCartServlet);设计说明request.getSession()默认true即无 Session 时自动创建。session.setAttribute(cart, cart)将购物车绑定到当前用户会话后续请求只要携带同一 JSESSIONID就能拿到这个 cart。Tomcat 默认用 Cookie 存储 JSESSIONID若禁用 Cookie则回退 URL 重写response.encodeRedirectURL()本方案暂不处理此分支但你要知道它的存在。3. 核心模块实现从 HTML 页面到 Servlet 的完整链路本章带你走通一条真实请求用户打开shop.html→ 点击“添加”链接 →AddToCartServlet解析参数 → 更新 Session →ShowCartServlet渲染表格。每一步都给出可运行代码并解释“为什么这么写”。3.1 商品展示页 shop.html静态页面如何携带动态参数shop.html是纯静态 HTML不依赖 JSP但需通过超链接 URL 携带商品 ID 和默认数量。链接格式必须为AddToCartServlet?pidp001qty1且pid必须与ServletContext中初始化的商品 ID 完全一致大小写敏感。!-- WebContent/shop.html -- !DOCTYPE html html headtitle商品列表/title/head body h2欢迎选购/h2 table border1 trth商品ID/thth名称/thth价格/thth操作/th/tr tr tdp001/td td无线鼠标/td td¥89.90/td tda hrefAddToCartServlet?pidp001qty1添加到购物车/a/td /tr tr tdp002/td td机械键盘/td td¥329.00/td tda hrefAddToCartServlet?pidp002qty1添加到购物车/a/td /tr tr tdp003/td td27寸显示器/td td¥1299.00/td tda hrefAddToCartServlet?pidp003qty1添加到购物车/a/td /tr /table pa hrefShowCartServlet查看购物车/a/p /body /html设计说明此处未用 JavaScript 动态生成链接是因为本方案定位“零框架、纯 Servlet”。若你后续想支持“输入数量再添加”则需改用form提交并在AddToCartServlet中处理doPost()。当前链接方式最简且符合题目“超链接跳转”的原始需求。3.2 AddToCartServlet参数校验与会话绑定的实战细节AddToCartServlet是第一个真正处理业务逻辑的 Servlet。它必须做三件事参数非空校验、商品存在性检查、购物车初始化与更新。漏掉任何一项都会在测试时出现 500 错误或数据错乱。// src/com/servlet/AddToCartServlet.java WebServlet(/AddToCartServlet) public class AddToCartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数强制非空校验 String pid request.getParameter(pid); String qtyStr request.getParameter(qty); if (pid null || pid.trim().isEmpty() || qtyStr null) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 缺少必要参数: pid 或 qty); return; } // 2. 转换数量捕获 NumberFormatException int qty; try { qty Integer.parseInt(qtyStr.trim()); if (qty 0) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 数量必须大于0); return; } } catch (NumberFormatException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 数量格式错误); return; } // 3. 从ServletContext获取商品目录 ServletContext ctx getServletContext(); SuppressWarnings(unchecked) MapString, Product productMap (MapString, Product) ctx.getAttribute(productMap); if (productMap null || !productMap.containsKey(pid)) { response.sendError(HttpServletResponse.SC_NOT_FOUND, 商品不存在: pid); return; } Product product productMap.get(pid); // 4. 获取或创建用户购物车 HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } // 5. 添加商品注意传入当前价格非查表 cart.addItem(pid, qty, product.getPrice()); // 6. 重定向URL变为 /ShowCartServlet response.sendRedirect(request.getContextPath() /ShowCartServlet); } }关键参数说明request.getParameter()返回String必须显式trim()再判空否则用户输空格也会导致Integer.parseInt()报NumberFormatException。sendError()直接返回 HTTP 错误码比printStackTrace()更规范也便于前端识别。3.3 ShowCartServlet从 Session 读取并构造响应数据ShowCartServlet不处理业务逻辑只做数据搬运和视图准备。它从 Session 中取出Cart计算总价然后将Cart对象和totalAmount放入request域再forward给cart.jsp或纯 HTML 内联 JS本方案用后者简化。// src/com/servlet/ShowCartServlet.java WebServlet(/ShowCartServlet) public class ShowCartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从Session获取购物车 HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null || cart.getItems().isEmpty()) { // 购物车为空直接显示空提示 request.setAttribute(emptyCart, true); } else { // 2. 计算总价并放入request域 request.setAttribute(cartItems, cart.getItems()); request.setAttribute(totalAmount, cart.getTotalAmount()); } // 3. 转发给展示页注意forward不改变URL request.getRequestDispatcher(/cart.html).forward(request, response); } }cart.html中用原生 JS 渲染表格因不依赖 JSP故用 JS 动态生成!-- WebContent/cart.html -- !DOCTYPE html html headtitle购物车/title/head body h2您的购物车/h2 div idcart-content/div script // 模拟从服务端传来的数据实际项目中应由AJAX获取 const cartItems ${requestScope.cartItems ! null ? JSON.parse(\ new Gson().toJson(requestScope.cartItems) \) : []}; const totalAmount ${requestScope.totalAmount ! null ? requestScope.totalAmount : 0}; const container document.getElementById(cart-content); if (cartItems.length 0) { container.innerHTML p购物车为空请先a hrefshop.html选购商品/a/p; } else { let table table border1trth商品ID/thth名称/thth单价/thth数量/thth小计/th/tr; cartItems.forEach(item { table trtd${item.productId}/tdtd${item.productName}/td td¥${item.unitPrice.toFixed(2)}/tdtd${item.quantity}/td td¥${item.subtotal.toFixed(2)}/td/tr; }); table trtd colspan4 alignrightstrong总计/strong/td tdstrong¥${totalAmount.toFixed(2)}/strong/td/tr/table; container.innerHTML table; } /script /body /html设计说明此处cart.html用 JS 渲染是权衡之举。若用 JSP可直接c:forEach但本方案坚持“零标签库”故用 JS 模拟。注意${}是 JSP 表达式若你坚持纯 HTML则需改用response.getWriter().println()输出完整 HTML 字符串——这正是 Servlet 原始方式也是你理解PrintWriter作用的必经之路。4. 避坑指南五个让初学者当场懵圈的典型问题与根因解决写购物车看似简单但 Servlet 生命周期、Session 作用域、URL 编码、重定向陷阱这些概念一叠加90% 的学生会在调试阶段卡住。以下是我在带某高校课程设计时学生提问频率最高的五个问题按“现象→原因→解决”结构整理全是血泪经验。4.1 现象点击“添加”后页面空白浏览器地址栏变成/AddToCartServlet?pidxxx原因AddToCartServlet中未调用response.sendRedirect()或forward()Servlet 执行完后response未写入任何内容Tomcat 默认返回空白 200 响应。解决检查AddToCartServlet末尾是否遗漏跳转语句。务必确认sendRedirect()前没有out.println()或response.getWriter().write()否则会报IllegalStateException: Cannot forward after response has been committed。4.2 现象用户 A 加了商品用户 B 刷新cart.html也看到 A 的商品原因购物车对象被错误地存入ServletContext全局而非HttpSession用户级。例如写了ctx.setAttribute(cart, cart)。解决全文搜索setAttribute确认所有购物车存储操作都是session.setAttribute(cart, cart)。ServletContext只存productMap这类只读全局数据。4.3 现象request.getParameter(pid)总是返回null原因HTML 中链接 URL 写错常见错误包括链接写成AddToCartServlet?PIDp001参数名大小写不匹配Servlet 中写的是pid链接写成AddToCartServlet?pidp001qty1extra后多了一个等号导致 Tomcat 解析失败使用了中文或特殊字符但未 URL 编码如写成全角符号。解决用浏览器开发者工具 Network 标签页点击链接后看 Request URL 是否与代码中getParameter(pid)的键名完全一致区分大小写。URL 中所有非 ASCII 字符必须URLEncoder.encode()。4.4 现象添加商品后购物车数量总是 1无法累加原因Cart.addItem()方法中每次都是新建CartItem未检查Map中是否已存在该pid。或者addItem()逻辑写成items.put(productId, new CartItem(...))覆盖了旧值。解决检查Cart.java中addItem()方法必须包含if (items.containsKey(productId)) { ... } else { ... }分支。打印items.size()调试确认 Map 是否真的在增长。4.5 现象重启 Tomcat 后购物车数据还在或者关闭浏览器再打开购物车没了原因前者是ServletContext数据未清空正常因productMap是全局只读后者是HttpSession默认超时 30 分钟关闭浏览器会丢失 JSESSIONID Cookie新会话无购物车。这不是 Bug是 Session 机制的正常行为。解决若需持久化应引入数据库若仅教学演示可在web.xml中配置session-configsession-timeout60/session-timeout/session-config延长超时。但请记住Session 本质是内存状态不可靠生产环境必须落库。5. 验收与答辩用三张表、两个动作、一次全流程演示拿下高分答辩不是背代码是证明你“懂需求、控流程、能排错”。我带过的某跨平台系统课设中A同学用以下方法让导师当场点头——不是因为他代码多炫而是他把验收标准具象成了可执行、可验证的动作。你也照做效果立竿见影。5.1 验收自检三张表覆盖功能、边界、异常别等答辩才测试。现在就拿出纸笔按下面三张表逐项打钩。每项都对应一个真实 URL 和预期输出错一项就回去改直到全部通过。测试类型操作步骤预期结果关键观察点基础功能1. 启动 Tomcat访问http://localhost:8080/shop010/shop.html2. 点击“无线鼠标”的添加链接页面跳转至cart.html表格显示一行p001无线鼠标累加逻辑1. 在cart.html页面再次点击shop.html中“无线鼠标”的添加链接2. 刷新cart.html表格中p001行数量变为2小计¥179.80总计¥179.80addItem()正确识别已有商品并累加非覆盖异常防护1. 手动在浏览器地址栏输入http://localhost:8080/shop010/AddToCartServlet?pidp999qty12. 输入http://localhost:8080/shop010/AddToCartServlet?pidp001qtyabc返回 HTTP 404 或 400 错误页明确提示“商品不存在”或“数量格式错误”sendError()生效未抛出 500 内部错误注意测试时务必用无痕窗口避免旧 Session 干扰。每次测试前清空浏览器 Cookie确保从干净状态开始。5.2 答辩现场两个必做动作画图 截图导师最怕听空泛描述。你上台后第一句话不是“我的系统实现了购物车”而是“请看这张图它展示了数据从用户点击到页面渲染的完整链路。” 然后快速手绘或提前准备好 PPT 图[shop.html] ↓ (a hrefAddToCartServlet?pidp001qty1) [AddToCartServlet] → 校验参数 → 读 productMap → 更新 session.cart ↓ (response.sendRedirect) [ShowCartServlet] → 读 session.cart → setAttribute → forward to cart.html ↓ (JSP/JS 渲染) [cart.html] ← 显示表格第二动作打开本地 Tomcat现场演示一次全流程清空浏览器 Cookie访问shop.html点两次“无线鼠标”添加切到cart.html展示数量为 2手动改 URL 为?pidp999触发 404。整个过程不超过 90 秒但比讲十分钟原理更有说服力。5.3 答辩高频问题预判与应答策略学生最慌的是被问倒。其实导师问题就那么几类答案早藏在你的代码里Q为什么不用数据库A“本次设计目标是掌握 Servlet 会话管理与请求流转数据库会掩盖HttpSession和ServletContext的核心机制。若需持久化我已在Cart类中预留了saveToDB()接口只需注入 DAO 即可扩展。”Q如果用户禁用 Cookie 怎么办A“Tomcat 会自动启用 URL 重写response.encodeRedirectURL()可将 JSESSIONID 编码进 URL。本方案未实现但我知道它是sendRedirect()的可选包装属于 Session 机制的兜底策略。”Q购物车线程安全吗A“单个用户 Session 中的 Cart 是线程安全的因为 Tomcat 为每个请求分配独立线程且 Cart 不被其他 Session 共享。若未来需支持同一用户多标签页并发操作我会对Cart.addItem()加synchronized或改用ConcurrentHashMap。”从那以后我每次带学生做 Web 课设都强制他们先填完这三张表、手绘一次流程图、录一段 90 秒演示视频。不是为了应付而是逼自己把“写出来”变成“讲明白”。希望帮到你。本文还有配套的精品资源点击获取