简介:这份购物车案例【简单版】压缩包面向Java Web初学者与课程设计者,提供一套基于JSP、Servlet、Tomcat与数据库的入门级电商购物车实现,帮助理解动态网页、请求处理与用户状态保持的完整链路。包内共62个文件,以26个class与13个java源码为主体,配合6个jsp页面、5个xml配置、4个jar依赖,另有少量js、html与工程配置文件,整体约916KB,结构覆盖entity、dao、service、servlet等分层模块,便于按层阅读与调试。目前已有670人学习下载。通过该案例,读者可掌握商品列表展示、加入与删除购物车、Session同步购物车状态等核心逻辑,并了解JSP与Servlet在Tomcat中的协同方式,适合作为Java Web入门练手或课程设计参考。
1. 从一份 JSP 购物车源码说起:为什么它值得你花半小时跑一遍
如果你正在找一份能直接跑起来的 JSP 入门项目,这份「购物车案例【简单版】」大概率就是你要的东西。它没有花哨的前端框架,也没有复杂的后端分层,核心就是 JSP + Java + Tomcat + HTML + Session 这套经典组合。别小看这套组合,很多做了两三年 CRUD 的人,真让他从零手写一个不依赖任何框架的购物车,Session 怎么存、商品列表怎么渲染、数量怎么增减,照样会卡壳。这个案例的价值恰恰在于:它把「用户登录态 → 商品浏览 → 加入购物车 → 修改数量 → 结算」这条完整链路,用最朴素的 Servlet/JSP 方式摊开给你看。适合谁?适合刚学完 JSP 语法、想找个完整小项目练手的学生,也适合想回头补一补 Web 基础、理解 Session 会话机制到底怎么落地的开发者。它不解决高并发,也不教你分布式,但它能让你把「浏览器和服务器之间到底发生了什么」这件事看得明明白白。
2. 项目结构与技术选型:一个典型 JSP 项目的骨架长什么样
2.1 目录结构拆解
拿到这份源码,解压后大概率是下面这种结构(不同 IDE 导出可能略有差异,但核心目录跑不掉):
shopping-cart/ ├── src/ │ └── com/ │ └── cart/ │ ├── entity/ │ │ ├── Product.java // 商品实体 │ │ └── CartItem.java // 购物车条目实体 │ ├── servlet/ │ │ ├── ProductServlet.java // 商品列表与加入购物车 │ │ └── CartServlet.java // 购物车增删改查 │ └── util/ │ └── DBUtil.java // 数据库连接工具(若有) ├── WebContent/ (或 webapp/) │ ├── css/ │ │ └── style.css │ ├── images/ │ ├── index.jsp // 商品首页 │ ├── cart.jsp // 购物车页面 │ ├── login.jsp // 登录页(可选) │ └── WEB-INF/ │ ├── web.xml // 部署描述符 │ └── lib/ // 依赖 jar 包 └── .classpath / .project // Eclipse 工程文件这个结构是 Eclipse 动态 Web 项目的标准形态。src放 Java 源码,WebContent放静态资源和 JSP,WEB-INF下的web.xml负责 Servlet 映射。如果你用的是 IDEA,导入时选「Eclipse 项目」或者手动把WebContent指定为 Web 资源目录即可。
2.2 技术栈逐项说明
| 技术 | 作用 | 版本建议 |
|---|---|---|
| JSP | 页面渲染,嵌入 Java 代码 | 2.3+ |
| Servlet | 处理请求、控制跳转 | 3.1+ |
| Tomcat | Web 容器 | 8.5 或 9.0 |
| Session | 存储购物车数据 | 内置 |
| HTML/CSS | 页面结构与样式 | HTML5 |
| JDK | 编译运行环境 | 1.8 及以上 |
这里有个血泪经验:Tomcat 10 之后包名从javax.servlet变成了jakarta.servlet,如果你拿到的源码是老的javax写法,直接扔进 Tomcat 10 会报ClassNotFoundException。稳妥做法是用 Tomcat 8.5 或 9.0,或者手动把 import 全部改掉。新手最容易在这里翻车,明明代码没问题,就是跑不起来,排查半天发现是容器版本不匹配。
2.3 数据流转的核心思路
整个项目的灵魂在于 Session。用户第一次访问时,服务器创建一个 Session 对象,购物车用一个List<CartItem>或者Map<Integer, CartItem>存进去。之后每次请求,服务器通过 JSESSIONID 找到对应的 Session,取出购物车数据。这意味着你不需要数据库也能跑通整个流程——当然,商品列表通常还是从数据库或静态集合里读的。
常见做法是:商品数据放在一个ProductServlet里初始化,或者直接写死在 JSP 里模拟;购物车数据全部走 Session。这样做的优点是简单直接,缺点是服务器重启后购物车清空、多台服务器无法共享 Session。但对于一个「简单版」案例来说,这些都不是问题,反而降低了上手门槛。
3. 环境搭建与启动:从解压到浏览器看到商品列表
3.1 准备工作:JDK、Tomcat、IDE
先把三样东西备齐。JDK 装 1.8 或 11 都行,装完命令行敲java -version确认。Tomcat 下载解压版,别下安装版,解压版更好控制。IDE 用 Eclipse 或 IDEA 都可以,Eclipse 对老 JSP 项目兼容性更好,IDEA 则需要手动配置 Facet。
配置 Tomcat 环境变量不是必须的,但建议配一下CATALINA_HOME,指向解压目录。启动时进bin目录,Windows 双击startup.bat,Mac/Linux 执行./startup.sh。看到控制台输出Server startup in xxx ms就算成功了,浏览器访问http://localhost:8080能看到 Tomcat 默认页即可。
3.2 导入项目并配置运行环境
以 Eclipse 为例,File → Import → Existing Projects into Workspace,选中解压后的项目根目录。导入后右键项目 →Properties → Project Facets,确认Dynamic Web Module版本和Java版本匹配你的环境。如果报错说找不到javax.servlet包,去Build Path → Add Library → Server Runtime,把 Tomcat 加进去。
IDEA 的话稍微麻烦一点:File → New → Project from Existing Sources,选项目目录,识别为 Eclipse 项目。然后在Project Structure → Modules里,把WebContent标记为 Web 资源目录,web.xml路径指到WEB-INF/web.xml。最后在Run/Debug Configurations里新建一个 Tomcat Local,把项目 artifact 加进去。
3.3 启动与验证:看到商品列表才算成功
配置好后,右键项目 →Run As → Run on Server,选 Tomcat,启动。浏览器自动打开或手动访问:
http://localhost:8080/shopping-cart/index.jsp如果页面正常显示商品列表,说明环境没问题。如果 404,检查项目上下文路径(Context Path)是不是shopping-cart,可以在web.xml或 Tomcat 配置里改。如果 500,看控制台堆栈,大概率是某个类没编译或者 jar 包缺失。
提示:第一次跑建议先把
index.jsp里的数据库连接代码注释掉,用静态商品列表测试。很多源码包里带的数据库脚本不一定和你的 MySQL 版本兼容,先跑通页面逻辑,再回头接数据库。
4. 核心模块拆解:Session 购物车的增删改查怎么写的
4.1 商品实体与购物车条目实体
先看两个最基础的 JavaBean。Product代表商品,CartItem代表购物车里的一行——它比Product多了一个quantity字段。
// Product.java public class Product { private int id; private String name; private double price; private String image; // 构造方法、getter/setter 省略 public Product(int id, String name, double price, String image) { this.id = id; this.name = name; this.price = price; this.image = image; } // getter/setter... }// CartItem.java public class CartItem { private Product product; // 关联的商品 private int quantity; // 购买数量 public CartItem(Product product, int quantity) { this.product = product; this.quantity = quantity; } // 计算该条目小计金额 public double getSubtotal() { return product.getPrice() * quantity; } // getter/setter... }CartItem里放的是Product引用而不是商品 ID,这样在 JSP 里渲染时可以直接item.getProduct().getName(),省去一次查询。代价是 Session 里存的数据稍微大一点,但对于简单案例完全可接受。getSubtotal()方法把计算逻辑收在实体内部,JSP 页面只负责展示,这是最基本的职责分离。
4.2 加入购物车的 Servlet 逻辑
核心逻辑在CartServlet的add方法里。它要做三件事:拿到商品 ID、从 Session 取购物车、判断商品是否已在购物车中。
// CartServlet.java 片段 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); HttpSession session = request.getSession(); // 从 Session 获取购物车,没有就新建 List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } if ("add".equals(action)) { int productId = Integer.parseInt(request.getParameter("id")); Product product = ProductService.getById(productId); // 模拟获取商品 boolean found = false; for (CartItem item : cart) { if (item.getProduct().getId() == productId) { item.setQuantity(item.getQuantity() + 1); // 已存在则数量+1 found = true; break; } } if (!found) { cart.add(new CartItem(product, 1)); // 不存在则新增条目 } response.sendRedirect("cart.jsp"); // 跳转到购物车页面 } }这段代码有几个关键点。第一,request.getSession()默认会创建 Session,如果只是想读取可以用getSession(false)。第二,购物车用List存储,查找时遍历比对 ID,数据量小的时候没问题,商品多了建议换成Map<Integer, CartItem>,查找复杂度从 O(n) 降到 O(1)。第三,sendRedirect用的是重定向而不是转发,目的是防止用户刷新页面时重复提交。这是购物车场景的经典处理方式,新手经常在这里踩坑——用forward的话,刷新一次就多加一件商品。
4.3 修改数量与删除条目的实现
购物车页面通常有「+」「-」按钮和「删除」链接,对应update和delete两个 action。
if ("update".equals(action)) { int productId = Integer.parseInt(request.getParameter("id")); int newQty = Integer.parseInt(request.getParameter("qty")); Iterator<CartItem> it = cart.iterator(); while (it.hasNext()) { CartItem item = it.next(); if (item.getProduct().getId() == productId) { if (newQty <= 0) { it.remove(); // 数量减到 0 直接移除 } else { item.setQuantity(newQty); } break; } } response.sendRedirect("cart.jsp"); } if ("delete".equals(action)) { int productId = Integer.parseInt(request.getParameter("id")); cart.removeIf(item -> item.getProduct().getId() == productId); response.sendRedirect("cart.jsp"); }用Iterator遍历时删除元素是 Java 集合操作的基本功,直接for-each里调remove会抛ConcurrentModificationException。removeIf是 Java 8 的写法,更简洁。数量减到 0 时自动移除条目,这个细节让用户体验更顺滑,不然购物车里会留一行数量为 0 的僵尸数据。
4.4 JSP 页面如何渲染购物车
cart.jsp的职责是把 Session 里的List<CartItem>遍历出来,算总价。
<%@ page import="java.util.*, com.cart.entity.*" %> <% List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); double total = 0; if (cart != null && !cart.isEmpty()) { for (CartItem item : cart) { total += item.getSubtotal(); %> <tr> <td><%= item.getProduct().getName() %></td> <td>¥<%= item.getProduct().getPrice() %></td> <td> <a href="CartServlet?action=update&id=<%= item.getProduct().getId() %>&qty=<%= item.getQuantity() - 1 %>">-</a> <%= item.getQuantity() %> <a href="CartServlet?action=update&id=<%= item.getProduct().getId() %>&qty=<%= item.getQuantity() + 1 %>">+</a> </td> <td>¥<%= item.getSubtotal() %></td> <td><a href="CartServlet?action=delete&id=<%= item.getProduct().getId() %>">删除</a></td> </tr> <% } } %> <p>总计:¥<%= total %></p>JSP 里写 Java 代码虽然现在不推荐,但作为学习材料,它把「页面怎么拿到 Session 数据」这件事暴露得很清楚。session是 JSP 内置对象,不用声明就能用。数量增减直接通过 URL 参数传给 Servlet,Servlet 处理完重定向回cart.jsp,页面重新渲染。这个「请求 → 处理 → 重定向 → 重新渲染」的循环,就是最经典的 Web 交互模型。
5. 避坑与常见问题:跑不起来时先查这几处
5.1 404 错误:路径到底写对没有
现象是浏览器显示HTTP Status 404,控制台没有异常堆栈。原因通常是访问路径和实际部署路径不一致。比如项目上下文是/shopping-cart,你访问的是/cart/index.jsp,那肯定找不到。解决方法是去 Tomcat 的webapps目录看实际部署的文件夹名,或者在 IDE 的 Server 配置里确认 Path。另一个常见原因是web.xml里 Servlet 映射写错了,url-pattern和表单action对不上。
5.2 中文乱码:POST 和 GET 处理方式不一样
现象是商品名称显示成问号或乱码。原因是请求和响应的字符编码没设置。POST 请求在 Servlet 里加request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8")。GET 请求在 Tomcat 8 之前需要改server.xml的Connector加URIEncoding="UTF-8",Tomcat 8 之后默认就是 UTF-8。JSP 页面头部也要加<%@ page contentType="text/html;charset=UTF-8" %>。三处都设了,基本不会乱码。
5.3 Session 丢失:购物车突然空了
现象是加了商品,跳转后购物车还是空的。原因可能是浏览器禁用了 Cookie,导致 JSESSIONID 传不过去。解决方法是改用 URL 重写,在链接后拼;jsessionid=xxx,或者干脆检查浏览器设置。另一个原因是代码里调用了session.invalidate()或者设置了过短的超时时间。默认 Session 超时是 30 分钟,可以在web.xml里配:
<session-config> <session-timeout>60</session-timeout> </session-config>5.4 重复提交:刷新页面多加一件商品
现象是用户在购物车页面按 F5,商品数量莫名增加。原因是「加入购物车」用的是转发而不是重定向,或者表单提交后没有做重定向。解决方法是严格遵循 PRG 模式(Post-Redirect-Get):表单 POST 到 Servlet,Servlet 处理完sendRedirect到展示页面。这样刷新的是 GET 请求,不会重复提交。这个坑我在早期项目里踩过不止一次,后来养成习惯,凡是修改数据的操作,处理完一律重定向。
5.5 数据库连接失败:驱动和 URL 的坑
如果项目带了数据库功能,启动时报ClassNotFoundException: com.mysql.jdbc.Driver,说明 MySQL 驱动 jar 没放进WEB-INF/lib。如果报Access denied,检查用户名密码。如果报时区错误,URL 后面加?serverTimezone=Asia/Shanghai。MySQL 8 的驱动类名变成了com.mysql.cj.jdbc.Driver,老代码里写的com.mysql.jdbc.Driver会警告但通常还能用。稳妥做法是统一换成新驱动类名。
6. 二次开发改造:从 Session 购物车到可扩展的小型电商骨架
6.1 把购物车数据从 Session 迁移到数据库
Session 方案最大的问题是服务器重启数据就没了,而且无法在多台服务器之间共享。改造思路是建两张表:cart和cart_item,用user_id关联。用户登录后,购物车数据从数据库读取;未登录用户仍然走 Session,登录时把 Session 购物车合并到数据库。合并逻辑是遍历 Session 里的条目,数据库有则数量相加,没有则插入。
CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, cart_id INT NOT NULL, product_id INT NOT NULL, quantity INT DEFAULT 1, UNIQUE KEY uk_cart_product (cart_id, product_id) );UNIQUE KEY保证同一个购物车里同一个商品只有一行,插入时用INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + 1,一条 SQL 搞定「存在则累加,不存在则插入」。
6.2 用 Filter 统一处理编码和登录校验
现在每个 Servlet 都要写一遍setCharacterEncoding,很啰嗦。可以抽一个EncodingFilter:
public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }在web.xml里配/*拦截所有请求。同理可以加一个LoginFilter,拦截购物车和结算页面,未登录用户跳转到登录页。这样 Servlet 里就不用重复写校验逻辑了。
6.3 把 JSP 里的 Java 代码抽到 Servlet 或 JSTL
cart.jsp里混着 Java 代码,维护起来很痛苦。改造方向是用 JSTL 和 EL 表达式替代:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${sessionScope.cart}" var="item"> <tr> <td>${item.product.name}</td> <td>¥${item.product.price}</td> <td>${item.quantity}</td> <td>¥${item.subtotal}</td> </tr> </c:forEach>EL 表达式${item.subtotal}会自动调用getSubtotal()方法,页面里看不到一行 Java 代码。配合 Servlet 在转发前把数据塞进request,JSP 只负责展示。这一步做完,项目才算真正有了点 MVC 的样子。
6.4 一个验证改造是否成功的小技巧
改完之后怎么确认没改坏?我的习惯是准备一份「冒烟测试清单」:打开首页 → 点三个不同商品加入购物车 → 进购物车看数量对不对 → 改一个数量 → 删一个条目 → 刷新页面看数据是否还在 → 清空购物车。这七步走完没问题,基本功能就是通的。如果接了数据库,再加一步:重启 Tomcat,看购物车数据是否还在。在的话,说明持久化生效了。
从那以后我每次改完购物车这类状态相关的代码,都强制走一遍这个清单,不管改动多小。因为购物车这东西,表面看就是增删改查,但 Session、重定向、并发修改这些点随便哪个出问题,用户看到的就是「东西没了」或者「数量不对」,而这种 bug 往往在开发阶段不容易发现。希望这份拆解能帮你少走点弯路,把这份简单版购物车真正跑起来、改起来。
本文还有配套的精品资源,点击获取