Java Web购物车基于Session实现:JSP+Servlet完整链路
简介购物车是电商网站的基础功能这份用JSP、Servlet和Tomcat打造的简单版源码包主要面向刚接触Java Web开发的学生、自学者以及需要快速完成课程设计的人。案例围绕商品列表展示、加入购物车、删除商品、Session状态保持等核心逻辑清晰演示了一个微型购物系统从页面请求到数据回显的完整链路。压缩包共62个文件包含13个Java源文件、26个class编译文件、6个JSP页面另有XML配置、jar依赖、HTML与JavaScript等辅助资源整体约916KB体量很小便于完整阅读。已有670人浏览学习可直接作为课程设计或练手项目的参考蓝本。通过剖析源码你不仅能学会JSP与Servlet如何协同处理HTTP请求、Session如何临时存放购物车内容还能理清数据库存储商品信息的基本写法并熟悉Web项目的目录分层与前后端交互方式。1. 为什么是 Session 购物车一个 Java Web 课设题的真实取舍购物车是 Java Web 课程里出现频率最高的实训题目但只要带过一届学生就知道真正上手时最常见的翻车点不在 JSP 标签也不在 SQL 语句而在“购物车数据到底该存在哪”。第一次做这个案例的 A 同学一开始想得很复杂建一张购物车表用户点一下加购就写一次数据库结果登录、商品、购物车三张表关联到一半就卡住了。这个案例采用了一种看起来更简单但更符合教学目标的方案用 Session 保存购物车配合 JSP、Servlet、Tomcat 和 HTML 静态页面把完整流程串起来。方案的核心思路是把购物车当成“会话级数据”用户登录后由服务器在内存里维护一份购物车对象关掉浏览器或会话超时后自动释放。它适合两类人正在做课设、需要快速跑通完整流程的学生以及想给团队做入门参考、展示 JSP Servlet Session 经典组合怎么落地的开发者。2. 总体设计从需求拆分到四个模块与数据模型2.1 需求边界这份“简单版”到底包含哪些功能拿到案例压缩包后第一步不是看代码而是把需求边界划清楚。这个案例定位是“简单版”所以功能范围刻意做了收敛用户登录、商品列表浏览、加入购物车、修改购物车数量、删除购物车条目、清空购物车、结算时展示订单信息。没有用户注册、没有支付对接、没有商品分类、没有库存扣减。这样的边界是合理的——课设题的核心教学目标是让学生理解 Session 的用法和 JSP/Servlet 的分工而非实现一个生产级电商系统。如果评审老师要求“再加一个模块”通常是在此基础上扩展商品管理或订单表而不需要推翻原有设计。从交付角度说一个“完整”的简单版购物车应该同时具备两条链路一条是“登录 → 逛商品 → 加购 → 改数量 → 结算”的主流程另一条是“会话过期 → 重新登录”的边界流程。前者展示功能完整性后者展示对 Session 生命周期的理解这也是答辩时老师最常追问的点。2.2 技术选型为什么用 Session 而不是 Cookie 或数据库在这个案例里Session 是购物车数据的唯一载体没有任何数据库表。这不是偷懒而是刻意的设计决策。Cookie 不适合存购物车因为浏览器端数据暴露在客户端用户可以直接篡改数量甚至伪造条目同时 Cookie 有 4KB 的体积限制商品一多就存不下。数据库方案适合做持久化但会让“会话”的概念被稀释——用户没登录、临时逛逛的场景下购物车数据写进数据库反而成了负担。Session 方案的核心优势在于生命周期天然匹配购物场景用户打开浏览器访问网站时Tomcat 会自动创建一个 Session 对象用户关闭浏览器或超过默认的 30 分钟无操作时Session 自动失效。购物车随会话创建、随会话销毁不需要额外的清理任务。当然这个方案的代价也很明确服务器重启或集群部署时 Session 会丢失但作为课设演示完全没有问题。2.3 模块划分四个模块的职责与依赖关系整个案例按“展示与控制分离”的思路拆成四个模块模块之间通过 Session 传递数据避免在 JSP 页面里写业务逻辑。登录模块负责校验用户名和密码校验通过后把用户信息写入 Session标记登录状态。商品展示模块读取一个静态商品列表代码里预置了若干商品对象在 HTML 页面上渲染商品卡片每张卡片带一个“加入购物车”的按钮。购物车操作模块是核心承接添加、修改数量、删除、清空四类动作全部由 Servlet 接收请求后操作 Session 中的购物车对象再重定向回商品页或购物车页。结算模块读取购物车中的商品清单计算总价并在页面上展示订单预览。四个模块的依赖关系是单向的购物车操作模块依赖登录模块写入的用户信息结算模块依赖购物车操作模块维护的数据。商品展示模块不依赖其他模块可以直接打开。2.4 数据模型购物车对象的结构设计购物车的数据结构直接决定代码好不好写。这个案例采用“购物车对象 购物车条目对象”的两层结构而不是把商品信息直接塞进 Map。购物车条目CartItem包含四个字段商品编号、商品名称、单价、数量。这里有一个值得注意的设计细节——条目里存的商品名称和单价是“加入购物车那一刻的快照”而不是实时查询的引用。意思是说如果后面改了商品库里的价格购物车里的旧条目不会跟着变。这个设计符合实际电商的“下单锁价”逻辑也避免了 JSP 页面取值时还要二次关联。购物车对象内部用一个 Map 维护条目key 是商品编号value 是购物车条目对象。用 Map 而不是 List 的原因很好解释更新数量时只需要 get 一次再 set 数量即可不需要遍历整个列表。public class Cart implements Serializable { private MapInteger, CartItem items new HashMap(); public void add(Product product, int quantity) { CartItem item items.get(product.getId()); if (item null) { items.put(product.getId(), new CartItem(product, quantity)); } else { item.setQuantity(item.getQuantity() quantity); } } public void updateQuantity(int productId, int quantity) { CartItem item items.get(productId); if (item ! null quantity 0) { item.setQuantity(quantity); } } public void remove(int productId) { items.remove(productId); } public void clear() { items.clear(); } public double getTotalPrice() { return items.values().stream() .mapToDouble(item - item.getPrice() * item.getQuantity()) .sum(); } }Cart 类的关键点是 add 方法的合并逻辑同一个商品第二次加入时默认累加数量而不是新增一行。这是一个需要刻意设计的决策点。课设里常见的错误是把每个商品都当成独立条目结果同一个商品在购物车里出现三四行结算时总价还算得对但展示很混乱。另外Cart 实现 Serializable 接口是为了应对 Tomcat 重启时的 Session 持久化机制虽然课设一般用不到但加上不会出错。3. 核心实现登录、商品列表与购物车操作的完整代码链路3.1 登录流程Session 里到底存了什么登录模块是整条链路的起点。用户从 index.html 页面输入用户名和密码表单以 POST 方式提交到 LoginServlet。Servlet 里先做参数校验再比对预置的账号数据比对通过后执行 session.setAttribute(username, username) 和 session.setAttribute(cart, new Cart())。这里有一个容易忽略的细节购物车对象应该在登录成功时创建而不是在第一次加购时才创建。原因在于业务上只有登录用户才能使用购物车登录时初始化购物车可以保证后续所有购物车操作代码拿到的对象不可能为 null。这个细节在答辩时非常加分因为它体现了对“对象生命周期”的思考而不是简单地“用到再取”。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); if (admin.equals(username) 123456.equals(password)) { HttpSession session request.getSession(); session.setAttribute(username, username); session.setAttribute(cart, new Cart()); response.sendRedirect(request.getContextPath() /products); } else { request.setAttribute(error, 用户名或密码错误请重新输入); request.getRequestDispatcher(/login.jsp).forward(request, response); } }注意这个 doPost 里的两个典型写法。第一个是 request.setCharacterEncoding(UTF-8)必须在读取参数之前调用否则中文用户名会乱码。第二个是登录成功用 sendRedirect 重定向、失败用 forward 转发这个选择不是随意的——重定向会发起一次新的请求避免用户刷新页面时表单重复提交而转发保留请求域中的错误提示让登录页能展示错误信息。方向不同语义不同。3.2 商品列表静态数据与 HTML 渲染的配合商品展示模块没有连接数据库而是在 ProductServlet 中维护了一个 ArrayList 商品列表。每个商品包含 id、名称、价格、图片路径四个属性。为什么这么做因为课设阶段引入数据库会让整个案例的复杂度翻倍而且数据库表结构会分散学生对 Session 的注意力。在线演示或答辩场景下静态数据加载更快、更稳定不会出现数据库没启动导致演示翻车的尴尬。商品列表页由 JSP 渲染核心是 JSTL 的 c:forEach 标签。这里要区分 HTML 和 JSP 的角色页面的静态框架用 HTML 写动态渲染部分用 JSP 标签完成。c:forEach varproduct items${productList} div classproduct-card img src${product.imagePath} alt${product.name} h3${product.name}/h3 p价格${product.price}/p a href${pageContext.request.contextPath}/addCart?productId${product.id}加入购物车/a /div /c:forEach这里有两个必须写规范的点。第一是 href 里必须带 ${pageContext.request.contextPath}也就是项目上下文路径。如果不带部署时项目名一旦不是 ROOT所有商品链接都会 404。第二是加购请求用 GET 方式这是为了演示方便让用户可以直接点击链接触达。但要注意GET 请求改变服务器状态本来不是最佳实践所以后面会看到添加成功后立刻重定向把影响降到最低。3.3 加购操作从请求到 Session 更新的完整链路AddCartServlet 接收商品 id 参数后先从 Session 中取购物车对象再从商品列表中查对应商品最后调用 cart.add(product, 1) 完成加购。这个链路的三步分别测试了“Session 是否正常”“商品数据是否完整”“Cart 对象的 add 逻辑是否正确”。protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } int productId Integer.parseInt(request.getParameter(productId)); Product product ProductDataSource.findById(productId); if (product ! null) { cart.add(product, 1); } response.sendRedirect(request.getContextPath() /products); }这里保留了 cart null 时的兜底逻辑虽然理论上登录成功时已经创建过购物车但兜底代码让这个 Servlet 在未登录状态被直接访问时也不会空指针崩溃。每次加购成功后统一重定向回商品列表页这是经典的 POST-REDIRECT-GET 模式——不过这个案例用得不够彻底因为加购本身是 GET 请求。重定向的实际价值在于用户点击浏览器的刷新按钮时重复执行的是商品列表请求而不是再次加购。如果不重定向、直接 forward 到商品页刷新一次就会重复加购一次购物车数据会越积越多这就是一个隐性 bug。3.4 购物车页面与结算预览数量修改和总价计算购物车页面 cart.jsp 读取 Session 中的 Cart 对象遍历条目生成表格。每一行包含商品名、单价、数量输入框、小计、删除操作。数量修改通过表单提交到 UpdateCartServlet删除是单独的小表单提交到 RemoveCartServlet。form action${pageContext.request.contextPath}/updateCart methodpost input typehidden nameproductId value${item.product.id} input typenumber namequantity value${item.quantity} min1 button typesubmit更新数量/button /form数量输入框设置 min1 是前端第一道防线阻止用户把数量改成 0 或负数。但后端的 UpdateCartServlet 里仍然要做二次校验因为请求可以被手工构造。后端校验逻辑是quantity 参数能解析为整数且大于 0 才执行更新否则忽略本次修改。结算模块在页面底部显示商品总件数和总金额数据直接来自 Cart 对象不查数据库。这是整个案例里最直观的“内存数据驱动页面”的例子也是演示时最容易被老师注意到的亮点——数据从用户操作到 Session再到页面展示整个闭环不落地数据库。4. 高频踩坑与排查Session 失效、乱码与重复提交4.1 Session 无故失效购物车说没就没了现象用户正在浏览商品、往购物车里加了几件商品去倒杯水回来刷新页面购物车空了登录状态也没了。原因Tomcat 默认的 Session 超时时间是 30 分钟。这 30 分钟按“最后一次请求”计算不是按登录时间算。如果用户停留在一个静态页面超过 30 分钟没有任何请求Session 就会被服务器回收。另一个隐蔽原因是 Tomcat 的 web.xml 里配置了全局的 session-timeout如果之前部署过其他项目可能会把超时时间改成 5 分钟。解决开发调试阶段在项目自己的 web.xml 里显式延长超时时间比如设置 120 分钟。生产环境保持默认 30 分钟就好让购物车随会话过期是合理行为。排查时先看 Tomcat 控制台有没有 Session 销毁日志再看 web.xml 配置不要上来就改代码。4.2 商品名称为中文时页面显示乱码现象商品名称是“无线鼠标”页面显示成“鏃犵嚎榧犳爣”或一堆问号。原因乱码问题 90% 出在编码不一致上。JSP 文件保存时用了 UTF-8但 Tomcat 接收请求参数时默认用 ISO-8859-1 解码或者 HTML 页面的 声明和后端不一致。解决三层交叉检查。第一层所有 JSP 页面和 Java 文件统一用 UTF-8 编码保存且 JSP 顶部声明 % page contentTypetext/html;charsetUTF-8 %。第二层所有 Servlet 的 doGet 和 doPost 开头都加 request.setCharacterEncoding(UTF-8) 和 response.setCharacterEncoding(UTF-8)。第三层查 Tomcat 的 server.xml确认 配置了 URIEncodingUTF-8。三层都对齐后乱码基本绝迹。4.3 浏览器禁用 Cookie 后Session 彻底失效现象某台电脑上的浏览器禁用了 Cookie登录成功后跳转到商品页再点击“加入购物车”直接跳回登录页或报 500 错误。原因Session 机制的底层依赖 Cookie。服务器创建 Session 后通过名为 JSESSIONID 的 Cookie 把会话标识发给浏览器浏览器后续请求自动携带这个 Cookie服务器才能识别出“你是同一个用户”。Cookie 被禁用后服务器每次都会创建新 Session购物车自然就丢了。解决在 Servlet 的输出链接上套用 response.encodeURL() 方法。这个方法会自动判断——如果浏览器支持 Cookie就原样输出地址如果检测到 Cookie 不可用就自动把 Session ID 拼在 URL 后面实现 URL 重写。课设阶段在链接上补上这个处理已经足够不需要额外引入别的会话跟踪方案。4.4 刷新页面导致商品重复加入购物车现象点击一次“加入购物车”商品数量变成 2 甚至更多抓包发现浏览器发出了相同的加购请求。原因加购是一个 GET 请求浏览器会缓存这个请求。用户点击“加入购物车”后Servlet 里如果再执行 forward 转发而不是重定向地址栏 URL 仍然是加购的地址用户按 F5 刷新时浏览器会再次执行这个 GET 请求。解决Servlet 操作完购物车后一律使用 response.sendRedirect() 重定向回商品列表页。重定向是 302 响应浏览器地址栏会变成商品列表的地址刷新时触发的是列表请求不会重复加购。这个坑在答辩中出现频率极高几乎每个演示现场都会有人好奇“为什么刷新一下数量就变多了”能主动说出原因和解法印象分直接拉满。4.5 JSP 页面里堆了一堆 Java 代码改需求时无从下手现象拿到代码后想改一个功能发现 cart.jsp 里既有 % % 脚本片段又嵌套了 HTML 标签和 EL 表达式改一处牵动全身。原因简单版案例为了减少文件数量把 Java 控制逻辑直接写进了 JSP 页面。初期跑通效果很快但后续维护成本直线上升。解决严格遵循“Servlet 负责处理逻辑JSP 只负责展示”的分工。JSP 页面中尽量使用 EL 表达式和 JSTL 标签不要出现 % % 脚本片段。如果看到页面里逻辑代码超过十行就应把这段逻辑抽回 Servlet 或 JavaBean。课设代码的评分标准里通常会有一条“代码结构清晰”这个清理动作能直接加分。5. 验收与答辩用一张测试表和一次会话串讲过关5.1 验收测试不要只看“能跑”要跑完整链路验收课设项目时很多同学的问题不是没实现功能而是没验证功能。演示的时候临时发现某个环节断了只能尴尬地解释“可能没点对”。准备的正确姿势是把主流程和边界异常分别列成测试用例逐个执行并记录结果。模块操作步骤预期结果实际结果登录输入错误密码登录页面提示错误不跳转通过登录输入正确账号密码跳转商品列表页通过加购点击商品“加入购物车”数量自动加 1页面刷新通过加购同一商品重复点击 3 次条目只有一行数量为 3通过修改数量更新数量为 5小计和总价同步变化通过删除删除某个商品条目条目消失总价重新计算通过清空点击清空购物车购物车条目清空通过会话超时停止操作超过 30 分钟购物车失效要求重新登录通过建议把这张表打印出来带到演示现场按表逐项执行。这不仅证明你系统地测试过还能避免现场演示时漏掉某个环节。5.2 答辩技巧用生命周期串讲整个项目答辩时老师最常问的一句话是“你讲讲购物车的数据在系统里是怎么流转的”很多同学上来就贴代码结果越讲越乱。推荐的串讲方式是抓住 Session 生命周期这条主线用户在登录页输入账号后Tomcat 在服务器内存中创建一个 Session 对象服务器通过响应头里的 Cookie 把会话标识传递给浏览器用户点“加入购物车”浏览器带着会话标识发起请求服务器根据标识找到 Session再从 Session 中取出购物车对象调用 add 方法更新数据之后页面展示时JSP 再次从 Session 中取出最新购物车数据完成渲染直到用户关闭浏览器或超过 30 分钟Session 被销毁购物车数据随之消失。讲完这条线以后主动问自己一个问题“Session 数据存在服务器内存里如果用两台 Tomcat 做集群第二次请求落在另一台服务器上怎么办”这个问题可能超出课设范围但提前想清楚会让你的答辩体面很多——参考答案是基于内存的 Session 无法跨节点共享生产环境通常引入集中式会话存储或改为无状态登录 前端存储购物车。能答到这一层说明你不是只会跑代码而是真的想清楚了方案的边界。5.3 一个收尾习惯带这个案例的时候我吃过一次亏验收当天发现商品图片在另一台电脑上加载不出来排查了半天是因为图片路径写的是绝对物理路径换机器就失效了。从那以后我每次验收前都会强制自己走一遍完整流程清空浏览器数据、重启 Tomcat、从登录开始按测试表手动操作一次、观察地址栏有没有出现绝对路径、确认 Session 超时时间符合预期。这些操作加起来五分钟不到但能把教具做、演示翻车这类意外全部提前消灭。希望这套思路能帮到你带着一份能稳定运行、结构清晰、能讲清来龙去脉的方案去交付比堆砌花哨功能更重要。本文还有配套的精品资源点击获取