简介:购物车是电商网站的基础功能,这份用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 Map<Integer, 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 var="product" items="${productList}"> <div class="product-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" method="post"> <input type="hidden" name="productId" value="${item.product.id}"> <input type="number" name="quantity" value="${item.quantity}" min="1"> <button type="submit">更新数量</button> </form>数量输入框设置 min="1" 是前端第一道防线,阻止用户把数量改成 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 contentType="text/html;charset=UTF-8" %>。第二层,所有 Servlet 的 doGet 和 doPost 开头都加 request.setCharacterEncoding("UTF-8") 和 response.setCharacterEncoding("UTF-8")。第三层,查 Tomcat 的 server.xml,确认 配置了 URIEncoding="UTF-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 超时时间符合预期。这些操作加起来五分钟不到,但能把教具做、演示翻车这类意外全部提前消灭。希望这套思路能帮到你,带着一份能稳定运行、结构清晰、能讲清来龙去脉的方案去交付,比堆砌花哨功能更重要。
本文还有配套的精品资源,点击获取